Compare commits
2 commits
a3d1dadc46
...
b9dfc8a720
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b9dfc8a720 | ||
|
|
05bc35208d |
3 changed files with 86 additions and 32 deletions
|
|
@ -717,7 +717,13 @@ 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 {
|
||||||
format!("{}...", &content[..200])
|
// Sicher an Char-Boundary abschneiden (Multi-Byte wie ─, ä, ü)
|
||||||
|
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()
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -993,9 +993,14 @@ 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
|
// Content auf ~300 Zeichen kürzen (sicher an Char-Boundary)
|
||||||
let content = if entry.content.len() > 300 {
|
let content = if entry.content.len() > 300 {
|
||||||
format!("{}...", &entry.content[..300])
|
let end = entry.content.char_indices()
|
||||||
|
.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
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
fn piper_model_path() -> String {
|
||||||
std::env::var("PIPER_MODEL")
|
piper_model_for_voice(None)
|
||||||
.unwrap_or_else(|_| {
|
}
|
||||||
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()
|
/// Modell-Pfad für eine bestimmte Stimme (oder Default)
|
||||||
.map(std::path::PathBuf::from);
|
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![
|
let voice_id = voice.unwrap_or(DEFAULT_VOICE);
|
||||||
// Relativ zum Binary (Dev-Modus)
|
let filename = PIPER_VOICES.iter()
|
||||||
exe_dir.as_ref().map(|d| d.join("../models/de_DE-thorsten-high.onnx")),
|
.find(|(id, _)| *id == voice_id)
|
||||||
exe_dir.as_ref().map(|d| d.join("models/de_DE-thorsten-high.onnx")),
|
.map(|(_, f)| *f)
|
||||||
// XDG Data Home / Home-Verzeichnis (AppImage + Nix-Wrapper)
|
.unwrap_or(PIPER_VOICES[0].1); // Fallback auf Kerstin
|
||||||
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")),
|
|
||||||
];
|
|
||||||
|
|
||||||
candidates.into_iter()
|
let exe_dir = std::env::current_exe()
|
||||||
.flatten()
|
.ok()
|
||||||
.find(|p| p.exists())
|
.and_then(|p| p.parent().map(|p| p.to_path_buf()));
|
||||||
.map(|p| p.to_string_lossy().to_string())
|
|
||||||
.unwrap_or_else(|| "models/de_DE-thorsten-high.onnx".to_string())
|
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
|
/// Voice-System Status
|
||||||
|
|
@ -227,9 +252,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>, // Wird für Piper ignoriert (Modell bestimmt Stimme)
|
voice: Option<String>,
|
||||||
) -> Result<String, String> {
|
) -> Result<String, String> {
|
||||||
let model = piper_model_path();
|
let model = piper_model_for_voice(voice.as_deref());
|
||||||
|
|
||||||
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));
|
||||||
|
|
@ -302,10 +327,28 @@ fn pcm_to_wav(pcm: &[u8], sample_rate: u32, channels: u16, bits_per_sample: u16)
|
||||||
wav
|
wav
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verfügbare TTS-Stimmen — bei Piper: Modell-basiert
|
/// Verfügbare TTS-Stimmen — prüft welche Modelle lokal vorhanden sind
|
||||||
#[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> {
|
||||||
Ok(vec![
|
let voices_meta = vec![
|
||||||
serde_json::json!({ "id": "thorsten-high", "name": "Thorsten (Deutsch)", "description": "Lokal, hohe Qualität, männlich" }),
|
("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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue