diff options
Diffstat (limited to 'crates/asist-asr')
| -rw-r--r-- | crates/asist-asr/Cargo.toml | 2 | ||||
| -rw-r--r-- | crates/asist-asr/src/lib.rs | 110 |
2 files changed, 57 insertions, 55 deletions
diff --git a/crates/asist-asr/Cargo.toml b/crates/asist-asr/Cargo.toml index e9b8345..2f74d9b 100644 --- a/crates/asist-asr/Cargo.toml +++ b/crates/asist-asr/Cargo.toml @@ -9,7 +9,7 @@ license.workspace = true asist-core.workspace = true asist-audio.workspace = true canary-rs.workspace = true -# Sólo para fijar la versión; ver la nota del Cargo.toml del workspace. +# Only to pin the version; see the note in the workspace Cargo.toml. ort.workspace = true crossbeam-channel.workspace = true tracing.workspace = true diff --git a/crates/asist-asr/src/lib.rs b/crates/asist-asr/src/lib.rs index 8212ceb..e6ecb36 100644 --- a/crates/asist-asr/src/lib.rs +++ b/crates/asist-asr/src/lib.rs @@ -1,14 +1,14 @@ -//! Reconocimiento de voz sobre Canary (ONNX). +//! Speech recognition on top of Canary (ONNX). //! -//! El hecho que da forma a este crate: **decodificar una ventana cuesta más -//! que grabarla**. En esta máquina, 6 s de ventana tardan cerca de 800 ms en -//! decodificarse y sólo avanzan 400 ms de audio. Cualquier diseño que procese -//! todas las ventanas en orden se va quedando atrás del hablante sin límite. +//! The fact that shapes this crate: **decoding a window costs more than +//! recording it**. On this machine a 6 s window takes about 800 ms to decode +//! and only advances 400 ms of audio. Any design that processes every window +//! in order keeps falling behind the speaker without limit. //! -//! La salida es tirar trabajo: el hilo de decodificación vacía su cola entera, -//! se queda sólo con la ventana más reciente y descarta el resto. La latencia -//! queda acotada por una decodificación en lugar de crecer sin freno, y lo que -//! se pierde son transcripciones provisionales que iban a ser sobreescritas. +//! The way out is dropping work: the decoding thread drains its whole queue, +//! keeps only the most recent window and discards the rest. Latency stays +//! bounded by one decode instead of growing unchecked, and what is lost are +//! partial transcriptions that were going to be overwritten anyway. use std::time::{Duration, Instant}; @@ -20,33 +20,33 @@ use asist_core::event::TurnId; pub use canary_rs::{Canary, CanarySession, ExecutionConfig, ExecutionProvider, StreamConfig}; -/// Trabajo que llega al decodificador. +/// Work arriving at the decoder. #[derive(Debug)] pub enum AsrJob { - /// Audio nuevo para la ventana deslizante. + /// New audio for the sliding window. Window { turn: TurnId, samples: Vec<f32>, at: Instant, }, - /// Intervención cerrada, para transcribir entera. + /// Closed utterance, to be transcribed in full. Utterance { turn: TurnId, samples: Vec<f32>, at: Instant, }, - /// La intervención no tenía nada: reinicia el estado de la ventana. + /// The utterance was empty: resets the window state. Reset { turn: TurnId }, } -/// Lo que el decodificador devuelve. +/// What the decoder returns. #[derive(Debug, Clone)] pub enum AsrResult { Partial { turn: TurnId, committed: String, volatile: String, - /// Ventanas descartadas por obsoletas antes de esta. + /// Windows discarded as stale before this one. dropped: usize, decode: Duration, }, @@ -55,9 +55,9 @@ pub enum AsrResult { text: String, audio_secs: f32, decode: Duration, - /// Instante en que el usuario dejó de hablar. Es el origen del que se - /// mide la latencia percibida, y no puede tomarse aquí: para cuando la - /// transcripción está lista ya ha pasado casi un segundo. + /// Instant the user stopped talking. It is the origin perceived latency + /// is measured from, and it cannot be taken here: by the time the + /// transcription is ready almost a second has passed. spoken_at: Instant, }, Empty { @@ -69,18 +69,18 @@ pub enum AsrResult { }, } -/// Motor cargado y listo para decodificar. +/// Engine loaded and ready to decode. pub struct Recognizer { model: Canary, config: AsrConfig, } impl Recognizer { - /// Carga el modelo desde `config.model_dir`. + /// Loads the model from `config.model_dir`. pub fn load(config: &AsrConfig) -> Result<Self> { if !config.model_dir.is_dir() { return Err(Error::Asr(format!( - "no existe la carpeta del modelo: {}. Ejecuta scripts/bootstrap.sh", + "the model directory does not exist: {}. Run scripts/bootstrap.sh", config.model_dir.display() ))); } @@ -89,12 +89,12 @@ impl Recognizer { config.model_dir.to_string_lossy().as_ref(), Some(execution_config(config)), ) - .map_err(|e| Error::Asr(format!("no se pudo cargar Canary: {e}")))?; + .map_err(|e| Error::Asr(format!("could not load Canary: {e}")))?; tracing::info!( target: "asr", - carpeta = %config.model_dir.display(), - proveedor = %config.execution_provider, + dir = %config.model_dir.display(), + provider = %config.execution_provider, ms = started.elapsed().as_millis(), "modelo cargado" ); @@ -104,7 +104,7 @@ impl Recognizer { }) } - /// Sesión suelta para transcribir de una vez (pruebas y comprobaciones). + /// Standalone session to transcribe in one go (tests and checks). pub fn transcribe(&self, samples: &[f32], sample_rate: u32) -> Result<String> { let mut session = self.model.session(); session @@ -119,8 +119,7 @@ impl Recognizer { .map_err(|e| Error::Asr(e.to_string())) } - /// Bucle del decodificador. Se ejecuta en su propio hilo hasta que se - /// cierre `jobs`. + /// Decoder loop. Runs on its own thread until `jobs` is closed. pub fn run(self, jobs: Receiver<AsrJob>, out: Sender<AsrResult>) { let mut stream = match self.model.stream( self.config.source_lang.clone(), @@ -131,7 +130,7 @@ impl Recognizer { Err(err) => { let _ = out.send(AsrResult::Error { turn: TurnId::default(), - message: format!("no se pudo abrir el flujo: {err}"), + message: format!("could not open the stream: {err}"), }); return; } @@ -141,9 +140,9 @@ impl Recognizer { let step_samples = (self.config.step * 16_000.0).max(1.0) as usize; while let Some(batch) = drain(&jobs) { - // Todo el audio que se quedó detrás de un cierre de intervención - // pertenece a un turno que ya se va a transcribir entero: gastar - // una ventana en él sería trabajo condenado a sobreescribirse. + // All the audio left behind an utterance close belongs to a turn that + // is about to be transcribed in full: spending a window on it would + // be work doomed to be overwritten. let mut pending: Vec<f32> = Vec::new(); let mut pending_turn = TurnId::default(); let mut newest = Instant::now(); @@ -181,8 +180,8 @@ impl Recognizer { if pending.is_empty() || !self.config.partials { continue; } - // Sólo sobrevive la ventana más nueva; lo anterior ya no describe - // lo que se está diciendo ahora. + // Only the newest window survives; the older ones no longer describe + // what is being said now. dropped += (pending.len() / step_samples).saturating_sub(1); let started = Instant::now(); @@ -206,10 +205,10 @@ impl Recognizer { let decode = started.elapsed(); tracing::debug!( target: "asr", - turno = pending_turn.0, + turn = pending_turn.0, ms = decode.as_millis(), - retraso_ms = newest.elapsed().as_millis(), - descartadas = dropped, + delay_ms = newest.elapsed().as_millis(), + dropped = dropped, "ventana" ); if out @@ -243,12 +242,12 @@ fn decode_final( let decode = started.elapsed(); tracing::debug!( target: "asr", - turno = turn.0, + turn = turn.0, ms = decode.as_millis(), audio_s = audio_secs, rtf = decode.as_secs_f32() / audio_secs.max(0.001), - retraso_ms = at.elapsed().as_millis(), - "transcripción final" + delay_ms = at.elapsed().as_millis(), + "final transcription" ); if text.is_empty() { AsrResult::Empty { turn } @@ -292,11 +291,11 @@ fn stream_config(config: &AsrConfig) -> StreamConfig { .with_emit_partial(true) .with_pad_partial(false) .with_stability_window(config.stability) - // El motivo de todo el diseño: nunca arrastrar una cola de ventanas viejas. + // The reason for the whole design: never drag a queue of old windows. .with_max_windows_per_push(1) } -/// Recoge de golpe todo lo encolado, bloqueando hasta que llegue lo primero. +/// Grabs everything queued at once, blocking until the first item arrives. fn drain(jobs: &Receiver<AsrJob>) -> Option<Vec<AsrJob>> { let mut batch = vec![jobs.recv().ok()?]; while let Ok(job) = jobs.try_recv() { @@ -305,7 +304,7 @@ fn drain(jobs: &Receiver<AsrJob>) -> Option<Vec<AsrJob>> { Some(batch) } -/// Une el texto estable con el fragmento nuevo respetando los espacios. +/// Joins the stable text with the new fragment, keeping the spacing right. pub fn append_delta(committed: &mut String, delta: &str) { if delta.is_empty() { return; @@ -319,10 +318,10 @@ pub fn append_delta(committed: &mut String, delta: &str) { committed.push_str(delta); } -/// Parte de la ventana actual que aún no es estable. +/// Part of the current window that is not stable yet. pub fn volatile_tail(committed: &str, window: &str) -> String { - // La ventana repite el final de lo ya fijado; lo interesante es lo que - // sobra por detrás. + // The window repeats the end of what is already committed; the + // interesting part is what follows it. match window.rfind(last_words(committed, 3).as_str()) { Some(idx) if !committed.is_empty() => window[idx + last_words(committed, 3).len()..] .trim_start() @@ -336,8 +335,8 @@ fn last_words(text: &str, n: usize) -> String { words[words.len().saturating_sub(n)..].join(" ") } -/// Canary a veces se atasca repitiendo un token cuando la ventana es casi -/// silencio. Mostrarlo confundiría más que ayudar. +/// Canary sometimes gets stuck repeating a token when the window is +/// almost silence. Showing that would confuse more than help. pub fn is_degenerate(text: &str) -> bool { let words: Vec<&str> = text.split_whitespace().collect(); if words.len() < 6 { @@ -347,7 +346,7 @@ pub fn is_degenerate(text: &str) -> bool { distinct.len() * 4 <= words.len() } -/// Arregla el espaciado de la puntuación que a veces deja el decodificador. +/// Fixes the punctuation spacing the decoder sometimes leaves. pub fn normalize(text: &str) -> String { let mut out = String::with_capacity(text.len()); for c in text.trim().chars() { @@ -366,7 +365,7 @@ mod tests { use super::*; #[test] - fn el_delta_se_une_con_espacio_salvo_ante_puntuacion() { + fn delta_is_joined_with_a_space_except_before_punctuation() { let mut s = String::from("hola"); append_delta(&mut s, "mundo"); assert_eq!(s, "hola mundo"); @@ -377,24 +376,27 @@ mod tests { } #[test] - fn la_cola_volatil_es_lo_que_sobra_tras_lo_fijado() { + fn volatile_tail_is_what_follows_the_committed_text() { assert_eq!(volatile_tail("hola qué tal", "hola qué tal estás"), "estás"); } #[test] - fn sin_texto_fijado_toda_la_ventana_es_volatil() { + fn without_committed_text_the_whole_window_is_volatile() { assert_eq!(volatile_tail("", "hola qué tal"), "hola qué tal"); } #[test] - fn se_detecta_la_repeticion_degenerada() { + fn degenerate_repetition_is_detected() { assert!(is_degenerate("sí sí sí sí sí sí sí sí")); assert!(!is_degenerate("hola qué tal estás hoy amigo")); - assert!(!is_degenerate("sí sí"), "una frase corta no es degenerada"); + assert!( + !is_degenerate("sí sí"), + "a short sentence is not degenerate" + ); } #[test] - fn la_puntuacion_pierde_el_espacio_de_delante() { + fn punctuation_loses_the_space_before_it() { assert_eq!(normalize("hola , qué tal ?"), "hola, qué tal?"); assert_eq!(normalize(" ya está "), "ya está"); } |