84f8df2bf4
Introduces a new module for interacting with the Whisper ASR service. This includes an async function to POST audio files and retrieve transcripts. Error handling for network and HTTP status codes is also implemented. New tests are added to cover successful transcription, HTTP error responses, and request timeouts.
119 lines
3.6 KiB
Rust
119 lines
3.6 KiB
Rust
use std::path::PathBuf;
|
|
use std::time::Duration;
|
|
|
|
use doctate_server::transcribe::ffmpeg::remux_faststart;
|
|
use doctate_server::transcribe::whisper::{transcribe, WhisperError};
|
|
use wiremock::matchers::{method, path, query_param};
|
|
use wiremock::{Mock, MockServer, ResponseTemplate};
|
|
|
|
fn fixture(name: &str) -> PathBuf {
|
|
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
|
.join("tests/fixtures")
|
|
.join(name)
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn ffmpeg_remux_produces_valid_output() {
|
|
let input = fixture("sample.m4a");
|
|
assert!(input.exists(), "fixture missing: {}", input.display());
|
|
|
|
let output = remux_faststart(&input)
|
|
.await
|
|
.expect("remux failed");
|
|
|
|
let meta = std::fs::metadata(output.path()).expect("output missing");
|
|
assert!(meta.len() > 0, "output file is empty");
|
|
|
|
// Sanity check: ffprobe should still recognize it as an m4a/mp4 container.
|
|
let probe = std::process::Command::new("ffprobe")
|
|
.arg("-hide_banner")
|
|
.arg("-loglevel")
|
|
.arg("error")
|
|
.arg("-show_entries")
|
|
.arg("format=format_name")
|
|
.arg("-of")
|
|
.arg("default=nw=1:nk=1")
|
|
.arg(output.path())
|
|
.output()
|
|
.expect("ffprobe spawn failed");
|
|
assert!(probe.status.success(), "ffprobe failed");
|
|
let fmt = String::from_utf8_lossy(&probe.stdout);
|
|
assert!(fmt.contains("mp4") || fmt.contains("m4a"), "unexpected format: {fmt}");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn ffmpeg_remux_fails_on_missing_input() {
|
|
let input = PathBuf::from("/nonexistent/does-not-exist.m4a");
|
|
let result = remux_faststart(&input).await;
|
|
assert!(result.is_err(), "expected error for missing input");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn whisper_client_posts_multipart_and_returns_text() {
|
|
let server = MockServer::start().await;
|
|
let expected = "Hallo Welt, das ist ein Test.";
|
|
|
|
Mock::given(method("POST"))
|
|
.and(path("/asr"))
|
|
.and(query_param("output", "txt"))
|
|
.and(query_param("encode", "false"))
|
|
.respond_with(ResponseTemplate::new(200).set_body_string(expected))
|
|
.expect(1)
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = reqwest::Client::new();
|
|
let text = transcribe(&client, &server.uri(), &fixture("sample.m4a"), Duration::from_secs(10))
|
|
.await
|
|
.expect("transcribe failed");
|
|
|
|
assert_eq!(text, expected);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn whisper_client_returns_status_error_on_500() {
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("POST"))
|
|
.and(path("/asr"))
|
|
.respond_with(ResponseTemplate::new(500).set_body_string("boom"))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = reqwest::Client::new();
|
|
let err = transcribe(&client, &server.uri(), &fixture("sample.m4a"), Duration::from_secs(10))
|
|
.await
|
|
.expect_err("expected error");
|
|
|
|
match err {
|
|
WhisperError::Status { status, body } => {
|
|
assert_eq!(status, 500);
|
|
assert_eq!(body, "boom");
|
|
}
|
|
other => panic!("unexpected error variant: {other:?}"),
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn whisper_client_times_out() {
|
|
let server = MockServer::start().await;
|
|
|
|
Mock::given(method("POST"))
|
|
.and(path("/asr"))
|
|
.respond_with(ResponseTemplate::new(200).set_delay(Duration::from_secs(2)))
|
|
.mount(&server)
|
|
.await;
|
|
|
|
let client = reqwest::Client::new();
|
|
let err = transcribe(
|
|
&client,
|
|
&server.uri(),
|
|
&fixture("sample.m4a"),
|
|
Duration::from_millis(100),
|
|
)
|
|
.await
|
|
.expect_err("expected timeout");
|
|
|
|
assert!(matches!(err, WhisperError::Http(_)), "expected Http error, got {err:?}");
|
|
}
|