From f8f98e83481e7376a235fb305a095552433f79ab Mon Sep 17 00:00:00 2001 From: elvis Date: Sun, 6 Sep 2026 19:21:16 -0300 Subject: Local voice assistant on top of Canary, llama.cpp and qwentts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 --- crates/asist-core/src/event.rs | 127 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 crates/asist-core/src/event.rs (limited to 'crates/asist-core/src/event.rs') diff --git a/crates/asist-core/src/event.rs b/crates/asist-core/src/event.rs new file mode 100644 index 0000000..549bec0 --- /dev/null +++ b/crates/asist-core/src/event.rs @@ -0,0 +1,127 @@ +use std::time::{Duration, Instant}; + +/// Identifica un turno de conversación (una intervención del usuario y la +/// respuesta que provoca). Todo lo que viaja por el bus lo lleva, para que un +/// resultado que llega tarde no se confunda con el turno que ya está en curso +/// —el caso típico cuando el usuario interrumpe al asistente. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct TurnId(pub u64); + +impl std::fmt::Display for TurnId { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "#{}", self.0) + } +} + +/// Motivo por el que se corta la reproducción en curso. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum InterruptReason { + /// El usuario ha empezado a hablar encima del asistente (barge-in). + UserSpoke, + /// Petición explícita: tecla, señal o cierre. + Requested, +} + +/// Todo lo que las etapas se cuentan entre sí. Un único enum mantiene el bus +/// observable: el renderizador y la telemetría ven exactamente los mismos +/// eventos que el orquestador, sin canales paralelos que se desincronicen. +#[derive(Debug, Clone)] +pub enum Event { + /// El VAD ha detectado el arranque de una intervención. + SpeechStarted { turn: TurnId, at: Instant }, + + /// Transcripción provisional de la ventana deslizante: `committed` ya es + /// estable, `volatile` todavía puede cambiar. + Partial { + turn: TurnId, + committed: String, + volatile: String, + }, + + /// Transcripción definitiva de la intervención completa. + Transcript { + turn: TurnId, + text: String, + audio_secs: f32, + decode: Duration, + }, + + /// La intervención no contenía nada transcribible. + Discarded { turn: TurnId }, + + /// Primer fragmento de texto que devuelve el modelo. + ReplyStarted { turn: TurnId, ttft: Duration }, + + /// Un trozo de respuesta según llega del modelo. + ReplyDelta { turn: TurnId, text: String }, + + /// Una frase completa lista para sintetizar. + Sentence { + turn: TurnId, + index: usize, + text: String, + }, + + /// Respuesta completa del modelo para este turno. + ReplyDone { turn: TurnId, text: String }, + + /// El modelo ha pedido ejecutar una herramienta. + ToolRequested { + turn: TurnId, + name: String, + arguments: String, + }, + + /// Resultado de esa ejecución. + ToolFinished { + turn: TurnId, + name: String, + ok: bool, + output: String, + took: Duration, + }, + + /// Primer audio audible del turno: la métrica que de verdad percibe quien habla. + AudioStarted { turn: TurnId, latency: Duration }, + + /// Se ha terminado de reproducir todo lo del turno. + AudioFinished { turn: TurnId }, + + /// Se ha cortado la reproducción. + Interrupted { + turn: TurnId, + reason: InterruptReason, + }, + + /// Aviso no fatal; el turno continúa. + Warning { turn: TurnId, message: String }, + + /// Fallo que aborta el turno. + Failed { turn: TurnId, message: String }, + + /// Cierre ordenado. + Shutdown, +} + +impl Event { + pub fn turn(&self) -> Option { + match self { + Event::SpeechStarted { turn, .. } + | Event::Partial { turn, .. } + | Event::Transcript { turn, .. } + | Event::Discarded { turn } + | Event::ReplyStarted { turn, .. } + | Event::ReplyDelta { turn, .. } + | Event::Sentence { turn, .. } + | Event::ReplyDone { turn, .. } + | Event::ToolRequested { turn, .. } + | Event::ToolFinished { turn, .. } + | Event::AudioStarted { turn, .. } + | Event::AudioFinished { turn } + | Event::Interrupted { turn, .. } + | Event::Warning { turn, .. } + | Event::Failed { turn, .. } => Some(*turn), + Event::Shutdown => None, + } + } +} -- cgit v1.2.3