aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-audio/src/vad.rs
diff options
context:
space:
mode:
authorelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
committerelvis <elvis@claros.ar>2026-09-26 20:20:19 -0300
commit8518a63f55153e7f45fd49ad6caff5555f4e374f (patch)
tree636684eea3fa6f35d78282ab95eb687ef49154af /crates/asist-audio/src/vad.rs
parent69de76dc9cbedc6092d1e5ce84094a8030de1470 (diff)
downloadasist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.tar.gz
asist-p-8518a63f55153e7f45fd49ad6caff5555f4e374f.zip
Translate code, comments, logs and terminal UI to English; add English README; rename scriptsHEADmain
Diffstat (limited to 'crates/asist-audio/src/vad.rs')
-rw-r--r--crates/asist-audio/src/vad.rs130
1 files changed, 65 insertions, 65 deletions
diff --git a/crates/asist-audio/src/vad.rs b/crates/asist-audio/src/vad.rs
index b30f833..24cf717 100644
--- a/crates/asist-audio/src/vad.rs
+++ b/crates/asist-audio/src/vad.rs
@@ -1,10 +1,10 @@
-//! Detección de voz y troceado en intervenciones.
+//! Voice activity detection and splitting into utterances.
//!
-//! Está escrito como una máquina de estados pura: se le dan muestras y
-//! devuelve eventos, sin hilos ni canales dentro. Así el comportamiento que
-//! más cuesta depurar a oído —cuándo arranca un turno, cuándo lo corta el
-//! silencio, cuándo se ignora el eco del propio altavoz— se puede probar
-//! entero con audio sintético y sin micrófono.
+//! It is written as a pure state machine: it is fed samples and returns
+//! events, with no threads or channels inside. That way the behaviour that is
+//! hardest to debug by ear (when a turn starts, when silence ends it, when
+//! the echo of the assistant's own speaker is ignored) can be fully tested
+//! with synthetic audio and no microphone.
use std::collections::VecDeque;
@@ -12,7 +12,7 @@ use asist_core::config::VadConfig;
use crate::{rms, ASR_SAMPLE_RATE};
-/// Una intervención cerrada, lista para transcribir.
+/// A closed utterance, ready to be transcribed.
#[derive(Debug, Clone)]
pub struct Utterance {
pub samples: Vec<f32>,
@@ -25,30 +25,30 @@ impl Utterance {
}
}
-/// Lo que el segmentador tiene que contar hacia fuera.
+/// What the segmenter reports to the outside.
#[derive(Debug, Clone)]
pub enum VoiceEvent {
/// Ha empezado a hablarse.
Started,
- /// Audio nuevo dentro de la intervención en curso, para las
- /// transcripciones provisionales.
+ /// New audio inside the current utterance, for the partial
+ /// transcriptions.
Audio(Vec<f32>),
- /// Intervención terminada y lo bastante larga como para transcribirla.
+ /// Utterance finished and long enough to be transcribed.
Ended(Utterance),
- /// Terminada pero demasiado corta: un golpe en la mesa, una tos.
+ /// Finished but too short: a knock on the table, a cough.
Discarded,
- /// Se ha detectado voz mientras el asistente hablaba, con el barge-in
- /// activo. El orquestador corta la reproducción al recibirlo.
+ /// Speech was detected while the assistant was talking, with barge-in
+ /// on. The orchestrator cuts playback when it gets this.
BargeIn,
}
-/// Qué hace el segmentador con el micrófono mientras suena el altavoz.
+/// What the segmenter does with the microphone while the speaker plays.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Gate {
- /// Nadie está hablando por el altavoz: se escucha con normalidad.
+ /// Nothing is playing on the speaker: listen normally.
Open,
- /// El asistente habla. Según la configuración, o se ignora la entrada
- /// (media dúplex) o se exige más volumen para interrumpir (barge-in).
+ /// The assistant is talking. Depending on the configuration, input is
+ /// either ignored (half duplex) or needs more volume to interrupt (barge-in).
Speaking,
}
@@ -63,10 +63,10 @@ pub struct Segmenter {
pending: Vec<f32>,
preroll: VecDeque<f32>,
utterance: Vec<f32>,
- /// Muestras de la intervención que estaban de verdad por encima del
- /// umbral. El mínimo se mide sobre esto y no sobre `utterance`, que
- /// arrastra el preroll: si no, 0,1 s de golpe en la mesa más 0,2 s de
- /// preroll pasan por una intervención válida y disparan un turno entero.
+ /// Samples of the utterance that really were above the threshold. The
+ /// minimum is measured on this and not on `utterance`, which carries the
+ /// preroll: otherwise 0.1 s of a knock on the table plus 0.2 s of preroll
+ /// passes for a valid utterance and triggers a whole turn.
voiced: usize,
noise_floor: f32,
silence_run: usize,
@@ -95,14 +95,14 @@ impl Segmenter {
}
}
- /// Abre o cierra el micrófono según hable o no el asistente.
+ /// Opens or closes the microphone depending on whether the assistant talks.
pub fn set_gate(&mut self, gate: Gate) {
if self.gate == gate {
return;
}
self.gate = gate;
- // Al volver a abrir tras una respuesta, lo acumulado es la cola del
- // propio altavoz: arrancar un turno con eso daría un turno fantasma.
+ // When reopening after an answer, what has accumulated is the tail of
+ // the assistant's own speaker: starting a turn with it would be a ghost turn.
if gate == Gate::Open {
self.pending.clear();
self.preroll.clear();
@@ -121,12 +121,12 @@ impl Segmenter {
self.noise_floor
}
- /// Umbral que separa voz de silencio ahora mismo.
+ /// Threshold that separates speech from silence right now.
pub fn threshold(&self) -> f32 {
let base = (self.noise_floor * self.config.threshold_factor)
.clamp(self.config.min_threshold, self.config.max_threshold);
- // Con el altavoz sonando hay que hablar más alto para colarse: el
- // micrófono se está oyendo a sí mismo.
+ // With the speaker playing you must speak louder to get through: the
+ // microphone is hearing itself.
if self.gate == Gate::Speaking {
base * self.config.barge_in_factor
} else {
@@ -134,7 +134,7 @@ impl Segmenter {
}
}
- /// Cierra a la fuerza la intervención en curso (cierre del programa).
+ /// Forcibly closes the current utterance (program shutdown).
pub fn flush(&mut self) -> Option<Utterance> {
if !self.speaking || self.voiced < self.min_utterance_samples {
return None;
@@ -147,11 +147,11 @@ impl Segmenter {
})
}
- /// Alimenta audio mono a 16 kHz y recoge lo que haya que hacer.
+ /// Feeds 16 kHz mono audio and collects whatever needs doing.
pub fn push(&mut self, samples: &[f32]) -> Vec<VoiceEvent> {
let mut events = Vec::new();
- // En media dúplex el micrófono está apagado de hecho: sin esto, el
- // asistente se transcribe a sí mismo y se responde solo.
+ // In half duplex the microphone is effectively off: without this, the
+ // assistant transcribes itself and answers itself.
if self.gate == Gate::Speaking && !self.config.barge_in {
return events;
}
@@ -211,8 +211,8 @@ impl Segmenter {
self.voiced = 0;
self.preroll.clear();
self.silence_run = 0;
- // Un corte por longitud cae a mitad de frase: se sigue escuchando
- // como si el usuario no hubiera dejado de hablar, que es la verdad.
+ // A length cut lands mid-sentence: keep listening as if the user had
+ // not stopped talking, which is the truth.
self.speaking = too_long && !ended;
if self.speaking {
events.push(VoiceEvent::Started);
@@ -221,8 +221,8 @@ impl Segmenter {
events
}
- /// El suelo de ruido sólo baja: una voz sostenida no debe poder arrastrar
- /// el umbral por encima de sí misma y dejar de detectarse.
+ /// The noise floor only goes down: sustained speech must not be able to
+ /// drag the threshold above itself and stop being detected.
fn track_noise_floor(&mut self, level: f32) {
if self.noise_floor == 0.0 {
self.noise_floor = level;
@@ -253,7 +253,7 @@ mod tests {
fn samples(secs: f32, amplitude: f32) -> Vec<f32> {
let n = (ASR_SAMPLE_RATE as f32 * secs) as usize;
- // Alterna de signo para que el RMS sea la amplitud y no un continuo.
+ // Alternates sign so the RMS is the amplitude and not a DC level.
(0..n)
.map(|i| if i % 2 == 0 { amplitude } else { -amplitude })
.collect()
@@ -264,19 +264,19 @@ mod tests {
}
#[test]
- fn el_silencio_no_arranca_ningun_turno() {
+ fn silence_starts_no_turn() {
let mut seg = Segmenter::new(&config());
let events = feed(&mut seg, 2.0, 0.0001);
assert!(
events.is_empty(),
- "el silencio no debe producir eventos: {events:?}"
+ "silence must produce no events: {events:?}"
);
}
#[test]
- fn la_voz_seguida_de_silencio_cierra_una_intervencion() {
+ fn speech_followed_by_silence_closes_an_utterance() {
let mut seg = Segmenter::new(&config());
- feed(&mut seg, 1.0, 0.0002); // deja que el suelo de ruido se asiente
+ feed(&mut seg, 1.0, 0.0002); // let the noise floor settle
let mut events = feed(&mut seg, 0.8, 0.3);
events.extend(feed(&mut seg, 0.6, 0.0002));
@@ -285,57 +285,57 @@ mod tests {
VoiceEvent::Ended(u) => Some(u),
_ => None,
});
- let utterance = ended.expect("la intervención debió cerrarse");
+ let utterance = ended.expect("the utterance should have closed");
assert!(
utterance.duration_secs() > 0.8,
- "el preroll debe ir incluido, duró {}",
+ "the preroll must be included, it lasted {}",
utterance.duration_secs()
);
}
#[test]
- fn un_ruido_corto_se_descarta() {
+ fn short_noise_is_discarded() {
let mut seg = Segmenter::new(&config());
feed(&mut seg, 1.0, 0.0002);
let mut events = feed(&mut seg, 0.1, 0.3);
events.extend(feed(&mut seg, 0.6, 0.0002));
- // 0,1 s de golpe no llegan al mínimo de 0,3 s de voz, por mucho que el
- // preroll haga que la intervención dure más.
+ // 0.1 s of a knock does not reach the 0.3 s speech minimum, however
+ // much the preroll makes the utterance last longer.
assert!(
events.iter().any(|e| matches!(e, VoiceEvent::Discarded)),
- "esperaba un descarte: {events:?}"
+ "expected a discard: {events:?}"
);
}
#[test]
- fn una_intervencion_interminable_se_corta_y_se_sigue_escuchando() {
+ fn endless_utterance_is_cut_and_listening_continues() {
let mut seg = Segmenter::new(&config());
feed(&mut seg, 1.0, 0.0002);
let events = feed(&mut seg, 3.0, 0.3);
assert!(
events.iter().any(|e| matches!(e, VoiceEvent::Ended(_))),
- "a los 2 s debe cortarse: {events:?}"
+ "it must be cut at 2 s: {events:?}"
);
assert!(
seg.is_speaking(),
- "tras un corte forzado se sigue en mitad de la frase"
+ "after a forced cut we are still mid-sentence"
);
}
#[test]
- fn en_media_duplex_el_altavoz_no_se_transcribe_a_si_mismo() {
+ fn in_half_duplex_the_speaker_is_not_transcribed() {
let mut seg = Segmenter::new(&config());
feed(&mut seg, 1.0, 0.0002);
seg.set_gate(Gate::Speaking);
let events = feed(&mut seg, 2.0, 0.5);
assert!(
events.is_empty(),
- "con barge_in apagado no debe entrar nada mientras habla el asistente: {events:?}"
+ "with barge_in off nothing may come in while the assistant talks: {events:?}"
);
}
#[test]
- fn con_barge_in_hace_falta_hablar_mas_alto_para_cortar() {
+ fn with_barge_in_you_must_speak_louder_to_interrupt() {
let mut config = config();
config.barge_in = true;
config.barge_in_factor = 4.0;
@@ -343,24 +343,24 @@ mod tests {
feed(&mut seg, 1.0, 0.0002);
seg.set_gate(Gate::Speaking);
- // Justo por encima del umbral normal pero por debajo del elevado.
+ // Just above the normal threshold but below the raised one.
let eco = seg.threshold() / config.barge_in_factor * 1.5;
let events = feed(&mut seg, 0.5, eco);
assert!(
events.is_empty(),
- "el eco del altavoz no debe cortar: {events:?}"
+ "speaker echo must not interrupt: {events:?}"
);
- let voz = seg.threshold() * 2.0;
- let events = feed(&mut seg, 0.5, voz);
+ let speech = seg.threshold() * 2.0;
+ let events = feed(&mut seg, 0.5, speech);
assert!(
events.iter().any(|e| matches!(e, VoiceEvent::BargeIn)),
- "una voz clara sí debe cortar: {events:?}"
+ "clear speech must interrupt: {events:?}"
);
}
#[test]
- fn al_reabrir_el_microfono_se_tira_la_cola_del_altavoz() {
+ fn reopening_the_microphone_drops_the_speaker_tail() {
let mut config = config();
config.barge_in = true;
let mut seg = Segmenter::new(&config);
@@ -372,25 +372,25 @@ mod tests {
seg.set_gate(Gate::Open);
assert!(
!seg.is_speaking(),
- "reabrir debe descartar lo acumulado; si no, el turno siguiente arranca con eco"
+ "reopening must drop what accumulated; otherwise the next turn starts with echo"
);
}
#[test]
- fn el_suelo_de_ruido_no_sube_con_la_voz() {
+ fn noise_floor_does_not_rise_with_speech() {
let mut seg = Segmenter::new(&config());
feed(&mut seg, 1.0, 0.0002);
- let quieto = seg.noise_floor();
+ let quiet = seg.noise_floor();
feed(&mut seg, 2.0, 0.5);
assert!(
- seg.noise_floor() <= quieto * 1.5,
- "hablar no debe elevar el suelo de ruido ({quieto} -> {})",
+ seg.noise_floor() <= quiet * 1.5,
+ "speech must not raise the noise floor ({quiet} -> {})",
seg.noise_floor()
);
}
#[test]
- fn el_cierre_entrega_la_intervencion_a_medias() {
+ fn shutdown_delivers_the_unfinished_utterance() {
let mut seg = Segmenter::new(&config());
feed(&mut seg, 1.0, 0.0002);
feed(&mut seg, 0.5, 0.3);