swarm-stt: reply deadline agrees with the limits, one request per agent, short errors to the sender
This commit is contained in:
parent
ee10acb154
commit
f5e7caa2eb
1 changed files with 194 additions and 65 deletions
|
|
@ -14,6 +14,11 @@ pub trait Engine: Send + Sync {
|
|||
async fn transcribe(&self, audio: &[u8]) -> Result<String, String>;
|
||||
}
|
||||
|
||||
/// What a sender is told when the service could not run the tools at all.
|
||||
/// The detail (paths, OS errors, tool output) goes to the log, never to the
|
||||
/// sender.
|
||||
const COULD_NOT_START: &str = "the service could not start the transcription";
|
||||
|
||||
/// `ffmpeg` decodes any container to 16 kHz mono WAV, then `whisper-cli` reads
|
||||
/// it with the configured model.
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
@ -42,14 +47,17 @@ impl Engine for Whisper {
|
|||
.to_owned(),
|
||||
);
|
||||
}
|
||||
let dir = tempfile_in(&self.scratch)
|
||||
let dir = tempfile_in(&self.scratch).await.map_err(|e| {
|
||||
tracing::warn!(error = %e, "making a scratch directory failed");
|
||||
COULD_NOT_START.to_owned()
|
||||
})?;
|
||||
// Dropping `run` on a timeout kills the child that was running.
|
||||
let result = tokio::time::timeout(self.timeout, self.run(&dir, audio))
|
||||
.await
|
||||
.map_err(|e| format!("making a scratch directory: {e}"))?;
|
||||
let result = match tokio::time::timeout(self.timeout, self.run(&dir, audio)).await {
|
||||
Ok(result) => result,
|
||||
// Dropping `run` kills the child that was running.
|
||||
Err(_) => Err(format!("timed out after {}s", self.timeout.as_secs())),
|
||||
};
|
||||
.unwrap_or_else(|_| {
|
||||
tracing::warn!(secs = self.timeout.as_secs(), "a transcription timed out");
|
||||
Err("the transcription timed out".to_owned())
|
||||
});
|
||||
let _ = tokio::fs::remove_dir_all(&dir).await;
|
||||
result
|
||||
}
|
||||
|
|
@ -80,20 +88,24 @@ impl Whisper {
|
|||
.stderr(Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.map_err(|e| format!("starting ffmpeg: {e}"))?;
|
||||
.map_err(|e| {
|
||||
tracing::warn!(error = %e, "starting ffmpeg failed");
|
||||
COULD_NOT_START.to_owned()
|
||||
})?;
|
||||
if let Some(mut stdin) = decode.stdin.take() {
|
||||
// ffmpeg may stop reading early (a cut at `-t`): a broken pipe is fine.
|
||||
let _ = stdin.write_all(audio).await;
|
||||
}
|
||||
let decoded = decode
|
||||
.wait_with_output()
|
||||
.await
|
||||
.map_err(|e| format!("waiting for ffmpeg: {e}"))?;
|
||||
let decoded = decode.wait_with_output().await.map_err(|e| {
|
||||
tracing::warn!(error = %e, "waiting for ffmpeg failed");
|
||||
COULD_NOT_START.to_owned()
|
||||
})?;
|
||||
if !decoded.status.success() {
|
||||
return Err(format!(
|
||||
"ffmpeg could not decode the audio: {}",
|
||||
String::from_utf8_lossy(&decoded.stderr).trim()
|
||||
));
|
||||
tracing::warn!(
|
||||
stderr = %String::from_utf8_lossy(&decoded.stderr).trim(),
|
||||
"ffmpeg could not decode the audio"
|
||||
);
|
||||
return Err("the audio could not be decoded".to_owned());
|
||||
}
|
||||
let heard = Command::new(&self.whisper)
|
||||
.arg("-m")
|
||||
|
|
@ -105,12 +117,16 @@ impl Whisper {
|
|||
.kill_on_drop(true)
|
||||
.output()
|
||||
.await
|
||||
.map_err(|e| format!("starting whisper-cli: {e}"))?;
|
||||
.map_err(|e| {
|
||||
tracing::warn!(error = %e, "starting whisper-cli failed");
|
||||
COULD_NOT_START.to_owned()
|
||||
})?;
|
||||
if !heard.status.success() {
|
||||
return Err(format!(
|
||||
"whisper-cli failed: {}",
|
||||
String::from_utf8_lossy(&heard.stderr).trim()
|
||||
));
|
||||
tracing::warn!(
|
||||
stderr = %String::from_utf8_lossy(&heard.stderr).trim(),
|
||||
"whisper-cli failed"
|
||||
);
|
||||
return Err("the transcription failed".to_owned());
|
||||
}
|
||||
Ok(String::from_utf8_lossy(&heard.stdout).trim().to_owned())
|
||||
}
|
||||
|
|
@ -221,24 +237,18 @@ mod tests {
|
|||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn an_ffmpeg_failure_is_reported_with_its_stderr() {
|
||||
async fn an_ffmpeg_failure_tells_the_sender_briefly_and_keeps_the_detail_out() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let engine = whisper(dir.path(), "echo 'bad data' >&2; exit 1", WHISPER_ECHO);
|
||||
let err = engine.transcribe(b"OggSx").await.unwrap_err();
|
||||
assert!(
|
||||
err.contains("could not decode") && err.contains("bad data"),
|
||||
"{err}"
|
||||
);
|
||||
assert_eq!(err, "the audio could not be decoded");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn a_whisper_failure_is_reported_with_its_stderr() {
|
||||
async fn a_whisper_failure_tells_the_sender_briefly_and_keeps_the_detail_out() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let engine = whisper(dir.path(), FFMPEG_OK, "echo 'no model' >&2; exit 2");
|
||||
let err = engine.transcribe(b"OggSx").await.unwrap_err();
|
||||
assert!(
|
||||
err.contains("whisper-cli failed") && err.contains("no model"),
|
||||
"{err}"
|
||||
);
|
||||
assert_eq!(err, "the transcription failed");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue