Files
filmcraft/crates/tts/examples/narrate.rs
Manuel CamachoandClaude Opus 5.5 beebdbcd39 M7.12: natural Text to Speech voices (Kokoro-82M in pure Rust)
- filmcraft-tts (feature kokoro): Kokoro-82M on candle, implemented from
  the StyleTTS 2 / iSTFTNet / ALBERT papers, config.json and the
  checkpoint's tensor shapes; official .pth read with candle's restricted
  pickle reader; voice packs from their zip. Neural Voice: tts-text
  phonemize -> Kokoro per chunk -> pauses -> pitch via audio-dsp.
- catalog: 9 US English voices (model card grade C+ or better) + CMUdict,
  every file pinned to a revision and SHA-256 (336 MB).
- speech: generic download_files (same verified downloader).
- engine: tts.render (background synthesis into Session::tts_cache),
  tts.downloadVoices (background job, cancellable), voices list with
  package state; feature neural-voices (default on in the desktop app).
- ui: natural/basic voice groups, consent box (size, sources, licences),
  download progress + Cancel, synthesis never blocks the window;
  ui.set {tts:{...}} for agents.

Measured: Whisper-base WER on 50 original sentences 2.1-3.6 % for all 9
voices (built-in robotic voice 96.6 %); 2.2-2.3x real time on CPU.
Gates: fmt, clippy, layers, assets, wasm pass; tests pass except
shortcuts_tests::assign_reassigns_conflicts_and_undoes (fails on main)
and the frame pool test (flaky under load, passes alone).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 06:52:30 -07:00

50 lines
1.8 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `cargo run -p filmcraft-tts --release --features kokoro --example narrate -- <models dir> <voice id> <script.txt> <out.wav>`
//!
//! Speaks a script file with any FilmCraft voice (built in or natural) through the full pipeline
//! (normalizer, pronunciation, Kokoro) and writes a mono 32-bit float WAV.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let a: Vec<String> = std::env::args().collect();
let (Some(models), Some(id), Some(script), Some(out)) = (a.get(1), a.get(2), a.get(3), a.get(4)) else {
eprintln!("usage: narrate <models dir> <voice id> <script.txt> <out.wav>");
std::process::exit(2);
};
let text = std::fs::read_to_string(script)?;
let t0 = std::time::Instant::now();
let v = filmcraft_tts::voice_in(id, Some(std::path::Path::new(models)))?;
let t1 = std::time::Instant::now();
let audio = v.synthesize(&text, &filmcraft_tts::Params::default())?;
let t2 = std::time::Instant::now();
let rate = audio.sample_rate;
let data = (audio.samples.len() * 4) as u32;
let mut w = Vec::with_capacity(44 + data as usize);
for part in [
&b"RIFF"[..],
&(36 + data).to_le_bytes(),
b"WAVEfmt ",
&16u32.to_le_bytes(),
&3u16.to_le_bytes(),
&1u16.to_le_bytes(),
&rate.to_le_bytes(),
&(rate * 4).to_le_bytes(),
&4u16.to_le_bytes(),
&32u16.to_le_bytes(),
b"data",
&data.to_le_bytes(),
] {
w.extend_from_slice(part);
}
for x in &audio.samples {
w.extend_from_slice(&x.to_le_bytes());
}
std::fs::write(out, w)?;
eprintln!(
"{id}: load {:.2}s, synth {:.2}s for {:.2}s of audio (×{:.1} real time)",
(t1 - t0).as_secs_f64(),
(t2 - t1).as_secs_f64(),
audio.seconds(),
audio.seconds() / (t2 - t1).as_secs_f64()
);
Ok(())
}