From 05bc35208d36dd9356d5f23c6665f3247456b217 Mon Sep 17 00:00:00 2001 From: Eddy Date: Tue, 21 Apr 2026 13:43:21 +0200 Subject: [PATCH] feat: Kerstin als Standard-Stimme + Stimmenauswahl [appimage] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Standard-TTS von Thorsten (männlich) auf Kerstin (weiblich) gewechselt - text_to_speech nutzt jetzt den voice-Parameter (vorher ignoriert) - get_tts_voices zeigt alle verfügbaren Stimmen mit Verfügbarkeits-Check - 5 deutsche Stimmen: Kerstin, Thorsten HQ, Thorsten, Eva, Ramona - Modell-Suche mit dynamischem Dateinamen pro Stimme Co-Authored-By: Claude Opus 4.6 --- src-tauri/src/voice.rs | 101 +++++++++++++++++++++++++++++------------ 1 file changed, 72 insertions(+), 29 deletions(-) diff --git a/src-tauri/src/voice.rs b/src-tauri/src/voice.rs index c1a4ba7..d251ade 100644 --- a/src-tauri/src/voice.rs +++ b/src-tauri/src/voice.rs @@ -49,33 +49,58 @@ fn whisper_model_path() -> String { }) } +/// Verfügbare Piper-Modelle mit Dateinamen +const PIPER_VOICES: &[(&str, &str)] = &[ + ("kerstin", "de_DE-kerstin-low.onnx"), + ("thorsten-high", "de_DE-thorsten-high.onnx"), + ("thorsten", "de_DE-thorsten-medium.onnx"), + ("eva", "de_DE-eva_k-x_low.onnx"), + ("ramona", "de_DE-ramona-low.onnx"), +]; + +/// Standard-Stimme: Kerstin (weiblich, deutsch) +const DEFAULT_VOICE: &str = "kerstin"; + fn piper_model_path() -> String { - std::env::var("PIPER_MODEL") - .unwrap_or_else(|_| { - let exe_dir = std::env::current_exe() - .ok() - .and_then(|p| p.parent().map(|p| p.to_path_buf())); + piper_model_for_voice(None) +} - let home_dir = std::env::var("HOME").ok() - .map(std::path::PathBuf::from); +/// Modell-Pfad für eine bestimmte Stimme (oder Default) +fn piper_model_for_voice(voice: Option<&str>) -> String { + // Env-Override hat Vorrang + if let Ok(path) = std::env::var("PIPER_MODEL") { + return path; + } - let candidates = vec![ - // Relativ zum Binary (Dev-Modus) - exe_dir.as_ref().map(|d| d.join("../models/de_DE-thorsten-high.onnx")), - exe_dir.as_ref().map(|d| d.join("models/de_DE-thorsten-high.onnx")), - // XDG Data Home / Home-Verzeichnis (AppImage + Nix-Wrapper) - home_dir.as_ref().map(|d| d.join(".local/share/claude-desktop/models/de_DE-thorsten-high.onnx")), - home_dir.as_ref().map(|d| d.join(".claude-desktop/models/de_DE-thorsten-high.onnx")), - // CWD Fallback - Some(std::path::PathBuf::from("models/de_DE-thorsten-high.onnx")), - ]; + let voice_id = voice.unwrap_or(DEFAULT_VOICE); + let filename = PIPER_VOICES.iter() + .find(|(id, _)| *id == voice_id) + .map(|(_, f)| *f) + .unwrap_or(PIPER_VOICES[0].1); // Fallback auf Kerstin - candidates.into_iter() - .flatten() - .find(|p| p.exists()) - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|| "models/de_DE-thorsten-high.onnx".to_string()) - }) + let exe_dir = std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())); + + let home_dir = std::env::var("HOME").ok() + .map(std::path::PathBuf::from); + + let candidates = vec![ + // Relativ zum Binary (Dev-Modus) + exe_dir.as_ref().map(|d| d.join("../models").join(filename)), + exe_dir.as_ref().map(|d| d.join("models").join(filename)), + // XDG Data Home / Home-Verzeichnis (AppImage + Nix-Wrapper) + home_dir.as_ref().map(|d| d.join(".local/share/claude-desktop/models").join(filename)), + home_dir.as_ref().map(|d| d.join(".claude-desktop/models").join(filename)), + // CWD Fallback + Some(std::path::PathBuf::from("models").join(filename)), + ]; + + candidates.into_iter() + .flatten() + .find(|p| p.exists()) + .map(|p| p.to_string_lossy().to_string()) + .unwrap_or_else(|| format!("models/{}", filename)) } /// Voice-System Status @@ -227,9 +252,9 @@ pub async fn transcribe_audio( #[tauri::command] pub async fn text_to_speech( text: String, - _voice: Option, // Wird für Piper ignoriert (Modell bestimmt Stimme) + voice: Option, ) -> Result { - let model = piper_model_path(); + let model = piper_model_for_voice(voice.as_deref()); if !std::path::Path::new(&model).exists() { return Err(format!("Piper-Modell nicht gefunden: {}", model)); @@ -302,10 +327,28 @@ fn pcm_to_wav(pcm: &[u8], sample_rate: u32, channels: u16, bits_per_sample: u16) wav } -/// Verfügbare TTS-Stimmen — bei Piper: Modell-basiert +/// Verfügbare TTS-Stimmen — prüft welche Modelle lokal vorhanden sind #[tauri::command] pub async fn get_tts_voices() -> Result, String> { - Ok(vec![ - serde_json::json!({ "id": "thorsten-high", "name": "Thorsten (Deutsch)", "description": "Lokal, hohe Qualität, männlich" }), - ]) + let voices_meta = vec![ + ("kerstin", "Kerstin (Deutsch)", "Weiblich, lokal, Standard"), + ("thorsten-high", "Thorsten HQ (Deutsch)", "Männlich, hohe Qualität, lokal"), + ("thorsten", "Thorsten (Deutsch)", "Männlich, mittlere Qualität, lokal"), + ("eva", "Eva (Deutsch)", "Weiblich, lokal"), + ("ramona", "Ramona (Deutsch)", "Weiblich, lokal"), + ]; + + let mut result = Vec::new(); + for (id, name, desc) in voices_meta { + let model_path = piper_model_for_voice(Some(id)); + let available = std::path::Path::new(&model_path).exists(); + result.push(serde_json::json!({ + "id": id, + "name": name, + "description": desc, + "available": available, + "default": id == DEFAULT_VOICE, + })); + } + Ok(result) }