//! Streaming STT demo: transcribe a WAV via Kyutai's 1B en/fr model. //! //! First run downloads ~3 GB from `kyutai/stt-1b-en_fr` to the HF cache. //! //! Usage: //! ``` //! cargo run -p rtx-csm --release --features metal --example stt_demo -- \ //! --in /tmp/csm_24k.wav //! ``` //! //! Note: until sentencepiece detok is wired (Phase 6a polish), the output //! is raw token IDs per word. The first version is intentionally minimal — //! demonstrates that the streaming pipeline is connected end to end. use anyhow::Result; use clap::Parser; use rtx_csm::{audio_io, stt::{AsrEvent, Stt, SAMPLE_RATE}}; use std::path::PathBuf; #[derive(Debug, Parser)] #[command(name = "stt_demo")] struct Cli { /// Input WAV (any rate / channels — resampled to 24 kHz mono). #[arg(long = "in")] input: PathBuf, /// Force CPU device. #[arg(long)] cpu: bool, } fn main() -> Result<()> { tracing_subscriber::fmt().init(); let cli = Cli::parse(); let device = if cli.cpu { candle_core::Device::Cpu } else if candle_core::utils::metal_is_available() { candle_core::Device::new_metal(0)? } else { candle_core::Device::Cpu }; println!("device: {device:?}"); let t = std::time::Instant::now(); let mut stt = Stt::load_default(&device)?; println!("loaded Kyutai STT 1B en/fr in {:.2}s", t.elapsed().as_secs_f32()); // Load WAV at 24 kHz mono (Mimi's expected input rate). let samples = audio_io::load_mono_at_rate(&cli.input, SAMPLE_RATE)?; println!( "loaded {}: {} samples ({:.2}s @ {} Hz)", cli.input.display(), samples.len(), samples.len() as f32 / SAMPLE_RATE as f32, SAMPLE_RATE ); // Stream the audio in 1-second chunks so we can observe streaming // behavior (events arriving as the model processes). let chunk_size = SAMPLE_RATE as usize; let mut all_events: Vec = Vec::new(); let t = std::time::Instant::now(); for (i, chunk) in samples.chunks(chunk_size).enumerate() { let evs = stt.step_pcm(chunk)?; let n_step = evs.iter().filter(|e| matches!(e, AsrEvent::Step { .. })).count(); let n_word = evs.iter().filter(|e| matches!(e, AsrEvent::Word { .. })).count(); let n_end = evs.iter().filter(|e| matches!(e, AsrEvent::EndWord { .. })).count(); println!("[chunk {i}] events: step={n_step} word={n_word} endword={n_end}"); all_events.extend(evs); } let evs_finish = stt.finish()?; all_events.extend(evs_finish); println!("inference: {:.2}s", t.elapsed().as_secs_f32()); // Summary: print all Word events. Step events are noisy (one per frame). let mut words = 0usize; for ev in &all_events { match ev { AsrEvent::Word { tokens, start_time, .. } => { println!( " word @ {:.2}s: tokens={:?}", start_time, tokens ); words += 1; } AsrEvent::EndWord { stop_time, .. } => { println!(" end_word @ {:.2}s", stop_time); } AsrEvent::Step { .. } => {} } } println!("total words detected: {}", words); Ok(()) }