diff options
| author | elvis <elvis@claros.ar> | 2026-09-06 19:21:16 -0300 |
|---|---|---|
| committer | elvis <elvis@claros.ar> | 2026-09-06 19:23:37 -0300 |
| commit | f8f98e83481e7376a235fb305a095552433f79ab (patch) | |
| tree | 6d0ecddb26bef8430a086431de40f8ce9b1039e6 /vendor/extra | |
| download | asist-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')
| -rw-r--r-- | vendor/extra/LEEME.md | 10 | ||||
| -rw-r--r-- | vendor/extra/canary-rs/examples/bench_live.rs | 91 | ||||
| -rwxr-xr-x | vendor/extra/qwentts.cpp/examples/all.sh | 99 | ||||
| -rwxr-xr-x | vendor/extra/qwentts.cpp/examples/server-test.sh | 51 |
4 files changed, 251 insertions, 0 deletions
diff --git a/vendor/extra/LEEME.md b/vendor/extra/LEEME.md new file mode 100644 index 0000000..658bde9 --- /dev/null +++ b/vendor/extra/LEEME.md @@ -0,0 +1,10 @@ +# Ficheros sueltos de los submódulos + +Los parches de `vendor/patches/` sólo llevan cambios a ficheros que sus +repositorios ya versionaban. Estos otros existían únicamente en el árbol de +trabajo local, así que se guardan aquí enteros y `scripts/bootstrap.sh` los +copia al submódulo correspondiente tras aplicar el parche. + +Hace falta al menos uno de ellos para que el submódulo compile: +`canary-rs/Cargo.toml` declara el ejemplo `bench_live`, y cargo se niega a +resolver el paquete si el fichero no está. 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(()) +} diff --git a/vendor/extra/qwentts.cpp/examples/all.sh b/vendor/extra/qwentts.cpp/examples/all.sh new file mode 100755 index 0000000..f0b701c --- /dev/null +++ b/vendor/extra/qwentts.cpp/examples/all.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# Matriz completa de pruebas de qwentts.cpp con los modelos disponibles. +# Genera un WAV por cada posibilidad en examples/out/ y un resumen al final. +# Uso: ./all.sh +set -u +cd "$(dirname "$0")" + +TTS=../build/qwen-tts +CODEC=../build/qwen-codec +COD=../models/qwen-tokenizer-12hz-Q8_0.gguf +T06B=../models/qwen-talker-0.6b-base-Q4_K_M.gguf +T06C=../models/qwen-talker-0.6b-customvoice-Q4_K_M.gguf +T17B=../models/qwen-talker-1.7b-base-Q8_0.gguf +T17C=../models/qwen-talker-1.7b-customvoice-Q8_0.gguf +T17V=../models/qwen-talker-1.7b-voicedesign-Q8_0.gguf +OUT=out +mkdir -p "$OUT" + +EN="qwentts runs entirely offline on your own machine." +ES="Hola, esto es una prueba de sintesis de voz totalmente local." +ZH="这是一个完全本地运行的语音合成测试。" + +n=0; ok=0 +# run <desc> <outfile> <texto> [args extra de qwen-tts...] +run() { + local desc="$1" out="$2" text="$3"; shift 3 + n=$((n+1)) + printf '%2d) %-46s ' "$n" "$desc" + local t0 t1 + t0=$(date +%s.%N) + if printf '%s' "$text" | "$TTS" --codec "$COD" --seed 42 "$@" \ + -o "$OUT/$out" >"$OUT/$out.log" 2>&1; then + t1=$(date +%s.%N) + printf 'OK %5s %4.0fs %s\n' "$(du -h "$OUT/$out" | cut -f1)" \ + "$(echo "$t1 - $t0" | bc)" "$out" + ok=$((ok+1)) + else + printf 'FALLO (ver %s)\n' "$OUT/$out.log" + fi +} + +echo "########## A. BASE (voz por defecto) ##########" +run "Base 0.6B english" base_0.6b_en.wav "$EN" --model "$T06B" --lang english +run "Base 1.7B english" base_1.7b_en.wav "$EN" --model "$T17B" --lang english +run "Base 1.7B auto (autodetec)" base_1.7b_auto.wav "$EN" --model "$T17B" +run "Base 1.7B spanish" base_1.7b_es.wav "$ES" --model "$T17B" --lang spanish +run "Base 1.7B chinese" base_1.7b_zh.wav "$ZH" --model "$T17B" --lang chinese + +echo "########## B. CLONACION (referencia freeman, Base) ##########" +run "Clone ref-wav (encode interno)" clone_refwav_1.7b.wav "$EN" --model "$T17B" --lang english \ + --ref-wav freeman.wav --ref-text freeman.txt +run "Clone x-vector only (solo .spk)" clone_xvec_1.7b.wav "$EN" --model "$T17B" --lang english \ + --ref-spk freeman.spk +run "Clone ICL (.spk+.rvq+.txt)" clone_icl_1.7b.wav "$EN" --model "$T17B" --lang english \ + --ref-spk freeman.spk --ref-rvq freeman.rvq --ref-text freeman.txt +run "Clone ICL 0.6B" clone_icl_0.6b.wav "$EN" --model "$T06B" --lang english \ + --ref-spk freeman.spk --ref-rvq freeman.rvq --ref-text freeman.txt + +echo "########## C. CUSTOMVOICE (speakers con nombre) ##########" +for spk in serena vivian uncle_fu ryan aiden ono_anna sohee; do + run "CustomVoice 1.7B $spk" "cv_1.7b_${spk}.wav" "$EN" --model "$T17C" --lang english --speaker "$spk" +done +run "CustomVoice 1.7B eric (sichuan, ZH)" cv_1.7b_eric_zh.wav "$ZH" --model "$T17C" --lang chinese --speaker eric +run "CustomVoice 1.7B dylan (beijing, ZH)" cv_1.7b_dylan_zh.wav "$ZH" --model "$T17C" --lang chinese --speaker dylan +run "CustomVoice 0.6B vivian" cv_0.6b_vivian.wav "$EN" --model "$T06C" --lang english --speaker vivian +run "CustomVoice 1.7B vivian + instruct" cv_1.7b_vivian_instr.wav "$EN" --model "$T17C" --lang english \ + --speaker vivian --instruct "happy, energetic" + +echo "########## D. VOICEDESIGN (instruccion de atributos, 1.7B) ##########" +run "VoiceDesign male young" vd_male_young.wav "$EN" --model "$T17V" --lang english \ + --instruct "male, young adult, moderate pitch" +run "VoiceDesign female elderly" vd_female_elderly.wav "$EN" --model "$T17V" --lang english \ + --instruct "female, elderly, low pitch, calm" +run "VoiceDesign teen excited" vd_teen_excited.wav "$EN" --model "$T17V" --lang english \ + --instruct "female, teenager, high pitch, excited and fast" + +echo "########## E. MUESTREO / FORMATO (Base 1.7B) ##########" +run "Greedy (determinista)" fmt_greedy.wav "$EN" --model "$T17B" --lang english --greedy +run "Sampled temp=1.2 top-p=0.9" fmt_temp.wav "$EN" --model "$T17B" --lang english --temp 1.2 --top-p 0.9 +run "Formato wav24" fmt_wav24.wav "$EN" --model "$T17B" --lang english --format wav24 +run "Formato wav32" fmt_wav32.wav "$EN" --model "$T17B" --lang english --format wav32 + +echo "########## F. QWEN-CODEC (encode/decode round-trip) ##########" +cp -f freeman.wav "$OUT/rt.wav" +echo -n "F1) codec encode (wav -> .rvq + .spk) " +if "$CODEC" --model "$COD" --talker "$T17B" -i "$OUT/rt.wav" >"$OUT/codec_encode.log" 2>&1; then + echo "OK ($(du -h "$OUT/rt.rvq" 2>/dev/null | cut -f1) rvq, $(du -h "$OUT/rt.spk" 2>/dev/null | cut -f1) spk)" +else echo "FALLO (ver $OUT/codec_encode.log)"; fi +cp -f "$OUT/rt.rvq" "$OUT/roundtrip.rvq" +echo -n "F2) codec decode (.rvq -> wav round-trip) " +if "$CODEC" --model "$COD" -i "$OUT/roundtrip.rvq" >"$OUT/codec_decode.log" 2>&1; then + echo "OK ($(du -h "$OUT/roundtrip.wav" 2>/dev/null | cut -f1))" +else echo "FALLO (ver $OUT/codec_decode.log)"; fi + +echo +echo "########## RESUMEN ##########" +echo "Sintesis OK: $ok / $n" +echo "WAVs en $(pwd)/$OUT/:" +/bin/ls -1 "$OUT"/*.wav 2>/dev/null | sed 's|^| |' diff --git a/vendor/extra/qwentts.cpp/examples/server-test.sh b/vendor/extra/qwentts.cpp/examples/server-test.sh new file mode 100755 index 0000000..7242afe --- /dev/null +++ b/vendor/extra/qwentts.cpp/examples/server-test.sh @@ -0,0 +1,51 @@ +#!/bin/bash +# Prueba de punta a punta del tts-server (API OpenAI-compatible). +set -u +cd "$(dirname "$0")" +OUT=out; mkdir -p "$OUT" +PORT=8080; H=127.0.0.1; B="http://$H:$PORT" + +echo "== arrancando tts-server (base 1.7B) ==" +../build/tts-server \ + --model ../models/qwen-talker-1.7b-base-Q8_0.gguf \ + --codec ../models/qwen-tokenizer-12hz-Q8_0.gguf \ + --alias qwen3-tts-base --host $H --port $PORT --lang auto \ + >"$OUT/server.log" 2>&1 & +SRV=$! +trap 'kill $SRV 2>/dev/null' EXIT + +echo -n "esperando readiness" +for i in $(seq 1 60); do + curl -sf "$B/v1/models" >/dev/null 2>&1 && { echo " listo"; break; } + kill -0 $SRV 2>/dev/null || { echo " el server murio"; tail -20 "$OUT/server.log"; exit 1; } + printf '.'; sleep 1 +done + +echo "== 1) GET /v1/models ==" +curl -s "$B/v1/models"; echo + +echo "== 2) voz por defecto -> wav ==" +curl -s -X POST "$B/v1/audio/speech" -H "Content-Type: application/json" \ + -d '{"input":"This is the default server voice.","response_format":"wav","seed":42}' \ + -o "$OUT/srv_default.wav" +echo " $(du -h "$OUT/srv_default.wav" | cut -f1) $OUT/srv_default.wav" + +echo "== 3) registrar voz clonada 'freeman' (spk+rvq b64) ==" +curl -s -X POST "$B/v1/audio/voices" -H "Content-Type: application/json" \ + -d "{\"name\":\"freeman\",\"ref_text\":\"$(cat freeman.txt)\", + \"spk_b64\":\"$(base64 -w0 freeman.spk)\",\"rvq_b64\":\"$(base64 -w0 freeman.rvq)\"}" +echo + +echo "== 4) sintesis con la voz clonada -> wav ==" +curl -s -X POST "$B/v1/audio/speech" -H "Content-Type: application/json" \ + -d '{"input":"Now I am speaking with the cloned voice.","voice":"freeman","response_format":"wav","seed":42,"temperature":0.8}' \ + -o "$OUT/srv_freeman.wav" +echo " $(du -h "$OUT/srv_freeman.wav" | cut -f1) $OUT/srv_freeman.wav" + +echo "== 5) streaming PCM (s16le) -> archivo raw ==" +curl -s -X POST "$B/v1/audio/speech" -H "Content-Type: application/json" \ + -d '{"input":"Streaming pcm chunks.","response_format":"pcm","seed":42}' \ + -o "$OUT/srv_stream.pcm" +echo " $(du -h "$OUT/srv_stream.pcm" | cut -f1) $OUT/srv_stream.pcm (s16le mono 24k)" + +echo "== ok, parando server ==" |