From 8518a63f55153e7f45fd49ad6caff5555f4e374f Mon Sep 17 00:00:00 2001 From: elvis Date: Sat, 26 Sep 2026 20:20:19 -0300 Subject: Translate code, comments, logs and terminal UI to English; add English README; rename scripts --- crates/asist-app/src/pipeline.rs | 176 +++++++++++++++++++-------------------- 1 file changed, 88 insertions(+), 88 deletions(-) (limited to 'crates/asist-app/src/pipeline.rs') diff --git a/crates/asist-app/src/pipeline.rs b/crates/asist-app/src/pipeline.rs index 69fc66a..e8eef18 100644 --- a/crates/asist-app/src/pipeline.rs +++ b/crates/asist-app/src/pipeline.rs @@ -1,23 +1,23 @@ -//! El orquestador: los hilos del asistente y los canales que los unen. +//! The orchestrator: the assistant threads and the channels that join them. //! //! ```text -//! micrófono ──muestras──> segmentador ──intervención──> ASR ──texto──┐ -//! (cpal, tiempo real) (VAD, turnos) (Canary) │ -//! v -//! altavoz <──muestras── síntesis <──frases── conversación <──────────┘ -//! (cpal, anillo) (qwentts) (llama.cpp + herramientas) +//! microphone ──samples──> segmenter ──utterance──> ASR ──text──┐ +//! (cpal, real time) (VAD, turns) (Canary) │ +//! v +//! speaker <──samples── synthesis <──sentences── conversation <─┘ +//! (cpal, ring) (qwentts) (llama.cpp + tools) //! ``` //! -//! Cada caja es un hilo y cada flecha un canal. Dos consecuencias que valen -//! por todo el diseño: +//! Each box is a thread and each arrow a channel. Two consequences carry the +//! whole design: //! -//! * **Nada bloquea al que va delante.** El micrófono nunca espera al ASR, y -//! el modelo nunca espera al sintetizador; quien va sobrado descarta trabajo -//! en lugar de acumular retraso. -//! * **Se responde por frases, no por respuestas.** La conversación entrega -//! cada frase al sintetizador en cuanto está cerrada, así que el asistente -//! empieza a hablar mientras el modelo sigue escribiendo. Es lo que separa -//! una latencia de medio segundo de una de cinco. +//! * **Nothing blocks the stage ahead.** The microphone never waits for the +//! ASR, and the model never waits for the synthesizer; whoever has spare +//! capacity drops work instead of accumulating delay. +//! * **Answers go out per sentence, not per answer.** The conversation hands +//! each sentence to the synthesizer as soon as it is closed, so the assistant +//! starts speaking while the model is still writing. That is what separates +//! half a second of latency from five. use std::sync::Arc; use std::thread::{self, JoinHandle}; @@ -41,17 +41,17 @@ use asist_tts::TtsClient; use crate::session::Session; -/// Una frase pendiente de sintetizar. +/// A sentence waiting to be synthesized. #[derive(Debug)] pub struct SpeakJob { pub turn: TurnId, pub index: usize, pub text: String, - /// Origen desde el que se mide la latencia percibida del turno. + /// Origin from which the perceived latency of the turn is measured. pub turn_started: Instant, } -/// Los hilos en marcha, para poder esperarlos al cerrar. +/// The running threads, so they can be joined on shutdown. pub struct Pipeline { handles: Vec>, pub events: Receiver, @@ -61,7 +61,7 @@ pub struct Pipeline { } impl Pipeline { - /// Monta el pipeline entero. `capture_rx` viene del micrófono. + /// Builds the whole pipeline. `capture_rx` comes from the microphone. #[allow(clippy::too_many_arguments)] pub fn spawn( config: &Config, @@ -78,15 +78,15 @@ impl Pipeline { let (event_tx, event_rx) = unbounded::(); let (asr_tx, asr_rx) = unbounded::(); let (result_tx, result_rx) = unbounded::(); - // Acotado a propósito: si la síntesis se atasca, el hilo de - // conversación debe notarlo y frenar en lugar de acumular frases que - // llegarán tarde a un turno que quizá ya se ha interrumpido. + // Bounded on purpose: if synthesis gets stuck, the conversation thread + // must notice and slow down instead of piling up sentences that will + // arrive late to a turn that may already have been interrupted. let (speak_tx, speak_rx) = bounded::(8); let metrics = Arc::new(Metrics::default()); let mut handles = Vec::new(); - handles.push(spawn_named("segmentador", { + handles.push(spawn_named("segmenter", { let config = config.clone(); let session = session.clone(); let events = event_tx.clone(); @@ -108,7 +108,7 @@ impl Pipeline { recognizer.run(asr_rx, result_tx) })); - handles.push(spawn_named("conversación", { + handles.push(spawn_named("conversation", { let config = config.clone(); let session = session.clone(); let events = event_tx.clone(); @@ -120,7 +120,7 @@ impl Pipeline { } })); - handles.push(spawn_named("síntesis", { + handles.push(spawn_named("synthesis", { let session = session.clone(); let events = event_tx; move || run_speech(session, tts, speak_rx, playback, events) @@ -135,8 +135,8 @@ impl Pipeline { }) } - /// Cierra en orden: se corta lo que suena, se suelta el emisor del - /// micrófono y con eso la cadena de hilos se desmonta sola. + /// Shuts down in order: what is playing is cut, the microphone sender is + /// dropped, and with that the chain of threads takes itself apart. pub fn shutdown(mut self) { self.session.request_stop(); self.capture_tx.take(); @@ -150,11 +150,11 @@ fn spawn_named(name: &str, body: impl FnOnce() + Send + 'static) -> JoinHandle<( thread::Builder::new() .name(name.to_string()) .spawn(body) - .expect("no se pudo crear un hilo del pipeline") + .expect("could not create a pipeline thread") } // --------------------------------------------------------------------------- -// Segmentador: micrófono -> intervenciones +// Segmenter: microphone -> utterances // --------------------------------------------------------------------------- fn run_segmenter( @@ -173,27 +173,27 @@ fn run_segmenter( if session.is_stopping() { break; } - // El micrófono se cierra mientras suena el altavoz. Con barge-in - // activo no se cierra, sólo sube el listón de volumen. + // The microphone is closed while the speaker is playing. With barge-in + // on it is not closed; only the volume bar goes up. // - // La segunda condición mira la cola y no una bandera a propósito: la - // cola no puede quedarse desfasada, y una bandera que se olvide de - // bajar deja el micrófono cerrado para el resto de la sesión. + // The second condition looks at the queue and not at a flag on purpose: + // the queue cannot fall out of sync, and a flag someone forgets to lower + // leaves the microphone closed for the rest of the session. segmenter.set_gate(if session.is_speaking() || playback.queued_secs() > 0.0 { Gate::Speaking } else { Gate::Open }); - // El dispositivo se abre a 16 kHz mono siempre que puede, y entonces - // esto no hace nada; cuando no puede, es aquí donde se convierte. + // The device is opened at 16 kHz mono whenever possible, and then this + // does nothing; when it cannot be, this is where it gets converted. let mono = asist_audio::to_mono_at(&block.samples, input_format, ASR_SAMPLE_RATE); for event in segmenter.push(&mono) { match event { VoiceEvent::BargeIn => { - // Se corta antes de abrir el turno nuevo: así lo que - // quedaba en el anillo no se cuela por encima. + // Cut before opening the new turn: that way whatever was left + // in the ring does not leak over it. playback.stop(); session.interrupt(); let _ = events.send(Event::Interrupted { @@ -239,7 +239,7 @@ fn run_segmenter( } } } - // Lo que quedara a medias al cerrar todavía merece transcribirse. + // Whatever was left half-done at shutdown still deserves a transcription. if let Some(utterance) = segmenter.flush() { let _ = asr.send(AsrJob::Utterance { turn, @@ -250,7 +250,7 @@ fn run_segmenter( } // --------------------------------------------------------------------------- -// Conversación: transcripción -> frases +// Conversation: transcription -> sentences // --------------------------------------------------------------------------- #[allow(clippy::too_many_arguments)] @@ -297,10 +297,10 @@ fn run_brain( decode, spoken_at, } => { - // Una transcripción de un turno superado llega tarde: el - // usuario ya ha dicho otra cosa. + // A transcription from an outdated turn arrives too late: the + // user has already said something else. if !session.is_current(turn) { - tracing::debug!(target: "brain", turno = turn.0, "transcripción obsoleta"); + tracing::debug!(target: "brain", turn = turn.0, "stale transcription"); continue; } let mut timer = TurnTimer::new(turn); @@ -321,7 +321,7 @@ fn run_brain( ); if config.general.report_latency { - tracing::info!(target: "latencia", "\n{}", timer.report()); + tracing::info!(target: "latency", "\n{}", timer.report()); } metrics.record_turn(&timer); } @@ -329,26 +329,26 @@ fn run_brain( } } -/// Las dos instrucciones de sistema entre las que alterna un turno. +/// The two system prompts a turn alternates between. /// -/// Que sean dos no es un capricho de diseño sino un resultado medido: con -/// Qwen3.5-2B, cualquier indicación de estilo junto a la guía de herramientas -/// hace que el modelo 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 añadida). Así que la -/// pasada en que el modelo *decide* si actuar lleva la guía a solas, y la -/// instrucción de voz se reserva para redactar lo que se va a pronunciar. +/// Having two is not a design whim but a measured result: with Qwen3.5-2B, +/// any style instruction next to the tool guide makes the model stop calling +/// tools and make the data up (8/8 hits with the guide alone, 0/8 with the +/// voice-assistant persona added). So the pass where the model *decides* +/// whether to act carries the guide alone, and the voice prompt is kept for +/// writing what will be spoken. struct Prompts { - /// Para la pasada en que el modelo decide si llamar a una herramienta. + /// For the pass where the model decides whether to call a tool. deciding: String, - /// Para redactar la respuesta hablada, ya con los resultados en la mano. + /// For writing the spoken answer, with the results already at hand. /// - /// Lleva pegada la orden de usar lo que la herramienta devolvió. Sin ella - /// el modelo se limita a anunciar lo que acaba de hacer —«he tomado una - /// foto, ahora puedo responderte sobre el objeto o color»— y se deja el - /// dato que tenía delante. Aquí sí se puede añadir estilo sin riesgo: la - /// llamada ya ocurrió, así que no hay nada que estropear. + /// It carries the instruction to use what the tool returned. Without it the + /// model just announces what it did («he tomado una foto, ahora puedo + /// responderte sobre el objeto o color») and leaves out the data it had in + /// front of it. Style can be added safely here: the call already happened, + /// so there is nothing left to break. speaking: String, - /// `false` cuando no hay herramientas: entonces ambas son la misma. + /// `false` when there are no tools: then both are the same. two_phase: bool, } @@ -372,8 +372,8 @@ impl Prompts { } } -/// Genera la respuesta de un turno, resolviendo herramientas si el modelo las -/// pide, y va soltando frases al sintetizador según se cierran. +/// Generates the answer of a turn, resolving tools if the model asks for +/// them, and releases sentences to the synthesizer as they close. #[allow(clippy::too_many_arguments)] fn answer( config: &Config, @@ -391,12 +391,12 @@ fn answer( let tools = (!tools.is_empty() && config.tools.enabled).then_some(tools); let mut splitter = SentenceSplitter::new(); let mut spoken = String::new(); - // Índice propio, y no el del troceador, porque al flujo de frases se le - // cuelan los acuses de las herramientas. El índice 0 marca el primer - // sonido del turno, que es de donde se mide la latencia percibida. + // Own index, not the chunker's, because tool acknowledgements slip into + // the sentence stream. Index 0 marks the first sound of the turn, which is + // where perceived latency is measured from. let mut emitted = 0usize; - // Manda una frase al sintetizador. Devuelve `false` si el canal se cerró. + // Sends a sentence to the synthesizer. Returns `false` if the channel closed. let say = |index: &mut usize, text: String| -> bool { let _ = events.send(Event::Sentence { turn, @@ -435,8 +435,8 @@ fn answer( text: text.clone(), }); spoken.push_str(text); - // Aquí está el solape: cada frase cerrada sale hacia el - // sintetizador sin esperar al resto de la respuesta. + // This is the overlap: every closed sentence goes to the + // synthesizer without waiting for the rest of the answer. for sentence in splitter.push(text) { if !say(&mut emitted, sentence) { return false; @@ -474,7 +474,7 @@ fn answer( } timer.sentences = emitted; chat.push(Message::assistant(outcome.text.clone())); - // El turno siguiente vuelve a empezar decidiendo. + // The next turn starts again by deciding. if prompts.two_phase { chat.set_system(&prompts.deciding); } @@ -485,20 +485,20 @@ fn answer( return; } - // El modelo ha pedido herramientas: se ejecutan, se le devuelve el - // resultado y se le vuelve a preguntar. + // The model asked for tools: they run, the result goes back to it and + // it is asked again. if round == config.llm.max_tool_rounds { let _ = events.send(Event::Warning { turn, message: format!( - "el modelo siguió pidiendo herramientas tras {} vueltas; se corta", + "the model kept asking for tools after {} rounds; stopping", config.llm.max_tool_rounds ), }); return; } - let tools = tools.expect("no puede haber llamadas sin herramientas registradas"); + let tools = tools.expect("there cannot be calls without registered tools"); chat.push(Message::tool_request( outcome.text.clone(), outcome.tool_calls.clone(), @@ -510,9 +510,9 @@ fn answer( name: call.name.clone(), arguments: call.arguments.clone(), }); - // Se dice antes de ejecutar, no después: la gracia es tapar la - // espera, y una búsqueda con las dos pasadas del modelo detrás son - // seis segundos que sin esto pasan en silencio absoluto. + // Said before running, not after: the point is to cover the + // wait, and a search with the two model passes behind it is + // six seconds that would otherwise pass in total silence. if config.tools.spoken_ack { if let Some(ack) = tools.get(&call.name).and_then(|t| t.acknowledgement()) { say(&mut emitted, ack.to_string()); @@ -531,9 +531,9 @@ fn answer( } timer.record(Stage::Tools, tools_started.elapsed()); - // Con el resultado ya en la conversación, la siguiente vuelta sólo - // tiene que redactar: es el momento de recuperar la instrucción de - // voz, que en la pasada anterior habría impedido la llamada. + // With the result already in the conversation, the next round only + // has to write: this is when the voice prompt comes back, which in + // the previous pass would have prevented the call. if prompts.two_phase { chat.set_system(&prompts.speaking); } @@ -541,7 +541,7 @@ fn answer( } // --------------------------------------------------------------------------- -// Síntesis: frases -> altavoz +// Synthesis: sentences -> speaker // --------------------------------------------------------------------------- fn run_speech( @@ -551,9 +551,9 @@ fn run_speech( playback: PlaybackHandle, events: Sender, ) { - // Un `Select` en vez de `recv()` a secas para poder despertar - // periódicamente y bajar la bandera de «hablando» cuando el anillo se - // vacía: si no, el micrófono seguiría cerrado tras la última frase. + // A `Select` instead of a plain `recv()` so it can wake up periodically + // and lower the «speaking» flag when the ring empties: otherwise the + // microphone would stay closed after the last sentence. let mut select = Select::new(); let job_index = select.recv(&jobs); let mut speaking_turn: Option = None; @@ -569,7 +569,7 @@ fn run_speech( }; let Some(job) = job else { - // Sin trabajo: si ya no queda audio, el turno ha terminado de sonar. + // No work: if there is no audio left, the turn has finished playing. if let Some(turn) = speaking_turn { if playback.queued_secs() <= 0.0 { session.set_speaking(false); @@ -584,9 +584,9 @@ fn run_speech( continue; }; - // Una frase de un turno ya superado no debe llegar a oírse. + // A sentence from an outdated turn must never be heard. if !session.is_current(job.turn) { - tracing::debug!(target: "tts", turno = job.turn.0, "frase obsoleta, descartada"); + tracing::debug!(target: "tts", turn = job.turn.0, "stale sentence, dropped"); continue; } @@ -623,8 +623,8 @@ fn run_speech( }); continue; } - // La cuenta del primer audio puede no haberse dado si el - // anillo aún no había servido nada cuando llegó el bloque. + // The first-audio mark may not have been set if the ring had + // not played anything yet when the block arrived. if first_of_turn && !announced { let _ = events.send(Event::AudioStarted { turn, @@ -635,7 +635,7 @@ fn run_speech( tracing::warn!( target: "tts", rtf = outcome.rtf(), - "la síntesis va por detrás del tiempo real; la voz se cortará a trozos" + "synthesis is running behind real time; the voice will break up" ); } } -- cgit v1.2.3