aboutsummaryrefslogtreecommitdiffstats
path: root/vendor/extra/canary-rs/examples
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-06 19:21:16 -0300
committerelvis <elvis@claros.ar>2026-09-06 19:23:37 -0300
commitf8f98e83481e7376a235fb305a095552433f79ab (patch)
tree6d0ecddb26bef8430a086431de40f8ce9b1039e6 /vendor/extra/canary-rs/examples
downloadasist-p-f8f98e83481e7376a235fb305a095552433f79ab.tar.gz
asist-p-f8f98e83481e7376a235fb305a095552433f79ab.zip
Local voice assistant on top of Canary, llama.cpp and qwentts
Pipeline en Rust de hilos y canales que une los tres motores: el micrófono alimenta un segmentador con VAD, las intervenciones cerradas van al reconocedor, la transcripción al modelo y cada frase que este cierra sale hacia el sintetizador sin esperar al resto de la respuesta. Dos reglas sostienen el diseño: ninguna etapa bloquea a la anterior —quien va sobrado descarta trabajo en lugar de acumular retraso— y todo lo que viaja por los canales lleva el turno al que pertenece, así que interrumpir es subir el contador y levantar dos banderas de cancelación. Seis crates: core (configuración, eventos, HTTP, telemetría, herramientas), audio (cpal, VAD, anillo de reproducción), asr, llm, tts y app (supervisor de procesos y orquestador). Los motores van como submódulos fijados a un commit, con los cambios locales en vendor/patches. Midiendo el pipeline aparecieron tres cuellos de botella de configuración que valieron más que cualquier cambio de código, todos documentados en docs/RENDIMIENTO.md: - tts-server decodificaba el audio en bloques de 24 s, de modo que el modo «streaming» llegaba de una pieza: 4948 ms -> 585 ms hasta el primer audio. - La plantilla de chat del modelo abre <think> y no lo cierra nunca, sin variable que lo apague: 8630 ms -> 413 ms hasta el primer token, con una copia de la plantilla que deja el bloque cerrado de entrada. - Cualquier indicación de estilo junto a la guía de herramientas hace que este modelo de 2B deje de llamarlas y se invente el dato (8/8 aciertos con la guía sola, 0/8 con la persona de asistente de voz). El turno alterna ahora entre dos instrucciones de sistema. La ejecución de órdenes del sistema queda implementada y apagada, tras cuatro barreras: lista blanca sobre el ejecutable, rutas rechazadas, sin shell que interprete metacaracteres y plazo máximo. 68 pruebas unitarias sin modelos, más seis de integración que se saltan solas si no hay servidores y se turnan la GPU: en paralelo, los dos servidores no caben en 4 GB y miden contención en vez de latencia. Claude-Session: https://claude.ai/code/session_01FNxz5cSdQSscJH9H7b8uGU
Diffstat (limited to 'vendor/extra/canary-rs/examples')
-rw-r--r--vendor/extra/canary-rs/examples/bench_live.rs91
1 files changed, 91 insertions, 0 deletions
diff --git a/vendor/extra/canary-rs/examples/bench_live.rs b/vendor/extra/canary-rs/examples/bench_live.rs
new file mode 100644
index 0000000..71c8420
--- /dev/null
+++ b/vendor/extra/canary-rs/examples/bench_live.rs
@@ -0,0 +1,91 @@
+#[path = "shared/utils.rs"]
+mod utils;
+
+use canary_rs::{Canary, StreamConfig};
+use std::time::Instant;
+
+/// Benchmarks the pieces that drive live-streaming latency:
+///
+/// * encoder cost as a function of window length,
+/// * decoder cost per generated token,
+/// * end-to-end cost of one streaming window vs. one full-utterance decode.
+///
+/// Usage: `bench_live <wav> [lang]`, honouring the same `CANARY_*` environment
+/// variables as the other examples.
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+ let wav = match std::env::args().nth(1) {
+ Some(path) => path,
+ None => return Err("usage: bench_live <wav> [lang]".into()),
+ };
+ let lang = std::env::args().nth(2).unwrap_or_else(|| "en".to_string());
+
+ let config = utils::execution_config_from_env();
+ let model_dir =
+ std::env::var("CANARY_MODEL_DIR").unwrap_or_else(|_| "canary-180m-flash".to_string());
+
+ let t0 = Instant::now();
+ let model = Canary::from_pretrained(&model_dir, Some(config))?;
+ println!("model load: {:.2?}", t0.elapsed());
+
+ let mut reader = hound::WavReader::open(&wav)?;
+ let spec = reader.spec();
+ let samples: Vec<f32> = reader
+ .samples::<i16>()
+ .map(|s| s.map(|v| v as f32 / 32768.0))
+ .collect::<std::result::Result<_, _>>()?;
+ let sr = spec.sample_rate as usize;
+ let duration = samples.len() as f32 / sr as f32;
+ println!("audio: {:.2}s @ {} Hz, {} ch", duration, sr, spec.channels);
+
+ let mut session = model.session();
+ let warm = &samples[..(sr).min(samples.len())];
+ let _ = session.transcribe_samples(warm, sr, 1, &lang, &lang)?;
+
+ // A live window always ends mid-utterance, so mirror that here: take the
+ // *last* `window` seconds and append the same tail silence the stream uses.
+ let tail = StreamConfig::default().tail_silence_duration;
+ println!("\nrolling windows (tail silence {:.2}s):", tail);
+ for window_s in [2.0f32, 4.0, 6.0, 8.0] {
+ if window_s > duration {
+ continue;
+ }
+ let start = samples.len() - (sr as f32 * window_s) as usize;
+ let mut win = samples[start..].to_vec();
+ win.resize(win.len() + (sr as f32 * tail) as usize, 0.0);
+
+ let mut best = f64::MAX;
+ let mut tokens = 0;
+ for _ in 0..3 {
+ let t = Instant::now();
+ let r = session.transcribe_samples(&win, sr, 1, &lang, &lang)?;
+ best = best.min(t.elapsed().as_secs_f64());
+ tokens = r.tokens.len();
+ }
+ // No tokens means the model saw nothing worth transcribing, and the time is encoder-only.
+ let per_token = match tokens {
+ 0 => "encoder only".to_string(),
+ n => format!("{:.1} ms/token", best * 1000.0 / n as f64),
+ };
+ println!(
+ " {:>4.1}s window -> {:>6.0} ms ({:>3} tokens, {})",
+ window_s,
+ best * 1000.0,
+ tokens,
+ per_token
+ );
+ }
+
+ let t = Instant::now();
+ let r = session.transcribe_samples(&samples, sr, 1, &lang, &lang)?;
+ let full = t.elapsed().as_secs_f64();
+ println!(
+ "\nfull utterance ({:.1}s) -> {:.0} ms ({} tokens, {:.1}x realtime)",
+ duration,
+ full * 1000.0,
+ r.tokens.len(),
+ duration as f64 / full
+ );
+ println!("text: {}", r.text);
+
+ Ok(())
+}