mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-29 04:27:47 -06:00
- 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>
50 lines
1.8 KiB
Rust
50 lines
1.8 KiB
Rust
//! `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(())
|
||
}
|