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>
This commit is contained in:
1 parent
264a6239e1
commit
d12897a928
11 files changed
+7048
No files matched your search
@@ -0,0 +1,67 @@
|
||||
//! 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())
|
||||
}
|
||||
Reference in new issue
Block a user