Jamulix: Optimierungen übernehmen, lokale Voice-Modelle ignorieren
This commit is contained in:
parent
e99508a727
commit
91b9ef3374
12 changed files with 555 additions and 94 deletions
117
app/web/app.js
117
app/web/app.js
|
|
@ -20,6 +20,29 @@ function langOverrides() {
|
|||
}
|
||||
|
||||
// Nutzer-Präferenzen als Request-Overrides für jeden Turn.
|
||||
// Geschlechtspräferenz: 'm', 'f', oder 'any' (= weiblich bevorzugt)
|
||||
let _voiceGender = 'f';
|
||||
|
||||
function applyGenderChoice(gender) {
|
||||
// 'any' aus alten Prefs → weiblich als Standardwert
|
||||
_voiceGender = (gender === 'm') ? 'm' : 'f';
|
||||
const btnM = document.getElementById('gender-m');
|
||||
const btnF = document.getElementById('gender-f');
|
||||
if (!btnM || !btnF) return;
|
||||
const mActive = (_voiceGender === 'm');
|
||||
const fActive = (_voiceGender === 'f');
|
||||
// Aktiv: blauer Hintergrund + weiße Schrift
|
||||
// Inaktiv: kein Hintergrund + dunkle Schrift (explizit, damit kein Vererbungsproblem)
|
||||
btnM.classList.toggle('bg-blue-600', mActive);
|
||||
btnM.classList.toggle('text-white', mActive);
|
||||
btnM.classList.toggle('border-blue-600', mActive);
|
||||
btnM.classList.toggle('text-slate-700', !mActive);
|
||||
btnF.classList.toggle('bg-blue-600', fActive);
|
||||
btnF.classList.toggle('text-white', fActive);
|
||||
btnF.classList.toggle('border-blue-600', fActive);
|
||||
btnF.classList.toggle('text-slate-700', !fActive);
|
||||
}
|
||||
|
||||
function overrides() {
|
||||
const out = { ...langOverrides() };
|
||||
if (isDeviceMode()) {
|
||||
|
|
@ -27,6 +50,7 @@ function overrides() {
|
|||
} else if (ttsSel && ttsSel.value) {
|
||||
out.tts_provider = ttsSel.value;
|
||||
}
|
||||
out.voice_gender = _voiceGender;
|
||||
return out;
|
||||
}
|
||||
|
||||
|
|
@ -45,7 +69,7 @@ async function savePrefs(obj) {
|
|||
const TTS_SUPPORTED = typeof window !== "undefined" && "speechSynthesis" in window;
|
||||
const LANG_BCP47 = {
|
||||
de: "de-DE", en: "en-US", fr: "fr-FR", es: "es-ES",
|
||||
it: "it-IT", nl: "nl-NL", ru: "ru-RU", zh: "zh-CN",
|
||||
it: "it-IT", pt: "pt-BR", pl: "pl-PL", ar: "ar-SA", ru: "ru-RU", zh: "zh-CN",
|
||||
};
|
||||
|
||||
function isMobile() {
|
||||
|
|
@ -179,6 +203,69 @@ function refreshDevicePreset() {
|
|||
syncPresetUI();
|
||||
}
|
||||
|
||||
// Graut "Hohe Qualität" (Chatterbox) aus, wenn kein GPU-Backend erreichbar ist.
|
||||
async function refreshChatterboxPreset() {
|
||||
const btn = document.querySelector('.tts-preset[data-tts="chatterbox"]');
|
||||
if (!btn || !ttsSel) return;
|
||||
try {
|
||||
const cfg = await fetch("/api/config", { cache: "no-store" }).then(r => r.json());
|
||||
const available = cfg.available?.chatterbox_reachable === true;
|
||||
btn.disabled = !available;
|
||||
btn.classList.toggle("opacity-50", !available);
|
||||
btn.classList.toggle("cursor-not-allowed", !available);
|
||||
const sub = btn.querySelector(".tts-sub");
|
||||
if (sub) sub.textContent = available
|
||||
? "Chatterbox · natürliche Stimme"
|
||||
: "Chatterbox · kein GPU-Backend erreichbar";
|
||||
if (!available && ttsSel.value === "chatterbox") {
|
||||
ttsSel.value = "piper";
|
||||
syncPresetUI();
|
||||
}
|
||||
} catch (e) { /* ignorieren */ }
|
||||
}
|
||||
|
||||
// Graut "Cartesia" aus, wenn API-Key oder Voice-ID fehlt.
|
||||
async function refreshCartesiaPreset() {
|
||||
const btn = document.querySelector('.tts-preset[data-tts="cartesia"]');
|
||||
if (!btn || !ttsSel) return;
|
||||
try {
|
||||
const cfg = await fetch("/api/config", { cache: "no-store" }).then(r => r.json());
|
||||
const available = cfg.available?.cartesia_available === true;
|
||||
btn.disabled = !available;
|
||||
btn.classList.toggle("opacity-50", !available);
|
||||
btn.classList.toggle("cursor-not-allowed", !available);
|
||||
const sub = btn.querySelector(".tts-sub");
|
||||
if (sub) sub.textContent = available
|
||||
? "Cartesia Sonic · natürlich & schnell"
|
||||
: "Cartesia · kein API-Key konfiguriert";
|
||||
if (!available && ttsSel.value === "cartesia") {
|
||||
ttsSel.value = "piper";
|
||||
syncPresetUI();
|
||||
}
|
||||
} catch (e) { /* ignorieren */ }
|
||||
}
|
||||
|
||||
// Graut "Cloud" (OpenRouter TTS) aus, wenn das TTS-Modell nicht verfügbar ist.
|
||||
async function refreshOpenrouterTtsPreset() {
|
||||
const btn = document.querySelector('.tts-preset[data-tts="openrouter"]');
|
||||
if (!btn || !ttsSel) return;
|
||||
try {
|
||||
const cfg = await fetch("/api/config", { cache: "no-store" }).then(r => r.json());
|
||||
const available = cfg.available?.openrouter_tts_available === true;
|
||||
btn.disabled = !available;
|
||||
btn.classList.toggle("opacity-50", !available);
|
||||
btn.classList.toggle("cursor-not-allowed", !available);
|
||||
const sub = btn.querySelector(".tts-sub");
|
||||
if (sub) sub.textContent = available
|
||||
? "OpenRouter · benötigt Internet"
|
||||
: "OpenRouter · kein TTS-Modell verfügbar";
|
||||
if (!available && ttsSel.value === "openrouter") {
|
||||
ttsSel.value = "piper";
|
||||
syncPresetUI();
|
||||
}
|
||||
} catch (e) { /* ignorieren */ }
|
||||
}
|
||||
|
||||
// Markiert das aktive Ton-Preset (Häkchen + Rahmen) passend zu #tts.
|
||||
function syncPresetUI() {
|
||||
const val = ttsSel ? ttsSel.value : "";
|
||||
|
|
@ -241,9 +328,10 @@ function loadMe() {
|
|||
// Flex-Modus -> "flex", sonst die feste Sprache (Fallback: Flex).
|
||||
langSel.value = prefs.language_mode === "flex"
|
||||
? "flex"
|
||||
: (prefs.language || "flex");
|
||||
: (prefs.language || "de");
|
||||
}
|
||||
applyPlaybackChoice(prefs.tts_provider);
|
||||
applyGenderChoice(prefs.voice_gender || 'f');
|
||||
return me;
|
||||
} catch (e) {
|
||||
$("#identity").textContent = "Verbindung fehlgeschlagen";
|
||||
|
|
@ -291,6 +379,7 @@ function addMessage(role, text, meta = {}) {
|
|||
div.className = BUBBLE[role] || BUBBLE.assistant;
|
||||
const span = document.createElement("span");
|
||||
span.className = "bubble-text";
|
||||
span.dir = "auto";
|
||||
span.textContent = text;
|
||||
div.appendChild(span);
|
||||
div._textEl = span;
|
||||
|
|
@ -684,7 +773,25 @@ if (ttsSel) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
// Gender-Buttons
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
const btnM = document.getElementById("gender-m");
|
||||
const btnF = document.getElementById("gender-f");
|
||||
if (btnM) btnM.addEventListener("click", () => {
|
||||
applyGenderChoice("m");
|
||||
savePrefs({ voice_gender: "m" });
|
||||
});
|
||||
if (btnF) btnF.addEventListener("click", () => {
|
||||
applyGenderChoice("f");
|
||||
savePrefs({ voice_gender: "f" });
|
||||
});
|
||||
});
|
||||
|
||||
loadMe();
|
||||
refreshChatterboxPreset();
|
||||
refreshCartesiaPreset();
|
||||
refreshOpenrouterTtsPreset();
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════
|
||||
// ADMIN PANEL
|
||||
|
|
@ -1570,12 +1677,12 @@ const FIELD_META = {
|
|||
default_stt_provider: { ui: "combo", opts: ["openrouter","faster-whisper"], test: "stt" },
|
||||
default_llm_provider: { ui: "combo", opts: ["openrouter","local-openai-compatible"], test: "llm" },
|
||||
default_tts_provider: { ui: "combo", opts: ["openrouter","piper","chatterbox"], test: "tts" },
|
||||
default_language: { ui: "combo", opts: ["de","en","fr","es","it","nl"],
|
||||
labels: { de:"Deutsch",en:"Englisch",fr:"Französisch",es:"Spanisch",it:"Italienisch",nl:"Niederländisch" } },
|
||||
default_language: { ui: "combo", opts: ["de","en","fr","es","it","pt","pl","ar"],
|
||||
labels: { de:"Deutsch",en:"Englisch",fr:"Französisch",es:"Spanisch",it:"Italienisch",pt:"Portugiesisch",pl:"Polnisch",ar:"Arabisch" } },
|
||||
openrouter_llm_model: { ui: "combo", opts: ["google/gemini-3.1-flash-lite","openai/gpt-4.1-mini","google/gemini-2.5-flash","anthropic/claude-haiku-4-5-20251001","meta-llama/llama-3.3-70b-instruct"], test: "llm" },
|
||||
openrouter_tts_model: { ui: "combo", opts: ["google/gemini-3.1-flash-tts-preview","openai/gpt-4o-mini-tts"], test: "tts", testProvider: "openrouter" },
|
||||
openrouter_tts_voice: { ui: "combo", opts: ["Zephyr","Puck","Charon","Kore","Fenrir","Leda","Orus","Aoede","Callirrhoe","Enceladus","Algieba","Despina","Achernar"], test: "tts", testProvider: "openrouter" },
|
||||
piper_voice: { ui: "combo", opts: ["de_DE-thorsten-high","en_US-ryan-high","en_US-lessac-high","en_GB-cori-high","es_ES-sharvard-medium","fr_FR-siwis-medium","it_IT-paola-medium","nl_NL-mls-medium","ru_RU-irina-medium","zh_CN-huayan-medium"], test: "tts", testProvider: "piper" },
|
||||
piper_voice: { ui: "combo", opts: ["de_DE-thorsten-high","de_DE-kerstin-low","en_US-lessac-high","en_US-amy-medium","en_US-ryan-high","en_GB-cori-high","fr_FR-siwis-medium","fr_FR-tom-medium","es_ES-sharvard-medium","it_IT-paola-medium","it_IT-riccardo-x_low","pt_BR-faber-medium","pl_PL-gosia-medium","pl_PL-darkman-medium","ar_JO-kareem-medium","ru_RU-irina-medium","ru_RU-ruslan-medium","zh_CN-huayan-medium"], test: "tts", testProvider: "piper" },
|
||||
local_llm_system_prompt: { ui: "textarea", test: "llm" },
|
||||
local_llm_temperature: { ui: "range", min: 0, max: 2, step: 0.1, test: "llm" },
|
||||
local_llm_top_p: { ui: "range", min: 0, max: 1, step: 0.05, test: "llm" },
|
||||
|
|
|
|||
|
|
@ -163,7 +163,9 @@
|
|||
<option value="fr">Französisch (fr)</option>
|
||||
<option value="es">Spanisch (es)</option>
|
||||
<option value="it">Italienisch (it)</option>
|
||||
<option value="nl">Niederländisch (nl)</option>
|
||||
<option value="pt">Portugiesisch (pt)</option>
|
||||
<option value="pl">Polnisch (pl)</option>
|
||||
<option value="ar">Arabisch (ar)</option>
|
||||
<option value="ru">Russisch (ru)</option>
|
||||
<option value="zh">Chinesisch (zh)</option>
|
||||
</select>
|
||||
|
|
@ -225,7 +227,9 @@
|
|||
<option value="fr">🇫🇷 FR</option>
|
||||
<option value="es">🇪🇸 ES</option>
|
||||
<option value="it">🇮🇹 IT</option>
|
||||
<option value="nl">🇳🇱 NL</option>
|
||||
<option value="pt">🇧🇷 PT</option>
|
||||
<option value="pl">🇵🇱 PL</option>
|
||||
<option value="ar">🇸🇦 AR</option>
|
||||
<option value="ru">🇷🇺 RU</option>
|
||||
<option value="zh">🇨🇳 ZH</option>
|
||||
</select>
|
||||
|
|
@ -244,6 +248,7 @@
|
|||
<option value="device">Im Gerät</option>
|
||||
<option value="piper">Server</option>
|
||||
<option value="chatterbox">Beste Qualität</option>
|
||||
<option value="cartesia">Cartesia</option>
|
||||
<option value="openrouter">Cloud</option>
|
||||
</select>
|
||||
|
||||
|
|
@ -278,6 +283,13 @@
|
|||
<span class="block text-xs text-slate-500 dark:text-slate-400">Chatterbox · natürliche Stimme</span></span>
|
||||
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0">✓</span>
|
||||
</button>
|
||||
<button type="button" data-tts="cartesia"
|
||||
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<span class="text-lg shrink-0">🎙</span>
|
||||
<span class="flex-1 min-w-0"><span class="block text-sm font-medium">Cartesia</span>
|
||||
<span class="tts-sub block text-xs text-slate-500 dark:text-slate-400">Cartesia Sonic · natürlich & schnell</span></span>
|
||||
<span class="check text-blue-600 dark:text-blue-400 font-bold opacity-0">✓</span>
|
||||
</button>
|
||||
<button type="button" data-tts="openrouter"
|
||||
class="tts-preset w-full flex items-center gap-3 rounded-lg border border-slate-200 dark:border-slate-700 px-3 py-2 text-left hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors">
|
||||
<span class="text-lg shrink-0">☁</span>
|
||||
|
|
@ -288,6 +300,22 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Geschlechtspräferenz (wirkt bei Cartesia) -->
|
||||
<div id="gender-pref-row">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400 mb-1.5 px-1">Stimme</p>
|
||||
<div class="flex gap-2">
|
||||
<button type="button" id="gender-m"
|
||||
class="gender-btn flex-1 rounded-lg border border-slate-200 dark:border-slate-700 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:hover:bg-slate-700/50">
|
||||
♂ Männlich
|
||||
</button>
|
||||
<button type="button" id="gender-f"
|
||||
class="gender-btn flex-1 rounded-lg border border-slate-200 dark:border-slate-700 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-50 dark:hover:bg-slate-700/50">
|
||||
♀ Weiblich
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Aktionen -->
|
||||
<div class="border-t border-slate-100 dark:border-slate-700 pt-2 space-y-1">
|
||||
<button id="new-chat" type="button"
|
||||
|
|
@ -314,7 +342,7 @@
|
|||
<form id="prompt-form" class="flex items-center gap-2 w-full max-w-3xl mx-auto">
|
||||
<button type="button" id="mic" title="Mikrofon" aria-label="Mikrofon"
|
||||
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-emerald-600 text-white text-lg">🎤</button>
|
||||
<input id="prompt" type="text" autocomplete="off" placeholder="Nachricht …"
|
||||
<input id="prompt" type="text" autocomplete="off" dir="auto" placeholder="Nachricht …"
|
||||
class="flex-1 min-w-0 rounded-xl border border-slate-300 dark:border-slate-600 bg-transparent px-4 py-2.5 focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
<button type="submit" id="send" title="Senden" aria-label="Senden"
|
||||
class="shrink-0 h-11 w-11 grid place-items-center rounded-full bg-blue-600 hover:bg-blue-700 text-white transition-colors">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue