aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-core/src/event.rs
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 /crates/asist-core/src/event.rs
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 'crates/asist-core/src/event.rs')
-rw-r--r--crates/asist-core/src/event.rs127
1 files changed, 127 insertions, 0 deletions
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<TurnId> {
+ 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,
+ }
+ }
+}