Files
tts/tts-studio/src/http.rs
T
Renato PfeilerandClaude Opus 5.5 d12897a928 TTS Studio: Rust-GUI fuer Ansagen mit Qwen3-TTS
Modell- und Stimmenauswahl, neue Klonstimme per Datei oder Mikrofon
(automatisch aufbereitet, an Satzpause gekuerzt, per Whisper transkribiert),
Texteingabe mit Pausen, Tempo, Satz-/Kommapausen mit Raumklang,
Hintergrundmusik, Ziel Asterisk (WAV/G.722) oder WhatsApp (MP3),
Vorhoeren und Speichern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 15:52:47 +02:00

68 lines
2.9 KiB
Rust

//! Aufrufe an die Server auf der DGX: Sprache erzeugen (Qwen3-TTS),
//! transkribieren (Whisper), Erreichbarkeit pruefen.
use std::time::Duration;
fn agent(timeout_secs: u64) -> ureq::Agent {
ureq::Agent::config_builder()
.timeout_global(Some(Duration::from_secs(timeout_secs)))
// Fehlerantworten selbst auswerten, der Server erklaert im Body, was nicht passt
.http_status_as_error(false)
.build()
.into()
}
fn net_err(url: &str, e: ureq::Error) -> String {
format!("{url} nicht erreichbar: {e}")
}
/// Liest die Antwort; bei Fehlerstatus wird die Meldung des Servers zurueckgegeben.
fn read_body(url: &str, mut resp: ureq::http::Response<ureq::Body>) -> Result<Vec<u8>, String> {
let status = resp.status();
let body = resp.body_mut().with_config().limit(100 * 1024 * 1024).read_to_vec().map_err(|e| net_err(url, e))?;
if status.is_success() {
return Ok(body);
}
let text = String::from_utf8_lossy(&body);
// vLLM: {"error": {"message": "..."}}
let msg = serde_json::from_str::<serde_json::Value>(&text)
.ok()
.and_then(|v| v["error"]["message"].as_str().map(String::from))
.unwrap_or_else(|| text.trim().to_string());
Err(format!("Server meldet {status}: {msg}"))
}
/// POST /v1/audio/speech -> WAV-Bytes. Erster Aufruf nach dem Serverstart dauert ~20 s.
pub fn speech(url: &str, body: &serde_json::Value) -> Result<Vec<u8>, String> {
let resp = agent(300).post(url).send_json(body).map_err(|e| net_err(url, e))?;
read_body(url, resp)
}
/// POST /v1/audio/transcriptions (multipart) -> erkannter Text.
pub fn transcribe(url: &str, model: &str, wav: &[u8]) -> Result<String, String> {
let boundary = format!("----tts-studio-{}", std::process::id());
let mut body = Vec::with_capacity(wav.len() + 512);
for (name, value) in [("model", model), ("language", "de"), ("response_format", "json")] {
body.extend(format!("--{boundary}\r\nContent-Disposition: form-data; name=\"{name}\"\r\n\r\n{value}\r\n").as_bytes());
}
body.extend(
format!("--{boundary}\r\nContent-Disposition: form-data; name=\"file\"; filename=\"stimme.wav\"\r\nContent-Type: audio/wav\r\n\r\n")
.as_bytes(),
);
body.extend(wav);
body.extend(format!("\r\n--{boundary}--\r\n").as_bytes());
let resp = agent(180)
.post(url)
.header("Content-Type", format!("multipart/form-data; boundary={boundary}"))
.send(&body[..])
.map_err(|e| net_err(url, e))?;
let json: serde_json::Value = serde_json::from_slice(&read_body(url, resp)?).map_err(|e| format!("Whisper-Antwort unlesbar: {e}"))?;
json["text"].as_str().map(|t| t.trim().to_string()).ok_or_else(|| "Whisper hat keinen Text geliefert.".into())
}
/// Kurzer Check, ob ein Server antwortet (GET .../v1/models).
pub fn reachable(models_url: &str) -> bool {
agent(3).get(models_url).call().is_ok_and(|r| r.status().is_success())
}