aboutsummaryrefslogtreecommitdiffstats
path: root/crates/asist-app/src/pipeline.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/asist-app/src/pipeline.rs')
-rw-r--r--crates/asist-app/src/pipeline.rs85
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 });
}