Compare commits

..

No commits in common. "b9dfc8a720e0bc94fbf29809dcc0aadb2a1967f3" and "a3d1dadc46dc33b4ae8a30ca3d46d6435263549b" have entirely different histories.

3 changed files with 32 additions and 86 deletions

View file

@ -717,13 +717,7 @@ impl Database {
let mut summary_parts: Vec<String> = Vec::new(); let mut summary_parts: Vec<String> = Vec::new();
for (_, role, content) in &old_messages { for (_, role, content) in &old_messages {
let preview = if content.len() > 200 { let preview = if content.len() > 200 {
// Sicher an Char-Boundary abschneiden (Multi-Byte wie ─, ä, ü) format!("{}...", &content[..200])
let end = content.char_indices()
.take_while(|(i, _)| *i < 200)
.last()
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(200.min(content.len()));
format!("{}...", &content[..end])
} else { } else {
content.clone() content.clone()
}; };

View file

@ -993,14 +993,9 @@ pub async fn format_tool_hints(
for entry in entries { for entry in entries {
hints.push(format!("\n**{}** ({})", entry.title, entry.category)); hints.push(format!("\n**{}** ({})", entry.title, entry.category));
// Content auf ~300 Zeichen kürzen (sicher an Char-Boundary) // Content auf ~300 Zeichen kürzen
let content = if entry.content.len() > 300 { let content = if entry.content.len() > 300 {
let end = entry.content.char_indices() format!("{}...", &entry.content[..300])
.take_while(|(i, _)| *i < 300)
.last()
.map(|(i, c)| i + c.len_utf8())
.unwrap_or(300.min(entry.content.len()));
format!("{}...", &entry.content[..end])
} else { } else {
entry.content entry.content
}; };

View file

@ -49,35 +49,9 @@ 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 { fn piper_model_path() -> String {
piper_model_for_voice(None) std::env::var("PIPER_MODEL")
} .unwrap_or_else(|_| {
/// 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 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
let exe_dir = std::env::current_exe() let exe_dir = std::env::current_exe()
.ok() .ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf())); .and_then(|p| p.parent().map(|p| p.to_path_buf()));
@ -87,20 +61,21 @@ fn piper_model_for_voice(voice: Option<&str>) -> String {
let candidates = vec![ let candidates = vec![
// Relativ zum Binary (Dev-Modus) // 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/de_DE-thorsten-high.onnx")),
exe_dir.as_ref().map(|d| d.join("models").join(filename)), exe_dir.as_ref().map(|d| d.join("models/de_DE-thorsten-high.onnx")),
// XDG Data Home / Home-Verzeichnis (AppImage + Nix-Wrapper) // 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(".local/share/claude-desktop/models/de_DE-thorsten-high.onnx")),
home_dir.as_ref().map(|d| d.join(".claude-desktop/models").join(filename)), home_dir.as_ref().map(|d| d.join(".claude-desktop/models/de_DE-thorsten-high.onnx")),
// CWD Fallback // CWD Fallback
Some(std::path::PathBuf::from("models").join(filename)), Some(std::path::PathBuf::from("models/de_DE-thorsten-high.onnx")),
]; ];
candidates.into_iter() candidates.into_iter()
.flatten() .flatten()
.find(|p| p.exists()) .find(|p| p.exists())
.map(|p| p.to_string_lossy().to_string()) .map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| format!("models/{}", filename)) .unwrap_or_else(|| "models/de_DE-thorsten-high.onnx".to_string())
})
} }
/// Voice-System Status /// Voice-System Status
@ -252,9 +227,9 @@ pub async fn transcribe_audio(
#[tauri::command] #[tauri::command]
pub async fn text_to_speech( pub async fn text_to_speech(
text: String, text: String,
voice: Option<String>, _voice: Option<String>, // Wird für Piper ignoriert (Modell bestimmt Stimme)
) -> Result<String, String> { ) -> Result<String, String> {
let model = piper_model_for_voice(voice.as_deref()); let model = piper_model_path();
if !std::path::Path::new(&model).exists() { if !std::path::Path::new(&model).exists() {
return Err(format!("Piper-Modell nicht gefunden: {}", model)); return Err(format!("Piper-Modell nicht gefunden: {}", model));
@ -327,28 +302,10 @@ fn pcm_to_wav(pcm: &[u8], sample_rate: u32, channels: u16, bits_per_sample: u16)
wav wav
} }
/// Verfügbare TTS-Stimmen — prüft welche Modelle lokal vorhanden sind /// Verfügbare TTS-Stimmen — bei Piper: Modell-basiert
#[tauri::command] #[tauri::command]
pub async fn get_tts_voices() -> Result<Vec<serde_json::Value>, String> { pub async fn get_tts_voices() -> Result<Vec<serde_json::Value>, String> {
let voices_meta = vec![ Ok(vec![
("kerstin", "Kerstin (Deutsch)", "Weiblich, lokal, Standard"), serde_json::json!({ "id": "thorsten-high", "name": "Thorsten (Deutsch)", "description": "Lokal, hohe Qualität, männlich" }),
("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)
} }