perf(tts): reduce Kokoro time-to-first-audio with session warmup and threading (#342)

This commit is contained in:
tlongwell-block
2026-04-16 17:37:38 -04:00
committed by GitHub
parent 2506341a28
commit 70059efa4a
3 changed files with 43 additions and 1 deletions
+1 -1
View File
@@ -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) {
+23
View File
@@ -590,6 +590,21 @@ pub fn load_text_to_speech(model_dir: &str) -> Result<KokoroTTS, String> {
.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<KokoroTTS, String> {
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<KokoroTTS, String> {
// 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()))?
}
+19
View File
@@ -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;