aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-asr/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/asist-asr/src')
-rw-r--r--crates/asist-asr/src/lib.rs110
1 files changed, 56 insertions, 54 deletions
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á");
}