diff --git a/desktop/scripts/check-file-sizes.mjs b/desktop/scripts/check-file-sizes.mjs index effe62231..ed87bce02 100644 --- a/desktop/scripts/check-file-sizes.mjs +++ b/desktop/scripts/check-file-sizes.mjs @@ -64,7 +64,7 @@ const overrides = new Map([ ["src-tauri/src/huddle/stt.rs", 580], // STT pipeline + PTT edge-detection flush + PTT gating (is_speech AND ptt_active) + barge-in for VAD mode + rubato resampler + earshot VAD + sherpa-onnx transcription ["src-tauri/src/huddle/preprocessing.rs", 670], // TTS text preprocessing pipeline + unified split_sentences + int_to_words 0-999999 + URL trailing punctuation preservation + 23 unit tests ["src-tauri/src/huddle/relay_api.rs", 510], // audio relay recv task + per-peer frame counting for remote human TTS interrupt - ["src-tauri/src/huddle/tts.rs", 1010], // TTS pipeline + cancel/shutdown handling + apply_fades + 18 unit tests for remote interrupt mechanism + ["src-tauri/src/huddle/tts.rs", 1030], // TTS pipeline + session warmup + cancel/shutdown handling + apply_fades + 18 unit tests for remote interrupt mechanism ]); async function walkFiles(directory) { diff --git a/desktop/src-tauri/src/huddle/kokoro.rs b/desktop/src-tauri/src/huddle/kokoro.rs index 9f1c138c1..464407e0d 100644 --- a/desktop/src-tauri/src/huddle/kokoro.rs +++ b/desktop/src-tauri/src/huddle/kokoro.rs @@ -590,6 +590,21 @@ pub fn load_text_to_speech(model_dir: &str) -> Result { .find(|p| p.exists()) .ok_or_else(|| format!("no model.onnx found in {model_dir}"))?; + // ── Session threading options ──────────────────────────────────────── + // + // parallel_execution — runs independent graph operators concurrently. + // Kokoro's graph is mostly sequential, so the benefit is modest, but + // it's safe and free to enable. + // intra_threads(0) — lets ONNX Runtime use all available CPU cores for + // parallelism within individual operators (e.g., large matmuls). + // + // NOTE: GraphOptimizationLevel::All is already the ort default — no + // need to set it explicitly. Accuracy-altering flags (approximate_gelu, + // flush_to_zero) are intentionally omitted — they need A/B audio + // validation before enabling for a TTS model. memory_pattern is omitted + // because Kokoro input lengths vary per sentence and the ORT docs warn + // against it for variable-size inputs. + // Try CoreML first (zero binary cost — macOS system framework). // If the model has ops CoreML can't handle (common with quantized models), // the EP registers fine but commit_from_file fails. Catch that and retry @@ -597,6 +612,10 @@ pub fn load_text_to_speech(model_dir: &str) -> Result { let session = { let mut builder_with_coreml = Session::builder() .map_err(|e| format!("session builder: {e}"))? + .with_parallel_execution(true) + .map_err(|e| format!("parallel execution: {e}"))? + .with_intra_threads(0) + .map_err(|e| format!("intra threads: {e}"))? .with_execution_providers([ort::ep::CoreML::default() .with_compute_units(ort::ep::coreml::ComputeUnits::All) .with_model_format(ort::ep::coreml::ModelFormat::MLProgram) @@ -617,6 +636,10 @@ pub fn load_text_to_speech(model_dir: &str) -> Result { // Retry without any execution providers — pure CPU. Session::builder() .map_err(|e| format!("session builder (CPU fallback): {e}"))? + .with_parallel_execution(true) + .map_err(|e| format!("parallel execution (CPU): {e}"))? + .with_intra_threads(0) + .map_err(|e| format!("intra threads (CPU): {e}"))? .commit_from_file(&model_path) .map_err(|e| format!("load model {} (CPU): {e}", model_path.display()))? } diff --git a/desktop/src-tauri/src/huddle/tts.rs b/desktop/src-tauri/src/huddle/tts.rs index bb696c1eb..6b5b1b97f 100644 --- a/desktop/src-tauri/src/huddle/tts.rs +++ b/desktop/src-tauri/src/huddle/tts.rs @@ -232,6 +232,25 @@ fn tts_worker( } }; + // ── 2b. Warmup inference ───────────────────────────────────────────────── + // The first ONNX inference on any session is significantly slower than + // subsequent ones — it triggers JIT compilation, memory pool allocation, + // and (on CoreML) lazy model compilation. Run a short dummy synthesis and + // discard the output so the first real utterance runs at warm-session speed. + { + let t = std::time::Instant::now(); + match engine.synth_chunk("warmup", "en", &style, SYNTH_STEPS, SYNTH_SPEED) { + Ok(_) => eprintln!( + "sprout-desktop: TTS warmup completed in {:.0}ms", + t.elapsed().as_millis() + ), + Err(e) => eprintln!( + "sprout-desktop: TTS warmup failed after {:.0}ms: {e} — first utterance may be slow", + t.elapsed().as_millis() + ), + } + } + // ── 3. Initialise rodio output device ───────────────────────────────────── use rodio::Player;