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>
68 lines
2.9 KiB
Rust
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())
|
|
}
|