diff options
Diffstat (limited to 'crates/asist-app/src/pipeline.rs')
| -rw-r--r-- | crates/asist-app/src/pipeline.rs | 85 |
1 files changed, 52 insertions, 33 deletions
diff --git a/crates/asist-app/src/pipeline.rs b/crates/asist-app/src/pipeline.rs index 9e9037e..69fc66a 100644 --- a/crates/asist-app/src/pipeline.rs +++ b/crates/asist-app/src/pipeline.rs @@ -67,7 +67,7 @@ impl Pipeline { config: &Config, session: Session, recognizer: Recognizer, - llm: LlmClient, + llm: Arc<LlmClient>, tts: TtsClient, tools: ToolRegistry, playback: PlaybackHandle, @@ -175,7 +175,11 @@ fn run_segmenter( } // El micrófono se cierra mientras suena el altavoz. Con barge-in // activo no se cierra, sólo sube el listón de volumen. - segmenter.set_gate(if session.is_speaking() || playback.is_active() { + // + // 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. + segmenter.set_gate(if session.is_speaking() || playback.queued_secs() > 0.0 { Gate::Speaking } else { Gate::Open @@ -253,7 +257,7 @@ fn run_segmenter( fn run_brain( config: &Config, session: Session, - llm: LlmClient, + llm: Arc<LlmClient>, tools: ToolRegistry, results: Receiver<AsrResult>, speak: Sender<SpeakJob>, @@ -337,6 +341,12 @@ struct Prompts { /// Para la pasada en que el modelo decide si llamar a una herramienta. deciding: String, /// Para redactar la respuesta hablada, ya con los resultados en la mano. + /// + /// 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. speaking: String, /// `false` cuando no hay herramientas: entonces ambas son la misma. two_phase: bool, @@ -352,7 +362,11 @@ impl Prompts { } else { speaking.clone() }, - speaking, + speaking: if two_phase { + format!("{speaking}\n\n{}", config.general.tool_result_prompt.trim()) + } else { + speaking.clone() + }, two_phase, } } @@ -377,6 +391,27 @@ 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. + let mut emitted = 0usize; + + // Manda una frase al sintetizador. Devuelve `false` si el canal se cerró. + let say = |index: &mut usize, text: String| -> bool { + let _ = events.send(Event::Sentence { + turn, + index: *index, + text: text.clone(), + }); + let job = SpeakJob { + turn, + index: *index, + text, + turn_started, + }; + *index += 1; + speak.send(job).is_ok() + }; for round in 0..=config.llm.max_tool_rounds { let llm_started = Instant::now(); @@ -403,21 +438,7 @@ fn answer( // Aquí está el solape: cada frase cerrada sale hacia el // sintetizador sin esperar al resto de la respuesta. for sentence in splitter.push(text) { - let index = splitter.emitted() - 1; - let _ = events.send(Event::Sentence { - turn, - index, - text: sentence.clone(), - }); - if speak - .send(SpeakJob { - turn, - index, - text: sentence, - turn_started, - }) - .is_err() - { + if !say(&mut emitted, sentence) { return false; } } @@ -449,20 +470,9 @@ fn answer( if outcome.tool_calls.is_empty() { if let Some(rest) = splitter.flush() { - let index = splitter.emitted() - 1; - let _ = events.send(Event::Sentence { - turn, - index, - text: rest.clone(), - }); - let _ = speak.send(SpeakJob { - turn, - index, - text: rest, - turn_started, - }); + say(&mut emitted, rest); } - timer.sentences = splitter.emitted(); + timer.sentences = emitted; chat.push(Message::assistant(outcome.text.clone())); // El turno siguiente vuelve a empezar decidiendo. if prompts.two_phase { @@ -500,6 +510,14 @@ 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. + if config.tools.spoken_ack { + if let Some(ack) = tools.get(&call.name).and_then(|t| t.acknowledgement()) { + say(&mut emitted, ack.to_string()); + } + } let result = tools.dispatch(call); let _ = events.send(Event::ToolFinished { turn, @@ -553,8 +571,9 @@ fn run_speech( let Some(job) = job else { // Sin trabajo: si ya no queda audio, el turno ha terminado de sonar. if let Some(turn) = speaking_turn { - if !playback.is_active() || playback.queued_secs() <= 0.0 { + if playback.queued_secs() <= 0.0 { session.set_speaking(false); + playback.mark_idle(); speaking_turn = None; let _ = events.send(Event::AudioFinished { turn }); } |