diff --git a/CHANGELOG.md b/CHANGELOG.md index d51bfaf..9e60377 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.20.8] — 2026-08-12 + +### Added +- **Fish Audio inline emotion-tag reference in Try It Out.** Selecting a Fish backend reveals a panel with all 49 documented tags (24 basic + 25 advanced); click any tag to insert it at the cursor, plus an "Insert example" button that loads a multi-emotion sample. Shown only for Fish, since it is the only engine that reads bracket tags from the text itself — other engines would speak "[happy]" aloud as literal words. + +### Fixed +- **Fish-Speech showed "Fetched 0 voices" and was unusable from the UI.** It clones from the app's own saved WAV library, but was missing from the set of backends populated with those voices, so the code probed the Fish server for a voice-listing endpoint it does not have and got nothing. Now reports the full library (218 voices). +- **Renamed the backend to "Fish Audio S2 Pro (Clone)"** — verified the running checkpoint is `fishaudio/s2-pro` at revision `1de9996b`, matching upstream HuggingFace exactly. +- **Corrected Fish's advertised capabilities.** It was listed as `style_aware: true` with "Strong: emotion/tone markers … are honoured per request" — measured to be wrong while cloning. With a reference clip, S2-Pro prepends it as an in-context example and imitates its prosody, which overwhelms inline tags: identical text showed a 10x loudness spread across emotions with no reference, but only 1.2x with one ([screaming] vs [whisper] was inaudible). Every lever was tested — canonical tags, free-form descriptions, repeated and mid-sentence tags, temperature to 1.0, truncated references, `reference_id` vs inline. The backend is now billed on what it does well: deterministic, byte-reproducible cloning. + ## [1.20.6] — 2026-08-11 ### Fixed diff --git a/VERSION b/VERSION index e63679c..95393fc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.6 +1.20.8 diff --git a/routes/tts.py b/routes/tts.py index b1b0f38..eaf419a 100644 --- a/routes/tts.py +++ b/routes/tts.py @@ -80,7 +80,7 @@ def _backend_display_name(backend: str, url: str) -> str: "kokoro": "Kokoro FastAPI (82M)", "vibevoice": "VibeVoice TTS", "xtts": "XTTS v2", - "fishspeech": "Fish-Speech (Clone + Emotion)", + "fishspeech": "Fish Audio S2 Pro (Clone)", } port = _backend_port_label(url) return f"{port} {names.get(backend, backend)}" if port else names.get(backend, backend) @@ -168,12 +168,22 @@ def _backend_capabilities(backend: str) -> dict: "uses_wav": False, "style_aware": False, "true_streaming": False, "speed": "~0.3× GPU", "latency": "1–3 s", "quality": "High", "ram": "~5 GB", }, + # Measured live against this exact checkpoint (fishaudio/s2-pro, rev 1de9996b): + # inline [tag] emotion markers work well ONLY when no reference audio is sent. + # With a reference (i.e. whenever cloning a library voice) S2-Pro prepends the + # clip as an in-context example and imitates its prosody, which overwhelms the + # tags: identical text scored a 10× loudness spread across emotions with no + # reference, but only 1.2× with one — [screaming] vs [whisper] was inaudible. + # Every lever was tried (canonical tags, free-form descriptions, repeated and + # mid-sentence tags, temperature up to 1.0, 3s truncated references, + # reference_id vs inline). So this backend is billed on what it actually does + # well: rock-solid deterministic cloning, not per-line emotion. "fishspeech": { - "purpose": "Clone a saved WAV voice AND control per-line tone via inline emotion markers.", - "identity": "Clones the selected voice's reference WAV — consistent speaker identity across lines.", - "style": "Strong: emotion/tone markers like (angry), (whispering), (excited) are honoured per request.", - "best_for": "Consistent character voices that still react to tone changes — ideal for the Rehearser.", - "uses_wav": True, "style_aware": True, "true_streaming": False, + "purpose": "Clone a saved WAV voice with byte-identical, reproducible output.", + "identity": "Excellent — clones the selected voice's reference WAV and is fully deterministic (same input always yields the same audio).", + "style": "Weak while cloning: inline [tag] markers are overwhelmed by the reference clip's own delivery. Tone follows the reference, so the emotion of the reference recording is what you hear.", + "best_for": "Consistent character voices and resumable bulk exports, where identical re-runs matter more than per-line emotion.", + "uses_wav": True, "style_aware": False, "true_streaming": False, "speed": "~0.4× GPU", "latency": "1–4 s", "quality": "Premium", "ram": "~4 GB", }, } @@ -369,7 +379,12 @@ def _active_library_voice_options(settings: dict) -> list[dict]: def _fetch_backend_voices(settings: dict, backend: str) -> list: backend = _clean_preview_backend(backend) - if backend in {"voice_clone", "streaming", "nvidia_zeroshot", "nvidia_flow"}: + # Fish-Speech clones from the app's own saved WAV library (see + # _fishspeech_request_audio, which resolves the voice via _find_voice_audio) and + # exposes no voice-listing endpoint of its own. Without it in this set the + # probe below found nothing and reported voice_count 0, so the Engines page + # listed it with no selectable voices even while the server was healthy. + if backend in {"voice_clone", "streaming", "nvidia_zeroshot", "nvidia_flow", "fishspeech"}: return _active_library_voice_options(settings) tts_url = _validate_http_url(_preview_backend_base_url(settings, backend), allow_private=True).rstrip("/") key = (settings.get("voice_design_api_key") if backend == "voice_design" else settings.get("tts_api_key")) or "" diff --git a/static/dist/main.min.js b/static/dist/main.min.js index e2202aa..e0eddd4 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -738,7 +738,17 @@ This warms each voice so the engine caches its .pt and first playback is instant - `;const close=()=>{ov.remove(),document.removeEventListener("keydown",onKey)};function onKey(e){e.key==="Escape"&&close()}ov.addEventListener("click",e=>{e.target===ov&&close()}),ov.querySelector(".vl-bdc-cancel").addEventListener("click",close),document.addEventListener("keydown",onKey),ov.querySelector(".vl-bdc-go").addEventListener("click",async()=>{const goBtn=ov.querySelector(".vl-bdc-go"),cancelBtn=ov.querySelector(".vl-bdc-cancel");goBtn.disabled=cancelBtn.disabled=!0;let done=0,errors=0;await runPool(ids,async id=>{try{(await fetch(`/api/voice/${encodeURIComponent(id)}`,{method:"DELETE"})).ok?done++:errors++}catch{errors++}},5,n=>{goBtn.innerHTML=` Deleting ${n}/${ids.length}\u2026`}),close(),toast(`Deleted ${done} voice${done!==1?"s":""}${errors?` (${errors} errors)`:""}`,errors?"error":"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),document.body.appendChild(ov),ov.querySelector(".vl-bdc-cancel").focus()}async function _bulkSetEnabled(ids,enabled){let done=0;for(const id of ids)await saveMeta(id,{enabled}).catch(()=>{}),done++;return done}function backendVoiceId(value){return typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||JSON.stringify(value)}function shouldFilterBackendVoices(backend){return["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend||"")}function shouldReplaceWithLibraryVoices(backend){return backend==="voice_design"}async function activeLibraryVoiceIds(){return _voices.length||await loadVoiceLibrary(),new Set((_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id))}function cleanReferenceText(text){return String(text||"").trim()}function selectedPreviewLibraryVoice(){var _a2;const id=((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"";return id?(_voices||[]).find(v=>v.id===id):null}function previewVoiceWarnings(v){var _a2;const warnings=[],backend=backendById(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"");backend&&backend.id&&!["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend.id)&&warnings.push(backend.id==="nvidia_magpie"?"NVIDIA Magpie uses fixed speaker voices, not saved WAV clone identity.":"This backend may follow style/model voice more than the saved WAV identity."),backend&&backend.id==="nvidia_zeroshot"&&v.duration&&(Number(v.duration)<3||Number(v.duration)>10)&&warnings.push("NVIDIA Zeroshot works best with a clear 3-10 second prompt."),backend&&backend.id==="nvidia_flow"&&!v.transcript&&warnings.push("NVIDIA Flow requires the exact saved reference transcript for this voice."),v.transcript||warnings.push("No reference transcript is saved; cloned identity is harder to judge."),v.duration&&(Number(v.duration)<3||Number(v.duration)>20)&&warnings.push("Reference clip length is outside the 3-20 second sweet spot."),v.needs_tts_restart&&warnings.push("This voice changed since the last backend refresh; restart or clear restart flags before judging it.");const healthWarnings=v.health&&Array.isArray(v.health.warnings)?v.health.warnings:[];return warnings.push(...healthWarnings.slice(0,3)),warnings}function updatePreviewVoiceMatchPanel(){const panel=$("preview-match-panel");if(!panel)return;const v=selectedPreviewLibraryVoice();if(!v){panel.hidden=!0;return}panel.hidden=!1;const lang=v.language||v.lang||(v.id||"").split("_")[0]||"-",gender=v.gender||(v.id||"").split("_")[1]||"-",db=fmtDbfs(v),dur=v.duration?fmtDuration(v.duration):"-";$("preview-match-title").textContent=v.id,$("preview-match-detail").textContent=`${lang} \xB7 ${gender} \xB7 ${dur} \xB7 ${db} dBFS`;const warnings=previewVoiceWarnings(v);$("preview-match-warning").textContent=warnings.length?warnings.join(" "):"For a fair voice match check, play the WAV and synthesize the exact saved reference text.";const transcript=cleanReferenceText(v.transcript||"");$("preview-match-transcript").textContent=transcript||"No reference text saved for this voice.",$("preview-ref-use-text").disabled=!transcript,$("preview-ref-synth").disabled=!transcript;const audio=$("preview-ref-audio"),expected=voiceFileUrl(v);audio.dataset.src!==expected&&(audio.pause(),audio.src=expected,audio.dataset.src=expected);const actionsEl=panel.querySelector(".preview-match-actions");let personaBtn=panel.querySelector(".preview-persona-btn");v.persona?personaBtn||(personaBtn=document.createElement("button"),personaBtn.className="btn-secondary preview-persona-btn",personaBtn.type="button",personaBtn.textContent="Rewrite with persona",actionsEl==null||actionsEl.appendChild(personaBtn),personaBtn.addEventListener("click",async()=>{const text=$("preview-text-area").value.trim();if(!text){toast("Enter text to rewrite","error");return}const lv=selectedPreviewLibraryVoice();if(!(lv!=null&&lv.persona)){toast("This voice has no persona","error");return}personaBtn.disabled=!0,personaBtn.textContent="Rewriting\u2026";try{const llmUrl=localStorage.getItem("refine-llm-url")||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",r=await fetch("/api/rewrite-with-persona",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,persona:lv.persona,llm_url:llmUrl,model:(_appSettings==null?void 0:_appSettings.llm_model)||"",mode:"rewrite"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("preview-text-area").value=d.text,toast("Text rewritten in persona style","success")}catch(e){toast("Persona rewrite failed: "+e.message,"error")}finally{personaBtn.disabled=!1,personaBtn.textContent="Rewrite with persona"}})):personaBtn==null||personaBtn.remove();const personaToggle=$("preview-persona-toggle");if(personaToggle){const label=personaToggle.closest("label");v.persona?(personaToggle.disabled=!1,label&&(label.title="Rewrite text through this voice's character persona before generating")):(personaToggle.checked=!1,personaToggle.disabled=!0,label&&(label.title="This voice has no character persona saved \u2014 set one on the Voice Inspector page first."))}}async function synthesizeSelectedReferenceText(){const v=selectedPreviewLibraryVoice();if(!v){toast("Select a library voice first","error");return}let text=cleanReferenceText(v.transcript||"");if(!text){toast("This voice has no reference text","error");return}if(v.needs_tts_restart){if(!confirm("This voice is marked as needing a TTS restart. If you already restarted the backend, clear the flag and synthesize anyway?"))return;await clearTtsRestartFlags(),v.needs_tts_restart=!1,updatePreviewVoiceMatchPanel()}const backend=$("tts-backend-select").value;if(!backend){toast("No available TTS backend","error");return}const btn=$("preview-ref-synth");btn.disabled=!0;try{$("preview-text-area").value=text;const source=await createTtsAudioSource(v.id,text,backend,$("preview-playback-mode").value,$("preview-style-instruction").value.trim());previewBlob=source.blob;const audio=$("preview-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,toast(source.streaming?"Reference text streaming":"Reference text synthesized","success")}catch(e){toast("Reference synthesis failed: "+e.message,"error")}finally{btn.disabled=!1}}$("fetch-tts-voices-btn").addEventListener("click",async()=>{var _a2;$("fetch-tts-voices-btn").disabled=!0;try{const backend=(_a2=$("tts-backend-select"))==null?void 0:_a2.value;if(!backend)throw new Error("No available TTS backend");let ids;if(shouldReplaceWithLibraryVoices(backend))_voices.length||await loadVoiceLibrary(),ids=(_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id);else{const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());let voices=Array.isArray(rawVoices)?rawVoices:[];if(shouldFilterBackendVoices(backend)){const activeIds=await activeLibraryVoiceIds();voices=voices.filter(v=>activeIds.has(backendVoiceId(v)))}ids=voices.map(backendVoiceId)}const sel=$("tts-voice-select"),prev=sel.value;window.VoicePicker?(VoicePicker.upgrade("tts-voice-select"),VoicePicker.populate("tts-voice-select",ids),prev&&ids.includes(prev)&&VoicePicker.setValue("tts-voice-select",prev)):(sel.innerHTML='',ids.forEach(id=>{const o=document.createElement("option");o.value=o.textContent=id,sel.appendChild(o)}),prev&&ids.includes(prev)&&(sel.value=prev)),updatePreviewVoiceMatchPanel();const suffix=shouldFilterBackendVoices(backend)||shouldReplaceWithLibraryVoices(backend)?" active voices":" voices";toast("Fetched "+ids.length+suffix,"success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("fetch-tts-voices-btn").disabled=!1}});function _ttsIsFishBackend(id){return/fish/i.test(id||"")}function _ttsApplyEmotionTag(text,emotionValue){var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"";if(!_ttsIsFishBackend(backend)||!emotionValue||/^\s*\[/.test(text))return text;const tag=typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(emotionValue):"";return tag?`[${tag}] ${text}`:text}setTimeout(function(){const sel=$("preview-emotion-select");!sel||typeof window.REH_EMOTIONS=="undefined"||(window.REH_EMOTIONS.forEach(e=>{if(!e.value)return;const o=document.createElement("option");o.value=e.value,o.textContent=`${e.emoji} ${e.label}`,sel.appendChild(o)}),sel.addEventListener("change",()=>{var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"",help=$("preview-emotion-help");if(_ttsIsFishBackend(backend))help&&(help.style.display=sel.value?"block":"none",help.textContent=sel.value?"Applied as an inline [tag] in the text for Fish-Speech \u2014 the style instruction field below is ignored by this backend.":"");else{help&&(help.style.display="none");const styleInput=$("preview-style-instruction");styleInput&&sel.value&&(styleInput.value=sel.value)}}))},0),$("tts-backend-select").addEventListener("change",()=>{var _a2;const sel=$("tts-voice-select");sel.innerHTML='',updateBackendHelp(),updatePreviewVoiceMatchPanel(),previewBlob=null,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,(_a2=$("preview-emotion-select"))==null||_a2.dispatchEvent(new Event("change"))}),$("tts-voice-select").addEventListener("change",updatePreviewVoiceMatchPanel),$("preview-ref-play").addEventListener("click",async()=>{updatePreviewVoiceMatchPanel();const audio=$("preview-ref-audio");try{await audio.play()}catch(e){toast("Reference playback failed: "+e.message,"error")}}),$("preview-ref-use-text").addEventListener("click",()=>{const v=selectedPreviewLibraryVoice(),text=cleanReferenceText((v==null?void 0:v.transcript)||"");if(!text){toast("This voice has no reference text","error");return}$("preview-text-area").value=text,toast("Reference text copied to target text","success")}),$("preview-ref-synth").addEventListener("click",synthesizeSelectedReferenceText);let _ttsStreamHealth=null;function effectiveTtsPlaybackMode(override="settings"){return override&&override!=="settings"?override:_appSettings.tts_stream_mode||"auto"}async function isTtsStreamAvailable(force=!1){if(_ttsStreamHealth&&!force)return _ttsStreamHealth.ok;try{return _ttsStreamHealth=await fetch("/api/tts-stream-health").then(r=>r.json()),!!_ttsStreamHealth.ok}catch{return _ttsStreamHealth={ok:!1},!1}}async function createTtsStreamUrl(voice,text,instruct=""){if(!await isTtsStreamAvailable())throw new Error("streaming backend unavailable");const r=await fetch("/api/tts-stream-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,voice,instruct})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return(await r.json()).url}async function _ttsPreviewFetchWithRetry(body,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})}catch(e){if(i===tries)throw e;await new Promise(r=>setTimeout(r,2500*i))}}function _ttsBackendForVoice(voiceId,fallbackBackend){if(!voiceId||voiceId==="me")return fallbackBackend;const v=(window._voices||[]).find(x=>x.id===voiceId);return v&&!v.has_ref?"voice_design":fallbackBackend}async function fetchTtsPreviewBlob(voice,text,responseFormat="wav",instruct="",backend="voice_clone",applyPersona=!1,extra=null){const body={text,voice,response_format:responseFormat,instruct,backend};applyPersona&&(body.apply_persona=!0),extra&&typeof extra=="object"&&Object.assign(body,extra);const r=await _ttsPreviewFetchWithRetry(body);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const blob=await r.blob();if(responseFormat==="wav"){const v=(window._voices||[]).find(x=>x.id===voice),gainDb=v&&v.loudness&&typeof v.loudness.gain_db=="number"?v.loudness.gain_db:0;if(gainDb)try{const nr=await fetch("/api/audio/apply-gain?gain_db="+encodeURIComponent(gainDb),{method:"POST",body:blob});if(nr.ok)return await nr.blob()}catch{}}return blob}function _textWords(s){return String(s||"").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean)}function _textSimilarity(a,b){const wa=_textWords(a),wb=_textWords(b);if(!wa.length&&!wb.length)return 1;if(!wa.length||!wb.length)return 0;const counts=new Map;wa.forEach(w=>counts.set(w,(counts.get(w)||0)+1));let overlap=0;return wb.forEach(w=>{const c=counts.get(w);c&&(overlap++,counts.set(w,c-1))}),2*overlap/(wa.length+wb.length)}async function _voiceRoundtripCheck(voiceId,text,backend,instruct=""){const blob=await fetchTtsPreviewBlob(voiceId,text,"wav",instruct,backend),fd=new FormData;fd.append("file",blob,"roundtrip.wav"),fd.append("backend","configured");const r=await fetch("/api/transcribe-bytes",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return{transcript:d.text||"",score:_textSimilarity(text,d.text||""),blob}}async function createTtsAudioSource(voice,text,backend="voice_clone",modeOverride="settings",instruct="",applyPersona=!1,extra=null){const mode=effectiveTtsPlaybackMode(modeOverride);if(backend!=="streaming"||mode==="buffered"){const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}try{return{url:await createTtsStreamUrl(voice,text,instruct),blob:null,streaming:!0,label:"streaming"}}catch(e){if(mode==="streaming")throw e;const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}}let previewBlob=null;const PREVIEW_SAMPLE_TEXT="Hello! This is a voice preview from TTS Voice Creator - Clone and Design.";$("preview-text-area").addEventListener("focus",()=>{$("preview-text-area").value===PREVIEW_SAMPLE_TEXT&&($("preview-text-area").value="")},{once:!0});function _onPreviewGenerated(source,voice,text,backend,instruct){typeof effectsSourceBlob!="undefined"&&(window._effectsSourceBlob=null),window._effectsSynthArgs={voice,text,instruct:instruct||"",backend};const ea=$("effects-apply-btn");ea&&(ea.disabled=!1);const ap=$("add-to-playlist-btn");ap&&source.blob&&(ap.disabled=!1),typeof historyPush=="function"&&source.blob&&historyPush(voice,text,backend,source.blob,source.url)}const _TRYOUT_SPEED_KEY="ttsvc_tryout_native_speed";(function(){const saved=localStorage.getItem(_TRYOUT_SPEED_KEY);if(saved){const el=$("preview-native-speed");el&&(el.value=saved)}})(),(_R=$("preview-native-speed"))==null||_R.addEventListener("change",function(){localStorage.setItem(_TRYOUT_SPEED_KEY,this.value)}),$("preview-btn").addEventListener("click",async()=>{var _a2,_b2,_c2,_d2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value),applyPersona=((_b2=$("preview-persona-toggle"))==null?void 0:_b2.checked)||!1;if(!backend){toast("No available TTS backend","error");return}if(!voice){toast("Select a TTS voice","error");return}if(!text){toast("Enter preview text","error");return}$("preview-btn").disabled=!0,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,$("add-to-playlist-btn")&&($("add-to-playlist-btn").disabled=!0),$("effects-apply-btn")&&($("effects-apply-btn").disabled=!0);const _nspd=parseFloat((_c2=$("preview-native-speed"))==null?void 0:_c2.value),_extra=!isNaN(_nspd)&&_nspd!==1?{speed:_nspd}:null;try{const audio=$("preview-audio"),source=((_d2=$("preview-chunked-toggle"))==null?void 0:_d2.checked)&&text.length>200&&typeof generateChunkedTts=="function"?await generateChunkedTts(voice,text,backend,instruct,_extra,applyPersona):await createTtsAudioSource(voice,text,backend,$("preview-playback-mode").value,instruct,applyPersona,_extra);previewBlob=source.blob,window._previewVoice=voice,window._previewBackend=backend,window._previewText=text,audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,_onPreviewGenerated(source,voice,text,backend,instruct),toast(source.streaming?"Streaming preview playing":source.label==="chunked"?`Chunked (${text.length} chars) playing`:"Preview playing","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{$("preview-btn").disabled=!1}}),$("save-preview-mp3-btn").addEventListener("click",async()=>{var _a2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value);if(!backend){toast("No available TTS backend","error");return}if(!voice||!text)return;const btn=$("save-preview-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(voice||"preview")+"_preview.mp3",a.click(),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),$("save-preview-btn").addEventListener("click",()=>{if(!previewBlob)return;const a=document.createElement("a");a.href=URL.createObjectURL(previewBlob),a.download=($("tts-voice-select").value||"preview")+"_preview.wav",a.click()});const PERF_HISTORY_KEY="vcf-perf-history",PERF_HISTORY_MAX=50,PERF_HISTORY_SORT={key:"ts",dir:"desc"};function perfHistoryLoad(){try{return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY)||"[]")}catch{return[]}}function perfHistorySave(entries){try{localStorage.setItem(PERF_HISTORY_KEY,JSON.stringify(entries.slice(-PERF_HISTORY_MAX)))}catch{}}function perfHistoryAdd(entry){const h=perfHistoryLoad();h.push(entry),perfHistorySave(h)}function perfSparklineSvg(rtfValues){if(!rtfValues.length)return"";const W=120,H=32,PAD=2,barW=Math.max(4,Math.floor((W-PAD*2)/rtfValues.length)-1),maxV=Math.max(...rtfValues,1),bars=rtfValues.map((v,i)=>{const bh=Math.max(3,Math.round(v/maxV*(H-PAD*2))),x=PAD+i*(barW+1),y=H-PAD-bh,col=v<1?"var(--green)":"var(--yellow)";return``}).join("");return``}function perfHistoryVoiceLookup(voiceId){return(Array.isArray(window._voices)&&window._voices.length?window._voices:typeof _voices!="undefined"&&Array.isArray(_voices)?_voices:[]).find(v=>v&&(v.id===voiceId||v.name===voiceId||v.voice_id===voiceId))||null}function perfHistoryVoiceMeta(entry){var _a2,_b2,_c2;const voice=(entry==null?void 0:entry.voice)||"",saved=(entry==null?void 0:entry.voiceMeta)||{},lib=perfHistoryVoiceLookup(voice)||{},language=saved.lang||saved.language||(entry==null?void 0:entry.lang)||(entry==null?void 0:entry.language)||lib.lang||lib.language||"",gender=String(saved.gender||(entry==null?void 0:entry.gender)||lib.gender||"").trim().toUpperCase().charAt(0);return{id:voice,label:saved.label||saved.display_name||(entry==null?void 0:entry.voiceLabel)||lib.display_name||lib.name||voice,lang:language,gender:["F","M","N"].includes(gender)?gender:"",flag:saved.flag||(entry==null?void 0:entry.flag)||lib.flag||"",avatar:saved.avatar||(entry==null?void 0:entry.avatar)||lib.avatar||"",hasPicture:!!((_c2=(_b2=(_a2=saved.has_picture)!=null?_a2:saved.hasPicture)!=null?_b2:entry==null?void 0:entry.hasPicture)!=null?_c2:lib.has_picture)}}function perfHistoryExtraFields(voice,backend){const lib=perfHistoryVoiceLookup(voice)||{};return{voiceMeta:{label:lib.display_name||lib.name||voice,lang:lib.lang||lib.language||"",gender:lib.gender||"",flag:lib.flag||"",avatar:lib.avatar||"",has_picture:!!lib.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function perfHistoryGenderLabel(gender){return{F:"Female",M:"Male",N:"Diverse"}[gender]||""}function perfHistoryDeviceLabel(entry){return(entry==null?void 0:entry.device)||(typeof backendComputeDevice=="function"?backendComputeDevice((entry==null?void 0:entry.backend)||""):"")||"Unknown"}function perfHistoryDeviceHtml(entry){const label=perfHistoryDeviceLabel(entry),cls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass((entry==null?void 0:entry.backend)||""):label.toLowerCase().includes("gpu")?"gpu":label.toLowerCase().includes("cpu")?"cpu":"";return`${escHtml(label)}`}function perfHistoryAvatarHtml(entry){var _a2;const meta=perfHistoryVoiceMeta(entry),title=meta.label||meta.id||"Voice";if(meta.hasPicture)return``;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,24):null;if(icon)return`${icon.replace(/vp-avatar/g,"perf-history-avatar-icon")}`;const color=typeof avatarColor=="function"?avatarColor(meta.lang||meta.id||title):"#6b7280",init=((_a2=(title||"?").trim()[0])==null?void 0:_a2.toUpperCase())||"?";return`${escHtml(init)}`}function perfHistorySortValue(entry,key){switch(key){case"backend":return String(entry.backend||"").toLowerCase();case"language":return String(perfHistoryVoiceMeta(entry).lang||"").toLowerCase();case"gender":return String(perfHistoryGenderLabel(perfHistoryVoiceMeta(entry).gender)||"").toLowerCase();case"voice":return String(entry.voice||"").toLowerCase();case"device":return String(perfHistoryDeviceLabel(entry)||"").toLowerCase();case"avgLatencyMs":return Number(entry.avgLatencyMs);case"minLatencyMs":return Number(entry.minLatencyMs);case"avgRtf":return Number(entry.avgRtf);case"ts":default:return Number(entry.ts)}}function perfHistoryCompare(a,b){const av=perfHistorySortValue(a,PERF_HISTORY_SORT.key),bv=perfHistorySortValue(b,PERF_HISTORY_SORT.key);let result=0;if(typeof av=="string"||typeof bv=="string")result=String(av).localeCompare(String(bv),void 0,{numeric:!0,sensitivity:"base"});else{const an=Number.isFinite(av)?av:-1/0,bn=Number.isFinite(bv)?bv:-1/0;result=an===bn?0:an-bn}return PERF_HISTORY_SORT.dir==="asc"?result:-result}function perfHistoryHeadButton(key,label){const active=PERF_HISTORY_SORT.key===key,icon=active?PERF_HISTORY_SORT.dir==="asc"?"mdi-arrow-up":"mdi-arrow-down":"mdi-swap-vertical";return``).join("")} + + `).join(""),body.querySelectorAll(".fish-tag").forEach(b=>b.addEventListener("click",()=>_fishInsertTag(b.dataset.tag))),body.dataset.built="1")}function _fishTagsSync(){var _a2;const panel=$("fish-tags-panel");panel&&(panel.hidden=!_ttsIsFishBackend(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||""))}(function(){var _a2;const toggle=$("fish-tags-toggle"),body=$("fish-tags-body");toggle==null||toggle.addEventListener("click",()=>{_fishTagsRender(),body.hidden=!body.hidden,toggle.textContent=body.hidden?"Show tags":"Hide tags"}),(_a2=$("fish-tags-sample"))==null||_a2.addEventListener("click",()=>{const ta=$("preview-text-area");ta&&(ta.value=FISH_TAG_SAMPLE_TEXT,ta.focus())})})();function _ttsApplyEmotionTag(text,emotionValue){var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"";if(!_ttsIsFishBackend(backend)||!emotionValue||/^\s*\[/.test(text))return text;const tag=typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(emotionValue):"";return tag?`[${tag}] ${text}`:text}setTimeout(function(){const sel=$("preview-emotion-select");!sel||typeof window.REH_EMOTIONS=="undefined"||(window.REH_EMOTIONS.forEach(e=>{if(!e.value)return;const o=document.createElement("option");o.value=e.value,o.textContent=`${e.emoji} ${e.label}`,sel.appendChild(o)}),sel.addEventListener("change",()=>{var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"",help=$("preview-emotion-help");if(_ttsIsFishBackend(backend))help&&(help.style.display=sel.value?"block":"none",help.textContent=sel.value?"Applied as an inline [tag] in the text for Fish-Speech \u2014 the style instruction field below is ignored by this backend.":"");else{help&&(help.style.display="none");const styleInput=$("preview-style-instruction");styleInput&&sel.value&&(styleInput.value=sel.value)}}),_fishTagsSync())},0),$("tts-backend-select").addEventListener("change",()=>{var _a2;const sel=$("tts-voice-select");sel.innerHTML='',updateBackendHelp(),updatePreviewVoiceMatchPanel(),previewBlob=null,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,(_a2=$("preview-emotion-select"))==null||_a2.dispatchEvent(new Event("change")),_fishTagsSync()}),$("tts-voice-select").addEventListener("change",updatePreviewVoiceMatchPanel),$("preview-ref-play").addEventListener("click",async()=>{updatePreviewVoiceMatchPanel();const audio=$("preview-ref-audio");try{await audio.play()}catch(e){toast("Reference playback failed: "+e.message,"error")}}),$("preview-ref-use-text").addEventListener("click",()=>{const v=selectedPreviewLibraryVoice(),text=cleanReferenceText((v==null?void 0:v.transcript)||"");if(!text){toast("This voice has no reference text","error");return}$("preview-text-area").value=text,toast("Reference text copied to target text","success")}),$("preview-ref-synth").addEventListener("click",synthesizeSelectedReferenceText);let _ttsStreamHealth=null;function effectiveTtsPlaybackMode(override="settings"){return override&&override!=="settings"?override:_appSettings.tts_stream_mode||"auto"}async function isTtsStreamAvailable(force=!1){if(_ttsStreamHealth&&!force)return _ttsStreamHealth.ok;try{return _ttsStreamHealth=await fetch("/api/tts-stream-health").then(r=>r.json()),!!_ttsStreamHealth.ok}catch{return _ttsStreamHealth={ok:!1},!1}}async function createTtsStreamUrl(voice,text,instruct=""){if(!await isTtsStreamAvailable())throw new Error("streaming backend unavailable");const r=await fetch("/api/tts-stream-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,voice,instruct})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return(await r.json()).url}async function _ttsPreviewFetchWithRetry(body,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})}catch(e){if(i===tries)throw e;await new Promise(r=>setTimeout(r,2500*i))}}function _ttsBackendForVoice(voiceId,fallbackBackend){if(!voiceId||voiceId==="me")return fallbackBackend;const v=(window._voices||[]).find(x=>x.id===voiceId);return v&&!v.has_ref?"voice_design":fallbackBackend}async function fetchTtsPreviewBlob(voice,text,responseFormat="wav",instruct="",backend="voice_clone",applyPersona=!1,extra=null){const body={text,voice,response_format:responseFormat,instruct,backend};applyPersona&&(body.apply_persona=!0),extra&&typeof extra=="object"&&Object.assign(body,extra);const r=await _ttsPreviewFetchWithRetry(body);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const blob=await r.blob();if(responseFormat==="wav"){const v=(window._voices||[]).find(x=>x.id===voice),gainDb=v&&v.loudness&&typeof v.loudness.gain_db=="number"?v.loudness.gain_db:0;if(gainDb)try{const nr=await fetch("/api/audio/apply-gain?gain_db="+encodeURIComponent(gainDb),{method:"POST",body:blob});if(nr.ok)return await nr.blob()}catch{}}return blob}function _textWords(s){return String(s||"").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean)}function _textSimilarity(a,b){const wa=_textWords(a),wb=_textWords(b);if(!wa.length&&!wb.length)return 1;if(!wa.length||!wb.length)return 0;const counts=new Map;wa.forEach(w=>counts.set(w,(counts.get(w)||0)+1));let overlap=0;return wb.forEach(w=>{const c=counts.get(w);c&&(overlap++,counts.set(w,c-1))}),2*overlap/(wa.length+wb.length)}async function _voiceRoundtripCheck(voiceId,text,backend,instruct=""){const blob=await fetchTtsPreviewBlob(voiceId,text,"wav",instruct,backend),fd=new FormData;fd.append("file",blob,"roundtrip.wav"),fd.append("backend","configured");const r=await fetch("/api/transcribe-bytes",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return{transcript:d.text||"",score:_textSimilarity(text,d.text||""),blob}}async function createTtsAudioSource(voice,text,backend="voice_clone",modeOverride="settings",instruct="",applyPersona=!1,extra=null){const mode=effectiveTtsPlaybackMode(modeOverride);if(backend!=="streaming"||mode==="buffered"){const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}try{return{url:await createTtsStreamUrl(voice,text,instruct),blob:null,streaming:!0,label:"streaming"}}catch(e){if(mode==="streaming")throw e;const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}}let previewBlob=null;const PREVIEW_SAMPLE_TEXT="Hello! This is a voice preview from TTS Voice Creator - Clone and Design.";$("preview-text-area").addEventListener("focus",()=>{$("preview-text-area").value===PREVIEW_SAMPLE_TEXT&&($("preview-text-area").value="")},{once:!0});function _onPreviewGenerated(source,voice,text,backend,instruct){typeof effectsSourceBlob!="undefined"&&(window._effectsSourceBlob=null),window._effectsSynthArgs={voice,text,instruct:instruct||"",backend};const ea=$("effects-apply-btn");ea&&(ea.disabled=!1);const ap=$("add-to-playlist-btn");ap&&source.blob&&(ap.disabled=!1),typeof historyPush=="function"&&source.blob&&historyPush(voice,text,backend,source.blob,source.url)}const _TRYOUT_SPEED_KEY="ttsvc_tryout_native_speed";(function(){const saved=localStorage.getItem(_TRYOUT_SPEED_KEY);if(saved){const el=$("preview-native-speed");el&&(el.value=saved)}})(),(_R=$("preview-native-speed"))==null||_R.addEventListener("change",function(){localStorage.setItem(_TRYOUT_SPEED_KEY,this.value)}),$("preview-btn").addEventListener("click",async()=>{var _a2,_b2,_c2,_d2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value),applyPersona=((_b2=$("preview-persona-toggle"))==null?void 0:_b2.checked)||!1;if(!backend){toast("No available TTS backend","error");return}if(!voice){toast("Select a TTS voice","error");return}if(!text){toast("Enter preview text","error");return}$("preview-btn").disabled=!0,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,$("add-to-playlist-btn")&&($("add-to-playlist-btn").disabled=!0),$("effects-apply-btn")&&($("effects-apply-btn").disabled=!0);const _nspd=parseFloat((_c2=$("preview-native-speed"))==null?void 0:_c2.value),_extra=!isNaN(_nspd)&&_nspd!==1?{speed:_nspd}:null;try{const audio=$("preview-audio"),source=((_d2=$("preview-chunked-toggle"))==null?void 0:_d2.checked)&&text.length>200&&typeof generateChunkedTts=="function"?await generateChunkedTts(voice,text,backend,instruct,_extra,applyPersona):await createTtsAudioSource(voice,text,backend,$("preview-playback-mode").value,instruct,applyPersona,_extra);previewBlob=source.blob,window._previewVoice=voice,window._previewBackend=backend,window._previewText=text,audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,_onPreviewGenerated(source,voice,text,backend,instruct),toast(source.streaming?"Streaming preview playing":source.label==="chunked"?`Chunked (${text.length} chars) playing`:"Preview playing","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{$("preview-btn").disabled=!1}}),$("save-preview-mp3-btn").addEventListener("click",async()=>{var _a2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value);if(!backend){toast("No available TTS backend","error");return}if(!voice||!text)return;const btn=$("save-preview-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(voice||"preview")+"_preview.mp3",a.click(),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),$("save-preview-btn").addEventListener("click",()=>{if(!previewBlob)return;const a=document.createElement("a");a.href=URL.createObjectURL(previewBlob),a.download=($("tts-voice-select").value||"preview")+"_preview.wav",a.click()});const PERF_HISTORY_KEY="vcf-perf-history",PERF_HISTORY_MAX=50,PERF_HISTORY_SORT={key:"ts",dir:"desc"};function perfHistoryLoad(){try{return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY)||"[]")}catch{return[]}}function perfHistorySave(entries){try{localStorage.setItem(PERF_HISTORY_KEY,JSON.stringify(entries.slice(-PERF_HISTORY_MAX)))}catch{}}function perfHistoryAdd(entry){const h=perfHistoryLoad();h.push(entry),perfHistorySave(h)}function perfSparklineSvg(rtfValues){if(!rtfValues.length)return"";const W=120,H=32,PAD=2,barW=Math.max(4,Math.floor((W-PAD*2)/rtfValues.length)-1),maxV=Math.max(...rtfValues,1),bars=rtfValues.map((v,i)=>{const bh=Math.max(3,Math.round(v/maxV*(H-PAD*2))),x=PAD+i*(barW+1),y=H-PAD-bh,col=v<1?"var(--green)":"var(--yellow)";return``}).join("");return``}function perfHistoryVoiceLookup(voiceId){return(Array.isArray(window._voices)&&window._voices.length?window._voices:typeof _voices!="undefined"&&Array.isArray(_voices)?_voices:[]).find(v=>v&&(v.id===voiceId||v.name===voiceId||v.voice_id===voiceId))||null}function perfHistoryVoiceMeta(entry){var _a2,_b2,_c2;const voice=(entry==null?void 0:entry.voice)||"",saved=(entry==null?void 0:entry.voiceMeta)||{},lib=perfHistoryVoiceLookup(voice)||{},language=saved.lang||saved.language||(entry==null?void 0:entry.lang)||(entry==null?void 0:entry.language)||lib.lang||lib.language||"",gender=String(saved.gender||(entry==null?void 0:entry.gender)||lib.gender||"").trim().toUpperCase().charAt(0);return{id:voice,label:saved.label||saved.display_name||(entry==null?void 0:entry.voiceLabel)||lib.display_name||lib.name||voice,lang:language,gender:["F","M","N"].includes(gender)?gender:"",flag:saved.flag||(entry==null?void 0:entry.flag)||lib.flag||"",avatar:saved.avatar||(entry==null?void 0:entry.avatar)||lib.avatar||"",hasPicture:!!((_c2=(_b2=(_a2=saved.has_picture)!=null?_a2:saved.hasPicture)!=null?_b2:entry==null?void 0:entry.hasPicture)!=null?_c2:lib.has_picture)}}function perfHistoryExtraFields(voice,backend){const lib=perfHistoryVoiceLookup(voice)||{};return{voiceMeta:{label:lib.display_name||lib.name||voice,lang:lib.lang||lib.language||"",gender:lib.gender||"",flag:lib.flag||"",avatar:lib.avatar||"",has_picture:!!lib.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function perfHistoryGenderLabel(gender){return{F:"Female",M:"Male",N:"Diverse"}[gender]||""}function perfHistoryDeviceLabel(entry){return(entry==null?void 0:entry.device)||(typeof backendComputeDevice=="function"?backendComputeDevice((entry==null?void 0:entry.backend)||""):"")||"Unknown"}function perfHistoryDeviceHtml(entry){const label=perfHistoryDeviceLabel(entry),cls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass((entry==null?void 0:entry.backend)||""):label.toLowerCase().includes("gpu")?"gpu":label.toLowerCase().includes("cpu")?"cpu":"";return`${escHtml(label)}`}function perfHistoryAvatarHtml(entry){var _a2;const meta=perfHistoryVoiceMeta(entry),title=meta.label||meta.id||"Voice";if(meta.hasPicture)return``;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,24):null;if(icon)return`${icon.replace(/vp-avatar/g,"perf-history-avatar-icon")}`;const color=typeof avatarColor=="function"?avatarColor(meta.lang||meta.id||title):"#6b7280",init=((_a2=(title||"?").trim()[0])==null?void 0:_a2.toUpperCase())||"?";return`${escHtml(init)}`}function perfHistorySortValue(entry,key){switch(key){case"backend":return String(entry.backend||"").toLowerCase();case"language":return String(perfHistoryVoiceMeta(entry).lang||"").toLowerCase();case"gender":return String(perfHistoryGenderLabel(perfHistoryVoiceMeta(entry).gender)||"").toLowerCase();case"voice":return String(entry.voice||"").toLowerCase();case"device":return String(perfHistoryDeviceLabel(entry)||"").toLowerCase();case"avgLatencyMs":return Number(entry.avgLatencyMs);case"minLatencyMs":return Number(entry.minLatencyMs);case"avgRtf":return Number(entry.avgRtf);case"ts":default:return Number(entry.ts)}}function perfHistoryCompare(a,b){const av=perfHistorySortValue(a,PERF_HISTORY_SORT.key),bv=perfHistorySortValue(b,PERF_HISTORY_SORT.key);let result=0;if(typeof av=="string"||typeof bv=="string")result=String(av).localeCompare(String(bv),void 0,{numeric:!0,sensitivity:"base"});else{const an=Number.isFinite(av)?av:-1/0,bn=Number.isFinite(bv)?bv:-1/0;result=an===bn?0:an-bn}return PERF_HISTORY_SORT.dir==="asc"?result:-result}function perfHistoryHeadButton(key,label){const active=PERF_HISTORY_SORT.key===key,icon=active?PERF_HISTORY_SORT.dir==="asc"?"mdi-arrow-up":"mdi-arrow-down":"mdi-swap-vertical";return``}function renderPerfHistory(){var _a2,_b2;const histList=$("perf-history-list");if(!histList)return;const filterEl=$("perf-history-filter-current"),filterOn=filterEl==null?void 0:filterEl.checked,curBack=(_a2=$("perf-backend-select"))==null?void 0:_a2.value,curVoice=(_b2=$("perf-voice-select"))==null?void 0:_b2.value;let entries=perfHistoryLoad().slice();if(filterOn&&curBack&&(entries=entries.filter(e=>e.backend===curBack&&e.voice===curVoice)),!entries.length){histList.innerHTML='
'+(filterOn?"No history for this backend/voice yet.":"No benchmark history yet. Run a benchmark above to start tracking.")+"
";return}entries.sort((a,b)=>perfHistoryCompare(a,b)||Number(b.ts)-Number(a.ts));const head=`
${perfHistoryHeadButton("ts","Date / Time")} @@ -1018,7 +1028,7 @@ This warms each voice so the engine caches its .pt and first playback is instant
- `}).join("");const countEl=$("reh-cast-count");if(countEl){const shown=others.length,total=allOthers.length;countEl.textContent=shown===total?`${total} ${total===1?"character":"characters"} + narrator`:`${shown} of ${total} characters`}if(typeof _wireCharCards=="function"&&(_rehLibCharsCache||[]).length){const recsById=new Map(_rehLibCharsCache.map(r=>[r.id,r]));_wireCharCards(list,recsById,_rehLibCharsCache,renderCastList,{container:list,onBack:renderCastList})}_wireCastControls(),applyCastView();const card=el=>el.closest(".reh-cast-card"),spOf=el=>card(el).dataset.speaker;window.VoicePicker&&list.querySelectorAll(".reh-voice-sel[id]").forEach(sel=>{const cur=sel.value;VoicePicker.upgrade(sel.id),cur&&VoicePicker.setValue(sel.id,cur)}),list.querySelectorAll(".reh-me-check").forEach(cb=>cb.addEventListener("change",function(){var _a3;const sp=spOf(this);rehState.cast[sp].voice=this.checked?"me":((_a3=card(this).querySelector(".reh-voice-sel"))==null?void 0:_a3.value)||"",rehState.cast[sp].voiceData=this.checked?null:getVoiceData(rehState.cast[sp].voice),renderCastList()})),list.querySelectorAll(".reh-voice-sel").forEach(sel=>sel.addEventListener("change",function(){const sp=this.dataset.speaker;rehState.cast[sp].voice=this.value,rehState.cast[sp].voiceData=getVoiceData(this.value),delete rehState.cast[sp].online,sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=this.value),renderCastList()})),list.querySelectorAll(".reh-cast-instruct").forEach(inp=>inp.addEventListener("input",function(){rehState.cast[spOf(this)].instruct=this.value})),list.querySelectorAll(".reh-cc-lang").forEach(s=>s.addEventListener("change",function(){rehState.cast[spOf(this)].lang=this.value})),list.querySelectorAll(".reh-cc-gender").forEach(s=>s.addEventListener("change",function(){rehState.cast[spOf(this)].gender=this.value})),list.querySelectorAll(".reh-cc-tags").forEach(i=>i.addEventListener("input",function(){rehState.cast[spOf(this)].tags=this.value})),list.querySelectorAll(".reh-cc-soul-text").forEach(t=>t.addEventListener("input",function(){rehState.cast[spOf(this)].soul=this.value})),list.querySelectorAll(".reh-cc-develop").forEach(b=>b.addEventListener("click",function(e){e.preventDefault(),_castDevelop(spOf(this),this)})),list.querySelectorAll(".reh-cc-iconbtn").forEach(b=>b.addEventListener("click",function(){const sp=spOf(this),act=this.dataset.act,c=rehState.cast[sp];act==="ignore"?(c.ignored=!c.ignored,_castApplyToLines(sp,l=>l.ignored=c.ignored),renderCastList()):act==="hide"?(c.hidden=!c.hidden,_castApplyToLines(sp,l=>l.hidden=c.hidden),renderCastList()):act==="delete"&&_castDeleteCharacter(sp)})),list._voDelegated||(list._voDelegated=!0,list.addEventListener("click",e=>{var _a3,_b2,_c2,_d2,_e2,_f2;const sampleBtn=e.target.closest(".reh-cc-sample-btn");if(sampleBtn){e.preventDefault(),_rehPreviewCastLine(sampleBtn.dataset.speaker,sampleBtn);return}const playBtn=e.target.closest(".reh-vo-play");if(playBtn){e.preventDefault(),playBtn.dataset.voice?_rehPreviewLocal(playBtn.dataset.voice,playBtn):_rehAudition(playBtn.dataset.url,playBtn);return}const toggle=e.target.closest(".reh-vo-toggle");if(toggle){e.preventDefault();const alts=(_a3=toggle.closest(".reh-cc-online"))==null?void 0:_a3.querySelector(".reh-vo-alts");alts&&(alts.hidden=!alts.hidden,toggle.textContent=toggle.textContent.replace(/[▾▴]\s*$/,"")+(alts.hidden?"\u25BE":"\u25B4"));return}const tool=e.target.closest(".reh-vo-tool");if(tool){e.preventDefault();const panel=tool.closest(".reh-cc-online"),want=tool.dataset.tool,localP=panel.querySelector(".reh-vo-local-panel"),searchP=panel.querySelector(".reh-vo-search-panel"),showLocal=want==="local"&&((_b2=localP==null?void 0:localP.hidden)!=null?_b2:!0),showSearch=want==="search"&&((_c2=searchP==null?void 0:searchP.hidden)!=null?_c2:!0);if(localP&&(localP.hidden=!showLocal),searchP&&(searchP.hidden=!showSearch),panel.querySelectorAll(".reh-vo-tool").forEach(t=>t.classList.toggle("active",t.dataset.tool==="local"&&showLocal||t.dataset.tool==="search"&&showSearch)),showLocal){const card2=e.target.closest(".reh-cast-card");_rehRenderLocalResults(card2,spOf(tool),""),(_d2=card2.querySelector(".reh-vo-local-input"))==null||_d2.focus()}showSearch&&((_e2=panel.querySelector(".reh-vo-search-input"))==null||_e2.focus());return}const goBtn=e.target.closest(".reh-vo-search-go");if(goBtn){e.preventDefault();const card2=e.target.closest(".reh-cast-card");_rehSearchOnline(card2,spOf(goBtn),(_f2=card2.querySelector(".reh-vo-search-input"))==null?void 0:_f2.value);return}const use=e.target.closest(".reh-vo-use");if(use){e.preventDefault();const sp=spOf(use),act=use.dataset.act;act==="local"?_rehAssignLocal(sp,use.dataset.voice):act==="search"?_rehUseSearchResult(sp,parseInt(use.dataset.idx,10),use):_rehUseCandidate(sp,parseInt(use.dataset.idx,10),use)}}),list.addEventListener("input",e=>{const li=e.target.closest(".reh-vo-local-input");li&&_rehRenderLocalResults(e.target.closest(".reh-cast-card"),spOf(li),li.value)}),list.addEventListener("keydown",e=>{const si=e.target.closest(".reh-vo-search-input");si&&e.key==="Enter"&&(e.preventDefault(),_rehSearchOnline(e.target.closest(".reh-cast-card"),spOf(si),si.value))}))}function applyCastView(){const list=$("reh-cast-list");if(!list)return;const view=rehState.castView==="list"?"list":"card";list.classList.toggle("reh-cast-view-card",view==="card"),list.classList.toggle("reh-cast-view-list",view==="list"),document.querySelectorAll("#reh-cast-view-toggle .reh-view-btn").forEach(b=>b.classList.toggle("active",b.dataset.view===view))}try{rehState.castView=localStorage.getItem("reh-cast-view")||"card"}catch{rehState.castView="card"}rehState.castFilter={search:"",gender:"",lang:""};try{rehState.castSort=JSON.parse(localStorage.getItem("reh-cast-sort"))||{by:"name",dir:"asc"}}catch{rehState.castSort={by:"name",dir:"asc"}}function _wireCastControls(){const toggle=$("reh-cast-view-toggle");if(!toggle||toggle._wired)return;toggle._wired=!0,toggle.querySelectorAll(".reh-view-btn").forEach(b=>b.addEventListener("click",()=>{rehState.castView=b.dataset.view;try{localStorage.setItem("reh-cast-view",b.dataset.view)}catch{}applyCastView()}));const search=$("reh-cast-search"),fg=$("reh-cast-filter-gender"),fl=$("reh-cast-filter-lang"),sortSel=$("reh-cast-sort"),dirBtn=$("reh-cast-sort-dir"),setDirIcon=()=>{dirBtn&&(dirBtn.dataset.dir=rehState.castSort.dir,dirBtn.querySelector(".mdi").className="mdi mdi-sort-"+(rehState.castSort.dir==="desc"?"descending":"ascending"))},persist=()=>{try{localStorage.setItem("reh-cast-sort",JSON.stringify(rehState.castSort))}catch{}};sortSel&&(sortSel.value=rehState.castSort.by),setDirIcon(),search==null||search.addEventListener("input",()=>{rehState.castFilter.search=search.value.trim(),renderCastList(),search.focus()}),fg==null||fg.addEventListener("change",()=>{rehState.castFilter.gender=fg.value,renderCastList()}),fl==null||fl.addEventListener("change",()=>{rehState.castFilter.lang=fl.value,renderCastList()}),sortSel==null||sortSel.addEventListener("change",()=>{rehState.castSort.by=sortSel.value,rehState.castSort.dir=sortSel.value==="lines"?"desc":"asc",setDirIcon(),persist(),renderCastList()}),dirBtn==null||dirBtn.addEventListener("click",()=>{rehState.castSort.dir=rehState.castSort.dir==="desc"?"asc":"desc",setDirIcon(),persist(),renderCastList()})}function _castSortFilter(speakers,lineCount){const f=rehState.castFilter||{},s=rehState.castSort||{by:"name",dir:"asc"},out=speakers.filter(sp=>{const c=rehState.cast[sp]||{};if(f.search){const q=f.search.toLowerCase();if(!sp.toLowerCase().includes(q)&&!(c.tags||"").toLowerCase().includes(q))return!1}return!(f.gender&&(c.gender||"")!==f.gender||f.lang&&(c.lang||"")!==f.lang)}),byName=(a,b)=>a.localeCompare(b,void 0,{sensitivity:"base"}),cmp={name:byName,gender:(a,b)=>(rehState.cast[a].gender||"~").localeCompare(rehState.cast[b].gender||"~")||byName(a,b),lang:(a,b)=>(rehState.cast[a].lang||"~").localeCompare(rehState.cast[b].lang||"~")||byName(a,b),lines:(a,b)=>lineCount(a)-lineCount(b)||byName(a,b),tag:(a,b)=>(rehState.cast[a].tags||"~").localeCompare(rehState.cast[b].tags||"~")||byName(a,b)}[s.by]||byName;return out.sort(cmp),s.dir==="desc"&&out.reverse(),out}function _castDeleteCharacter(sp){const n=rehState.lines.filter(l=>l.speaker===sp&&l.type==="dialog").length;if(confirm(`Delete \u201C${sp}\u201D and their ${n} line${n!==1?"s":""}? This cannot be undone.`)){for(let i=rehState.lines.length-1;i>=0;i--)rehState.lines[i].speaker===sp&&rehState.lines[i].type==="dialog"&&(rehState.lines.splice(i,1),_reindexLineState(i));delete rehState.cast[sp],renderCastList(),rehState.lines.length&&buildScriptPage(),toast(`Removed ${sp}`,"success")}}async function _castDevelop(sp,btn){var _a2,_b2,_c2,_d2,_e2;const script=((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText();if(!script){toast("Load a script first","error");return}const c=rehState.cast[sp],orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Developing\u2026';try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script,names:[sp],llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:c.lang||((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const info=((_e2=(await r.json()).characters)==null?void 0:_e2[0])||{};info.gender&&(c.gender=String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/,"N")),info.description&&(c.soul=info.description,c.instruct=c.instruct||info.description),renderCastList(),toast(`Developed ${sp}`,"success")}catch(e){btn.disabled=!1,btn.innerHTML=orig,toast("Develop failed: "+e.message,"error")}}async function refreshRehBackends(){rehInitLlmField();const sel=$("reh-backend-select");if(!sel)return;let backends=typeof availableTtsBackends=="function"?availableTtsBackends():[];!backends.length&&typeof refreshTtsBackendAvailability=="function"&&(await refreshTtsBackendAvailability().catch(()=>{}),backends=typeof availableTtsBackends=="function"?availableTtsBackends():[]),!backends.length&&typeof _ttsBackends!="undefined"&&Array.isArray(_ttsBackends)&&(backends=_ttsBackends);const prev=sel.value||rehState.backend;if(sel.innerHTML=backends.length?backends.map(b=>``).join(""):'',prev&&[...sel.options].some(o=>o.value===prev))sel.value=prev;else{const pick=["fishspeech","voice_clone","customvoice","voice_design"].find(id=>[...sel.options].some(o=>o.value===id));pick&&(sel.value=pick)}rehState.backend=sel.value||"",_checkToneStyleSupport()}(window._ttsRefreshHooks=window._ttsRefreshHooks||[]).push(()=>{const sel=$("reh-backend-select");if(!sel)return;const backends=typeof availableTtsBackends=="function"?availableTtsBackends():[];if(!backends.length)return;const prev=sel.value||rehState.backend;sel.innerHTML=backends.map(b=>``).join(""),prev&&[...sel.options].some(o=>o.value===prev)?sel.value=prev:sel.options.length&&(sel.value=sel.options[0].value),rehState.backend=sel.value||""});function _rehAllVoiceIds(include){const ids=new Set((rehState.voices||[]).filter(id=>_rehVoiceVisibleId(id,include)));return(window._voices||[]).forEach(v=>{v&&v.id&&(v.enabled!==!1||v.id===include)&&ids.add(v.id)}),include&&ids.add(include),[...ids].sort((a,b)=>a.localeCompare(b,void 0,{sensitivity:"base"}))}function populateNarratorSelect(){const sel=$("reh-narrator-voice");if(!sel)return;const cur=rehState.narratorVoice||sel.value;sel.innerHTML=''+_rehAllVoiceIds(cur).map(v=>``).join("")}(_sa=$("reh-fetch-voices-btn"))==null||_sa.addEventListener("click",async()=>{var _a2;const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a backend first","error");return}$("reh-fetch-voices-btn").disabled=!0;try{(!window._voices||!window._voices.length)&&typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{});const raw=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());rehState.voices=(Array.isArray(raw)?raw.map(v=>typeof v=="string"?v:v.id||String(v)):[]).filter(id=>_rehVoiceVisibleId(id)),renderCastList(),populateNarratorSelect(),toast("Fetched "+rehState.voices.length+" voices","success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("reh-fetch-voices-btn").disabled=!1}}),(_ta=$("reh-narrator-voice"))==null||_ta.addEventListener("change",function(){rehState.narratorVoice=this.value}),(_ua=$("reh-back-1-btn"))==null||_ua.addEventListener("click",()=>showPhase(1));const _BUILD_INSTRUCT_TEMPLATES={DE:e=>`Sprich in einem ${e} Tonfall.`,EN:e=>`Speak in a ${e} manner.`},_BUILD_INSTRUCT_LANG_NAMES={DE:"German",FR:"French",ES:"Spanish",IT:"Italian",PT:"Portuguese",NL:"Dutch",PL:"Polish"};function _buildAccentClause(langCode){const langName=_BUILD_INSTRUCT_LANG_NAMES[langCode];return langName?`Speak with an authentic native ${langName} accent \u2014 not American-accented, not an English speaker doing ${langName}.`:langCode==="EN"?"English with a neutral British or international accent, explicitly not American/US-accented.":""}function _buildInstruct(voiceProfile,emotion,voiceId){const p=(voiceProfile||"").trim(),e=(emotion||"").trim(),langCode=String(voiceId||"").split("_")[0].toUpperCase(),accent=_buildAccentClause(langCode);if(!e&&!p&&!accent)return"";const effectiveEmotion=(e&&langCode!=="EN"&&typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(e):"")||e,emotionTmpl=_BUILD_INSTRUCT_TEMPLATES.EN;return[e?emotionTmpl(effectiveEmotion):"",p,accent].filter(Boolean).join(" ")}function _rehBackendIsFish(){var _a2;let id="";try{id=typeof backendById=="function"&&((_a2=backendById(rehState.backend))==null?void 0:_a2.id)||rehState.backend||""}catch{id=rehState.backend||""}return/fish/i.test(id)}const _REH_EMOTION_DE_EN={w\u00FCtend:"angry",zornig:"angry",erz\u00FCrnt:"angry",ver\u00E4rgert:"annoyed",gereizt:"irritated",traurig:"sad",melancholisch:"melancholic",betr\u00FCbt:"sad",niedergeschlagen:"dejected",\u00E4ngstlich:"scared",furchtsam:"fearful",ver\u00E4ngstigt:"frightened",panisch:"panicked",nerv\u00F6s:"nervous",fr\u00F6hlich:"happy",gl\u00FCcklich:"happy",freudig:"joyful",heiter:"cheerful",vergn\u00FCgt:"delighted",fl\u00FCsternd:"whispering",leise:"quiet",ged\u00E4mpft:"hushed",aufgeregt:"excited",begeistert:"enthusiastic",euphorisch:"euphoric",\u00FCberrascht:"surprised",erstaunt:"astonished",verbl\u00FCfft:"amazed",genervt:"annoyed",frustriert:"frustrated",verzweifelt:"desperate",hoffnungslos:"hopeless",resigniert:"resigned",entschlossen:"determined",entschieden:"decisive",selbstbewusst:"confident",stolz:"proud",arrogant:"arrogant",sch\u00FCchtern:"shy",verlegen:"embarrassed",unsicher:"uncertain",ironisch:"sarcastic",sarkastisch:"sarcastic",sp\u00F6ttisch:"mocking",h\u00F6hnisch:"scornful",ver\u00E4chtlich:"contemptuous",ernst:"serious",streng:"stern",autorit\u00E4r:"authoritative",befehlend:"commanding",sanft:"gentle",z\u00E4rtlich:"tender",liebevoll:"loving",warm:"warm",kalt:"cold",distanziert:"distant",gleichg\u00FCltig:"indifferent",gelangweilt:"bored",geheimnisvoll:"mysterious",unheimlich:"eerie",d\u00FCster:"ominous",bedrohlich:"threatening",dramatisch:"dramatic",theatralisch:"theatrical",pathetisch:"melodramatic",ruhig:"calm",gelassen:"composed",besonnen:"measured",schockiert:"shocked",entsetzt:"horrified",fassungslos:"stunned",z\u00F6gernd:"hesitant",verwirrt:"confused",ratlos:"bewildered",unschl\u00FCssig:"undecided",weinend:"tearful",schluchzend:"sobbing",trauernd:"grieving",gebrochen:"broken",schroff:"curt",barsch:"gruff",grob:"rough",abweisend:"dismissive",freundlich:"friendly",herzlich:"warm",einladend:"welcoming",spielerisch:"playful",neckend:"teasing",frech:"cheeky",schelmisch:"mischievous",romantisch:"romantic",sehns\u00FCchtig:"longing",verliebt:"infatuated",triumphierend:"triumphant",siegessicher:"victorious",erleichtert:"relieved",beruhigt:"reassured",schuldbewusst:"guilty",reum\u00FCtig:"remorseful",neugierig:"curious",interessiert:"interested",m\u00FCde:"weary",ersch\u00F6pft:"exhausted",wehm\u00FCtig:"wistful",nostalgisch:"nostalgic",bemerkend:"remarking",feststellend:"noting",sachlich:"matter-of-fact",n\u00FCchtern:"plain",flehend:"pleading",bittend:"imploring",warnend:"warning",mahnend:"admonishing",trotzig:"defiant",rebellisch:"rebellious",erschrocken:"startled",verst\u00F6rt:"disturbed"};function _rehEmotionEnglishTag(emotion){const raw=(emotion||"").trim();if(!raw)return"";const lower=raw.split(/[,;]\s*/)[0].trim().toLowerCase();if(_REH_EMOTION_DE_EN[lower])return _REH_EMOTION_DE_EN[lower];const stem=lower.replace(/(e|er|es|en|em)$/,"");if(stem.length>=4){for(const key in _REH_EMOTION_DE_EN)if(key.startsWith(stem))return _REH_EMOTION_DE_EN[key]}return/^[a-z\- ]+$/.test(lower)?lower:""}function _rehInlineTone(text,emotion){if(!_rehBackendIsFish()||/^\s*\[/.test(text))return text;const tag=_rehEmotionEnglishTag(emotion);return tag?`[${tag}] ${text}`:text}const REH_LANG_CODE={English:"EN",German:"DE",French:"FR",Spanish:"ES",Italian:"IT",Auto:"EN"};function rehDefaultLlmUrl(){try{if(typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url)return _appSettings.llm_url}catch{}return"http://localhost:11434/v1"}function rehCollectLlmEndpoints(){const seen=new Set,results=[],add=(url,label)=>{url&&(url=url.trim(),!(!url||seen.has(url))&&(seen.add(url),results.push({url,label:label||url})))};return add(rehDefaultLlmUrl(),"Active LLM"),document.querySelectorAll(".llm-local-url-inp, [data-llm-local-key]").forEach(inp=>{var _a2,_b2,_c2;const v=(_a2=inp.value)==null?void 0:_a2.trim(),def=inp.dataset.llmLocalDefault,key=inp.dataset.llmLocalKey||inp.dataset.dcUrlKey||"",card=inp.closest('[class*="llm-local-card"], [class*="llm-local"]'),name=((_c2=(_b2=card==null?void 0:card.querySelector(".llm-local-name"))==null?void 0:_b2.textContent)==null?void 0:_c2.trim())||key;add(v||def,name)}),document.querySelectorAll(".dc-url-inp").forEach(inp=>{var _a2,_b2,_c2;const card=inp.closest('[class*="llm-local-card"]');if(!card)return;const name=((_b2=(_a2=card.querySelector(".llm-local-name"))==null?void 0:_a2.textContent)==null?void 0:_b2.trim())||"";add(((_c2=inp.value)==null?void 0:_c2.trim())||inp.dataset.dcDefault,name)}),[["http://localhost:11434/v1","Ollama"],["http://localhost:8000/v1","vLLM"],["http://localhost:1234/v1","LM Studio"],["http://localhost:28080/v1","llama-swap"],["http://localhost:14000/v1","LiteLLM"]].forEach(([u,l])=>add(u,l)),results}function rehInitLlmField(){const u=$("reh-llm-url");if(!u)return;u.value||(u.value=rehDefaultLlmUrl());const dl=$("reh-llm-url-list");dl&&(dl.innerHTML=rehCollectLlmEndpoints().map(e=>``).join(""))}(_va=$("reh-llm-refresh"))==null||_va.addEventListener("click",async()=>{var _a2;const url=((_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim())||rehDefaultLlmUrl(),sel=$("reh-llm-model");if(sel){sel.innerHTML='';try{const models=(await(await fetch("/api/conversation/llm-models?url="+encodeURIComponent(url))).json()).models||[];sel.innerHTML=''+models.map(m=>``).join("");const want=typeof _appSettings!="undefined"&&_appSettings?_appSettings.llm_model:"";want&&models.includes(want)&&(sel.value=want),toast(models.length?`Found ${models.length} models`:"No models found",models.length?"success":"error")}catch(e){sel.innerHTML='',toast("Could not list models: "+e.message,"error")}}});const REH_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"};function _pickVoiceAvatar(gender,desc,speaker){const d=((desc||"")+" "+(speaker||"")).toLowerCase();return/\b(robot|android|synthetic|artificial|computer|machine|cyborg|a\.?i\.?|operating system|\bos\b|digital|hologram|drone)\b/.test(d)?"robot":/\b(animal|creature|beast|dragon|monster|cat|dog|wolf|lion|bird|horse|dino|dinosaur|alien)\b/.test(d)?"animal":gender==="M"?"male":gender==="F"?"female":"neutral"}let rehDesignCancelled=!1;(_wa=$("reh-autodesign-cancel"))==null||_wa.addEventListener("click",()=>{rehDesignCancelled=!0}),(_xa=$("reh-autodesign-btn"))==null||_xa.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2;if(!((_a2=$("reh-backend-select"))==null?void 0:_a2.value)){toast("Select a TTS backend first","error");return}const speakers=Object.keys(rehState.cast);if(!speakers.length){toast("No characters to design for","error");return}const script=((_b2=$("reh-script-text"))==null?void 0:_b2.value.trim())||linesToScriptText(),llmUrl=((_c2=$("reh-llm-url"))==null?void 0:_c2.value.trim())||rehDefaultLlmUrl(),llmModel=((_d2=$("reh-llm-model"))==null?void 0:_d2.value)||"",language=((_e2=$("reh-design-lang"))==null?void 0:_e2.value)||"English",langCode=REH_LANG_CODE[language]||"EN",scriptTitle=((_f2=$("reh-script-title"))==null?void 0:_f2.value.trim())||"Script",tag=(typeof _umlautSafe=="function"?_umlautSafe(scriptTitle):scriptTitle).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,24)||"Script",btn=$("reh-autodesign-btn"),prog=$("reh-autodesign-progress"),fill=$("reh-autodesign-fill"),label=$("reh-autodesign-label");btn.disabled=!0,rehDesignCancelled=!1,prog&&(prog.hidden=!1);const setProg=(d,t,msg)=>{fill&&(fill.style.width=(t?d/t*100:0)+"%"),label&&(label.textContent=msg||`${d} / ${t}`)};setProg(0,speakers.length,"Analyzing script with LLM\u2026");let characters;try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script,names:speakers,llm_url:llmUrl,model:llmModel,language})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}characters=(await r.json()).characters||[]}catch(e){toast("Character analysis failed: "+e.message,"error"),btn.disabled=!1,prog&&(prog.hidden=!0);return}const byName={};characters.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)});let done=0;try{const designOnly=speakers.filter(sp=>{var _a3;return((_a3=rehState.cast[sp])==null?void 0:_a3.voice)!=="me"});for(const sp of designOnly){if(rehDesignCancelled){toast("Cancelled","error");break}if(!rehState.cast[sp]){done++;continue}const info=byName[sp.toUpperCase().trim()]||{},gender=(info.gender||"N").toUpperCase().charAt(0).replace(/[^MFN]/,"N")||"N",desc=info.description||`A ${info.age||"adult"} ${gender==="M"?"male":gender==="F"?"female":""} character named ${sp}, natural expressive voice.`,sampleLine=((_g2=rehState.lines.find(l=>l.type==="dialog"&&l.speaker===sp))==null?void 0:_g2.text)||`Hello, I am ${sp}.`,safeName=(typeof _umlautSafe=="function"?_umlautSafe(sp):sp).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,24)||"Char",voiceId=`${langCode}_${gender}_${safeName}_${tag}`.slice(0,90);rehMarkCastDesigning(sp,"designing",null,{gender,language,voiceId,desc,age:info.age||"",step:"Generating voice audio\u2026"}),setProg(done,designOnly.length,`Designing ${sp}\u2026 (${done+1}/${designOnly.length})`);try{const dr=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({instruct:desc,sample_text:stripMarkdown(sampleLine).slice(0,300),language,gender,dialogue:!1})});if(!dr.ok){const e=await dr.json().catch(()=>({}));throw new Error(e.detail||dr.statusText)}const dd=await dr.json(),sr=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:dd.id,voice_id:voiceId,transcript:stripMarkdown(sampleLine).slice(0,300)})});if(!sr.ok){const e=await sr.json().catch(()=>({}));throw new Error(e.detail||sr.statusText)}const saved=await sr.json(),charName=sp===REH_NARRATOR_KEY?"Narrator":sp;typeof saveMeta=="function"&&await saveMeta(saved.voice_id,{gender,name:charName,avatar:_pickVoiceAvatar(gender,desc,sp),origin:"designed",group:`Rehearser: ${scriptTitle}`,note:`Rehearser \xB7 ${scriptTitle} \xB7 ${charName} \u2014 ${desc.slice(0,180)}`,transcript:stripMarkdown(sampleLine).slice(0,300),tag:scriptTitle}).catch(()=>{}),rehState.cast[sp].voice=saved.voice_id,rehState.cast[sp].instruct=rehState.cast[sp].instruct||desc,rehState.cast[sp].soul=rehState.cast[sp].soul||[info.age?`Age: ${info.age}`:"",desc].filter(Boolean).join(" \xB7 "),rehState.cast[sp].voiceData=null,rehState.voices.includes(saved.voice_id)||rehState.voices.push(saved.voice_id),rehMarkCastDesigning(sp,"done")}catch(e){rehMarkCastDesigning(sp,"err",e.message)}done++,setProg(done,designOnly.length)}typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),rehDesignCancelled||toast(`Designed ${done} voice${done!==1?"s":""} \u2014 tagged "${tag}" + Rehearser`,"success")}catch(e){toast("Design all failed: "+((e==null?void 0:e.message)||e),"error")}finally{renderCastList(),populateNarratorSelect(),prog&&(prog.hidden=!0),btn.disabled=!1}});function rehWriteCharacterNote(sp,info){const c=rehState.cast[sp];if(!c||!info)return;info.gender&&!c.gender&&(c.gender=String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/,"N"));const bits=[];info.age&&bits.push(`Age: ${info.age}`),info.description&&bits.push(info.description);const note=bits.join(" \xB7 ");note&&!c.soul&&(c.soul=note),info.description&&!c.instruct&&(c.instruct=info.description)}async function rehResearchCast(speakers){var _a2,_b2,_c2,_d2;const names=speakers.filter(sp=>sp!==REH_NARRATOR_KEY);if(!names.length)return{};let chars=[];try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText(),names,llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});r.ok&&(chars=(await r.json()).characters||[])}catch{}const byName={};return chars.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)}),speakers.forEach(sp=>rehWriteCharacterNote(sp,byName[sp.toUpperCase().trim()])),byName}(_ya=$("reh-matchlib-btn"))==null||_ya.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2;const btn=$("reh-matchlib-btn"),speakers=Object.keys(rehState.cast).filter(sp=>rehState.cast[sp].voice!=="me");if(!speakers.length){toast("No characters to match","error");return}let lib=(window._voices||[]).filter(v=>v.enabled!==!1);if(!lib.length)try{lib=(await fetch("/api/voices").then(r=>r.json())).filter(v=>v.enabled!==!1)}catch{}if(!lib.length){toast("Your voice library is empty \u2014 clone, design or import some voices first","error");return}const candidates=lib.map(v=>({id:v.id,gender:v.gender||"",language:v.lang||"",tags:v.tag||"",description:(v.note||v.name||"").slice(0,140)})),nameFor=sp=>sp===REH_NARRATOR_KEY?"Narrator":sp,byDisplay={};speakers.forEach(sp=>{byDisplay[nameFor(sp).toUpperCase().trim()]=sp});const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Matching\u2026';try{const r=await fetch("/api/match-characters-voices",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText(),names:speakers.map(nameFor),voices:candidates,llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const assignments=(await r.json()).assignments||[],validIds=new Set(candidates.map(c=>c.id));let n=0;assignments.forEach(a=>{const sp=byDisplay[String(a.name||"").toUpperCase().trim()];sp&&a.voice_id&&validIds.has(a.voice_id)&&(rehState.cast[sp].voice=a.voice_id,rehState.cast[sp].voiceData=getVoiceData(a.voice_id),rehState.voices.includes(a.voice_id)||rehState.voices.push(a.voice_id),sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=a.voice_id),n++)}),btn.innerHTML=' Researching characters\u2026',await rehResearchCast(speakers),renderCastList(),populateNarratorSelect(),toast(n?`Matched ${n} character${n!==1?"s":""} & added notes`:"No good matches \u2014 try \u201CDesign all voices\u201D instead",n?"success":"error")}catch(e){toast("Match failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}});const REH_FISH_LANG={English:"en",German:"de",French:"fr",Spanish:"es",Italian:"it",Portuguese:"pt",Dutch:"nl",Auto:""};(_za=$("reh-matchonline-btn"))==null||_za.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2;const btn=$("reh-matchonline-btn"),speakers=Object.keys(rehState.cast).filter(sp=>rehState.cast[sp].voice!=="me"&&sp!==REH_NARRATOR_KEY);if(!speakers.length){toast("No characters to match","error");return}const lang=(_b2=REH_FISH_LANG[((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||"English"])!=null?_b2:"en",prog=$("reh-autodesign-progress"),fill=$("reh-autodesign-fill"),label=$("reh-autodesign-label"),setProg=(d,t,msg)=>{fill&&(fill.style.width=(t?d/t*100:0)+"%"),label&&(label.textContent=msg||`${d} / ${t}`)},orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Analyzing\u2026',prog&&(prog.hidden=!1),rehDesignCancelled=!1;try{let chars=[];try{const ar=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_c2=$("reh-script-text"))==null?void 0:_c2.value.trim())||linesToScriptText(),names:speakers,llm_url:((_d2=$("reh-llm-url"))==null?void 0:_d2.value.trim())||rehDefaultLlmUrl(),model:((_e2=$("reh-llm-model"))==null?void 0:_e2.value)||"",language:((_f2=$("reh-design-lang"))==null?void 0:_f2.value)||"English"})});ar.ok&&(chars=(await ar.json()).characters||[])}catch{}const byName={};chars.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)});const G={M:"male",F:"female",N:"neutral"};let done=0,n=0;for(let i=0;ir.json())).items||[]}catch{}let pick=items.find(v=>v.sample_audio),nameHit=!!pick;if(!pick){const fb=[`language=${lang}`,gender?`gender=${gender}`:"","page_size=12","sort_by=score",`page=${i%4+1}`].filter(Boolean);try{items=(await fetch("/api/fishaudio/voices?"+fb.join("&")).then(r=>r.json())).items||[]}catch{}pick=items.find(v=>v.sample_audio)}let cands=items.filter(v=>v.sample_audio).slice(0,6).map(v=>({title:v.title,sample_audio:v.sample_audio,image:v.image||"",gender:v.gender||"",language:v.language||lang||"",description:v.description||"",sample_text:v.sample_text||v.default_text||""}));if(!nameHit&&cands.length>1){const taken=new Set(Object.values(rehState.cast).map(c=>{var _a3,_b3,_c3;return(_c3=(_b3=(_a3=c.online)==null?void 0:_a3.candidates)==null?void 0:_b3[c.online.picked])==null?void 0:_c3.sample_audio}).filter(Boolean));cands=[...cands.slice(i%cands.length),...cands.slice(0,i%cands.length)].sort((a,b)=>(taken.has(a.sample_audio)?1:0)-(taken.has(b.sample_audio)?1:0))}if(cands.length)try{const vid=await _rehImportFishCandidate(sp,cands[0]);cands[0].voice_id=vid,rehState.cast[sp].voice=vid,rehState.cast[sp].voiceData=getVoiceData(vid),rehState.cast[sp].online={candidates:cands,picked:0},n++}catch(e){logErr("match-online import "+sp,e)}done++,setProg(done,speakers.length)}typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),renderCastList(),populateNarratorSelect(),toast(n?`Imported & matched ${n} online voice${n!==1?"s":""}`:"No online matches found",n?"success":"error")}catch(e){toast("Online match failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,prog&&(prog.hidden=!0)}});function rehMarkCastDesigning(sp,state,msg,info){var _a2;const row=document.querySelector(`.reh-cast-row[data-speaker="${CSS.escape(sp)}"]`);if(!row)return;let badge=row.querySelector(".reh-cast-design-badge");badge||(badge=document.createElement("span"),badge.className="reh-cast-design-badge",(_a2=row.querySelector("strong"))==null||_a2.after(badge)),badge.className="reh-cast-design-badge"+(state==="done"?" done":state==="err"?" err":""),badge.textContent=state==="designing"?"\u2728 designing\u2026":state==="done"?"\u2713 designed":"\u2717 failed",msg&&(badge.title=msg),row.classList.toggle("reh-cast-designing",state==="designing");const wrap=row.closest("div");let panel=wrap==null?void 0:wrap.querySelector(".reh-cast-design-panel");if(state==="designing"&&info){panel||(panel=document.createElement("div"),panel.className="reh-cast-design-panel",wrap.appendChild(panel));const genderIcon=info.gender==="M"?"\u2642":info.gender==="F"?"\u2640":"\u26A7",genderLabel=info.gender==="M"?"Male":info.gender==="F"?"Female":"Neutral";panel.innerHTML=` + `}).join("");const countEl=$("reh-cast-count");if(countEl){const shown=others.length,total=allOthers.length;countEl.textContent=shown===total?`${total} ${total===1?"character":"characters"} + narrator`:`${shown} of ${total} characters`}if(typeof _wireCharCards=="function"&&(_rehLibCharsCache||[]).length){const recsById=new Map(_rehLibCharsCache.map(r=>[r.id,r]));_wireCharCards(list,recsById,_rehLibCharsCache,renderCastList,{container:list,onBack:renderCastList})}_wireCastControls(),applyCastView();const card=el=>el.closest(".reh-cast-card"),spOf=el=>card(el).dataset.speaker;window.VoicePicker&&list.querySelectorAll(".reh-voice-sel[id]").forEach(sel=>{const cur=sel.value;VoicePicker.upgrade(sel.id),cur&&VoicePicker.setValue(sel.id,cur)}),list.querySelectorAll(".reh-me-check").forEach(cb=>cb.addEventListener("change",function(){var _a3;const sp=spOf(this);rehState.cast[sp].voice=this.checked?"me":((_a3=card(this).querySelector(".reh-voice-sel"))==null?void 0:_a3.value)||"",rehState.cast[sp].voiceData=this.checked?null:getVoiceData(rehState.cast[sp].voice),renderCastList()})),list.querySelectorAll(".reh-voice-sel").forEach(sel=>sel.addEventListener("change",function(){const sp=this.dataset.speaker;rehState.cast[sp].voice=this.value,rehState.cast[sp].voiceData=getVoiceData(this.value),delete rehState.cast[sp].online,sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=this.value),renderCastList()})),list.querySelectorAll(".reh-cast-instruct").forEach(inp=>inp.addEventListener("input",function(){rehState.cast[spOf(this)].instruct=this.value})),list.querySelectorAll(".reh-cc-lang").forEach(s=>s.addEventListener("change",function(){rehState.cast[spOf(this)].lang=this.value})),list.querySelectorAll(".reh-cc-gender").forEach(s=>s.addEventListener("change",function(){rehState.cast[spOf(this)].gender=this.value})),list.querySelectorAll(".reh-cc-tags").forEach(i=>i.addEventListener("input",function(){rehState.cast[spOf(this)].tags=this.value})),list.querySelectorAll(".reh-cc-soul-text").forEach(t=>t.addEventListener("input",function(){rehState.cast[spOf(this)].soul=this.value})),list.querySelectorAll(".reh-cc-develop").forEach(b=>b.addEventListener("click",function(e){e.preventDefault(),_castDevelop(spOf(this),this)})),list.querySelectorAll(".reh-cc-iconbtn").forEach(b=>b.addEventListener("click",function(){const sp=spOf(this),act=this.dataset.act,c=rehState.cast[sp];act==="ignore"?(c.ignored=!c.ignored,_castApplyToLines(sp,l=>l.ignored=c.ignored),renderCastList()):act==="hide"?(c.hidden=!c.hidden,_castApplyToLines(sp,l=>l.hidden=c.hidden),renderCastList()):act==="delete"&&_castDeleteCharacter(sp)})),list._voDelegated||(list._voDelegated=!0,list.addEventListener("click",e=>{var _a3,_b2,_c2,_d2,_e2,_f2;const sampleBtn=e.target.closest(".reh-cc-sample-btn");if(sampleBtn){e.preventDefault(),_rehPreviewCastLine(sampleBtn.dataset.speaker,sampleBtn);return}const playBtn=e.target.closest(".reh-vo-play");if(playBtn){e.preventDefault(),playBtn.dataset.voice?_rehPreviewLocal(playBtn.dataset.voice,playBtn):_rehAudition(playBtn.dataset.url,playBtn);return}const toggle=e.target.closest(".reh-vo-toggle");if(toggle){e.preventDefault();const alts=(_a3=toggle.closest(".reh-cc-online"))==null?void 0:_a3.querySelector(".reh-vo-alts");alts&&(alts.hidden=!alts.hidden,toggle.textContent=toggle.textContent.replace(/[▾▴]\s*$/,"")+(alts.hidden?"\u25BE":"\u25B4"));return}const tool=e.target.closest(".reh-vo-tool");if(tool){e.preventDefault();const panel=tool.closest(".reh-cc-online"),want=tool.dataset.tool,localP=panel.querySelector(".reh-vo-local-panel"),searchP=panel.querySelector(".reh-vo-search-panel"),showLocal=want==="local"&&((_b2=localP==null?void 0:localP.hidden)!=null?_b2:!0),showSearch=want==="search"&&((_c2=searchP==null?void 0:searchP.hidden)!=null?_c2:!0);if(localP&&(localP.hidden=!showLocal),searchP&&(searchP.hidden=!showSearch),panel.querySelectorAll(".reh-vo-tool").forEach(t=>t.classList.toggle("active",t.dataset.tool==="local"&&showLocal||t.dataset.tool==="search"&&showSearch)),showLocal){const card2=e.target.closest(".reh-cast-card");_rehRenderLocalResults(card2,spOf(tool),""),(_d2=card2.querySelector(".reh-vo-local-input"))==null||_d2.focus()}showSearch&&((_e2=panel.querySelector(".reh-vo-search-input"))==null||_e2.focus());return}const goBtn=e.target.closest(".reh-vo-search-go");if(goBtn){e.preventDefault();const card2=e.target.closest(".reh-cast-card");_rehSearchOnline(card2,spOf(goBtn),(_f2=card2.querySelector(".reh-vo-search-input"))==null?void 0:_f2.value);return}const use=e.target.closest(".reh-vo-use");if(use){e.preventDefault();const sp=spOf(use),act=use.dataset.act;act==="local"?_rehAssignLocal(sp,use.dataset.voice):act==="search"?_rehUseSearchResult(sp,parseInt(use.dataset.idx,10),use):_rehUseCandidate(sp,parseInt(use.dataset.idx,10),use)}}),list.addEventListener("input",e=>{const li=e.target.closest(".reh-vo-local-input");li&&_rehRenderLocalResults(e.target.closest(".reh-cast-card"),spOf(li),li.value)}),list.addEventListener("keydown",e=>{const si=e.target.closest(".reh-vo-search-input");si&&e.key==="Enter"&&(e.preventDefault(),_rehSearchOnline(e.target.closest(".reh-cast-card"),spOf(si),si.value))}))}function applyCastView(){const list=$("reh-cast-list");if(!list)return;const view=rehState.castView==="list"?"list":"card";list.classList.toggle("reh-cast-view-card",view==="card"),list.classList.toggle("reh-cast-view-list",view==="list"),document.querySelectorAll("#reh-cast-view-toggle .reh-view-btn").forEach(b=>b.classList.toggle("active",b.dataset.view===view))}try{rehState.castView=localStorage.getItem("reh-cast-view")||"card"}catch{rehState.castView="card"}rehState.castFilter={search:"",gender:"",lang:""};try{rehState.castSort=JSON.parse(localStorage.getItem("reh-cast-sort"))||{by:"name",dir:"asc"}}catch{rehState.castSort={by:"name",dir:"asc"}}function _wireCastControls(){const toggle=$("reh-cast-view-toggle");if(!toggle||toggle._wired)return;toggle._wired=!0,toggle.querySelectorAll(".reh-view-btn").forEach(b=>b.addEventListener("click",()=>{rehState.castView=b.dataset.view;try{localStorage.setItem("reh-cast-view",b.dataset.view)}catch{}applyCastView()}));const search=$("reh-cast-search"),fg=$("reh-cast-filter-gender"),fl=$("reh-cast-filter-lang"),sortSel=$("reh-cast-sort"),dirBtn=$("reh-cast-sort-dir"),setDirIcon=()=>{dirBtn&&(dirBtn.dataset.dir=rehState.castSort.dir,dirBtn.querySelector(".mdi").className="mdi mdi-sort-"+(rehState.castSort.dir==="desc"?"descending":"ascending"))},persist=()=>{try{localStorage.setItem("reh-cast-sort",JSON.stringify(rehState.castSort))}catch{}};sortSel&&(sortSel.value=rehState.castSort.by),setDirIcon(),search==null||search.addEventListener("input",()=>{rehState.castFilter.search=search.value.trim(),renderCastList(),search.focus()}),fg==null||fg.addEventListener("change",()=>{rehState.castFilter.gender=fg.value,renderCastList()}),fl==null||fl.addEventListener("change",()=>{rehState.castFilter.lang=fl.value,renderCastList()}),sortSel==null||sortSel.addEventListener("change",()=>{rehState.castSort.by=sortSel.value,rehState.castSort.dir=sortSel.value==="lines"?"desc":"asc",setDirIcon(),persist(),renderCastList()}),dirBtn==null||dirBtn.addEventListener("click",()=>{rehState.castSort.dir=rehState.castSort.dir==="desc"?"asc":"desc",setDirIcon(),persist(),renderCastList()})}function _castSortFilter(speakers,lineCount){const f=rehState.castFilter||{},s=rehState.castSort||{by:"name",dir:"asc"},out=speakers.filter(sp=>{const c=rehState.cast[sp]||{};if(f.search){const q=f.search.toLowerCase();if(!sp.toLowerCase().includes(q)&&!(c.tags||"").toLowerCase().includes(q))return!1}return!(f.gender&&(c.gender||"")!==f.gender||f.lang&&(c.lang||"")!==f.lang)}),byName=(a,b)=>a.localeCompare(b,void 0,{sensitivity:"base"}),cmp={name:byName,gender:(a,b)=>(rehState.cast[a].gender||"~").localeCompare(rehState.cast[b].gender||"~")||byName(a,b),lang:(a,b)=>(rehState.cast[a].lang||"~").localeCompare(rehState.cast[b].lang||"~")||byName(a,b),lines:(a,b)=>lineCount(a)-lineCount(b)||byName(a,b),tag:(a,b)=>(rehState.cast[a].tags||"~").localeCompare(rehState.cast[b].tags||"~")||byName(a,b)}[s.by]||byName;return out.sort(cmp),s.dir==="desc"&&out.reverse(),out}function _castDeleteCharacter(sp){const n=rehState.lines.filter(l=>l.speaker===sp&&l.type==="dialog").length;if(confirm(`Delete \u201C${sp}\u201D and their ${n} line${n!==1?"s":""}? This cannot be undone.`)){for(let i=rehState.lines.length-1;i>=0;i--)rehState.lines[i].speaker===sp&&rehState.lines[i].type==="dialog"&&(rehState.lines.splice(i,1),_reindexLineState(i));delete rehState.cast[sp],renderCastList(),rehState.lines.length&&buildScriptPage(),toast(`Removed ${sp}`,"success")}}async function _castDevelop(sp,btn){var _a2,_b2,_c2,_d2,_e2;const script=((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText();if(!script){toast("Load a script first","error");return}const c=rehState.cast[sp],orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Developing\u2026';try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script,names:[sp],llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:c.lang||((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const info=((_e2=(await r.json()).characters)==null?void 0:_e2[0])||{};info.gender&&(c.gender=String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/,"N")),info.description&&(c.soul=info.description,c.instruct=c.instruct||info.description),renderCastList(),toast(`Developed ${sp}`,"success")}catch(e){btn.disabled=!1,btn.innerHTML=orig,toast("Develop failed: "+e.message,"error")}}async function refreshRehBackends(){rehInitLlmField();const sel=$("reh-backend-select");if(!sel)return;let backends=typeof availableTtsBackends=="function"?availableTtsBackends():[];!backends.length&&typeof refreshTtsBackendAvailability=="function"&&(await refreshTtsBackendAvailability().catch(()=>{}),backends=typeof availableTtsBackends=="function"?availableTtsBackends():[]),!backends.length&&typeof _ttsBackends!="undefined"&&Array.isArray(_ttsBackends)&&(backends=_ttsBackends);const prev=sel.value||rehState.backend;if(sel.innerHTML=backends.length?backends.map(b=>``).join(""):'',prev&&[...sel.options].some(o=>o.value===prev))sel.value=prev;else{const pick=["fishspeech","voice_clone","customvoice","voice_design"].find(id=>[...sel.options].some(o=>o.value===id));pick&&(sel.value=pick)}rehState.backend=sel.value||"",_checkToneStyleSupport()}(window._ttsRefreshHooks=window._ttsRefreshHooks||[]).push(()=>{const sel=$("reh-backend-select");if(!sel)return;const backends=typeof availableTtsBackends=="function"?availableTtsBackends():[];if(!backends.length)return;const prev=sel.value||rehState.backend;sel.innerHTML=backends.map(b=>``).join(""),prev&&[...sel.options].some(o=>o.value===prev)?sel.value=prev:sel.options.length&&(sel.value=sel.options[0].value),rehState.backend=sel.value||""});function _rehAllVoiceIds(include){const ids=new Set((rehState.voices||[]).filter(id=>_rehVoiceVisibleId(id,include)));return(window._voices||[]).forEach(v=>{v&&v.id&&(v.enabled!==!1||v.id===include)&&ids.add(v.id)}),include&&ids.add(include),[...ids].sort((a,b)=>a.localeCompare(b,void 0,{sensitivity:"base"}))}function populateNarratorSelect(){const sel=$("reh-narrator-voice");if(!sel)return;const cur=rehState.narratorVoice||sel.value;sel.innerHTML=''+_rehAllVoiceIds(cur).map(v=>``).join("")}(_sa=$("reh-fetch-voices-btn"))==null||_sa.addEventListener("click",async()=>{var _a2;const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a backend first","error");return}$("reh-fetch-voices-btn").disabled=!0;try{(!window._voices||!window._voices.length)&&typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{});const raw=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());rehState.voices=(Array.isArray(raw)?raw.map(v=>typeof v=="string"?v:v.id||String(v)):[]).filter(id=>_rehVoiceVisibleId(id)),renderCastList(),populateNarratorSelect(),toast("Fetched "+rehState.voices.length+" voices","success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("reh-fetch-voices-btn").disabled=!1}}),(_ta=$("reh-narrator-voice"))==null||_ta.addEventListener("change",function(){rehState.narratorVoice=this.value}),(_ua=$("reh-back-1-btn"))==null||_ua.addEventListener("click",()=>showPhase(1));const _BUILD_INSTRUCT_TEMPLATES={DE:e=>`Sprich in einem ${e} Tonfall.`,EN:e=>`Speak in a ${e} manner.`},_BUILD_INSTRUCT_LANG_NAMES={DE:"German",FR:"French",ES:"Spanish",IT:"Italian",PT:"Portuguese",NL:"Dutch",PL:"Polish"};function _buildAccentClause(langCode){const langName=_BUILD_INSTRUCT_LANG_NAMES[langCode];return langName?`Speak with an authentic native ${langName} accent \u2014 not American-accented, not an English speaker doing ${langName}.`:langCode==="EN"?"English with a neutral British or international accent, explicitly not American/US-accented.":""}function _buildInstruct(voiceProfile,emotion,voiceId){const p=(voiceProfile||"").trim(),e=(emotion||"").trim(),langCode=String(voiceId||"").split("_")[0].toUpperCase(),accent=_buildAccentClause(langCode);if(!e&&!p&&!accent)return"";const effectiveEmotion=(e&&langCode!=="EN"&&typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(e):"")||e,emotionTmpl=_BUILD_INSTRUCT_TEMPLATES.EN;return(e?[emotionTmpl(effectiveEmotion),p].filter(Boolean):[p,accent].filter(Boolean)).join(" ")}function _rehBackendIsFish(){var _a2;let id="";try{id=typeof backendById=="function"&&((_a2=backendById(rehState.backend))==null?void 0:_a2.id)||rehState.backend||""}catch{id=rehState.backend||""}return/fish/i.test(id)}const _REH_EMOTION_DE_EN={w\u00FCtend:"angry",zornig:"angry",erz\u00FCrnt:"angry",ver\u00E4rgert:"annoyed",gereizt:"irritated",traurig:"sad",melancholisch:"melancholic",betr\u00FCbt:"sad",niedergeschlagen:"dejected",\u00E4ngstlich:"scared",furchtsam:"fearful",ver\u00E4ngstigt:"frightened",panisch:"panicked",nerv\u00F6s:"nervous",fr\u00F6hlich:"happy",gl\u00FCcklich:"happy",freudig:"joyful",heiter:"cheerful",vergn\u00FCgt:"delighted",fl\u00FCsternd:"whispering",leise:"quiet",ged\u00E4mpft:"hushed",aufgeregt:"excited",begeistert:"enthusiastic",euphorisch:"euphoric",\u00FCberrascht:"surprised",erstaunt:"astonished",verbl\u00FCfft:"amazed",genervt:"annoyed",frustriert:"frustrated",verzweifelt:"desperate",hoffnungslos:"hopeless",resigniert:"resigned",entschlossen:"determined",entschieden:"decisive",selbstbewusst:"confident",stolz:"proud",arrogant:"arrogant",sch\u00FCchtern:"shy",verlegen:"embarrassed",unsicher:"uncertain",ironisch:"sarcastic",sarkastisch:"sarcastic",sp\u00F6ttisch:"mocking",h\u00F6hnisch:"scornful",ver\u00E4chtlich:"contemptuous",ernst:"serious",streng:"stern",autorit\u00E4r:"authoritative",befehlend:"commanding",sanft:"gentle",z\u00E4rtlich:"tender",liebevoll:"loving",warm:"warm",kalt:"cold",distanziert:"distant",gleichg\u00FCltig:"indifferent",gelangweilt:"bored",geheimnisvoll:"mysterious",unheimlich:"eerie",d\u00FCster:"ominous",bedrohlich:"threatening",dramatisch:"dramatic",theatralisch:"theatrical",pathetisch:"melodramatic",ruhig:"calm",gelassen:"composed",besonnen:"measured",schockiert:"shocked",entsetzt:"horrified",fassungslos:"stunned",z\u00F6gernd:"hesitant",verwirrt:"confused",ratlos:"bewildered",unschl\u00FCssig:"undecided",weinend:"tearful",schluchzend:"sobbing",trauernd:"grieving",gebrochen:"broken",schroff:"curt",barsch:"gruff",grob:"rough",abweisend:"dismissive",freundlich:"friendly",herzlich:"warm",einladend:"welcoming",spielerisch:"playful",neckend:"teasing",frech:"cheeky",schelmisch:"mischievous",romantisch:"romantic",sehns\u00FCchtig:"longing",verliebt:"infatuated",triumphierend:"triumphant",siegessicher:"victorious",erleichtert:"relieved",beruhigt:"reassured",schuldbewusst:"guilty",reum\u00FCtig:"remorseful",neugierig:"curious",interessiert:"interested",m\u00FCde:"weary",ersch\u00F6pft:"exhausted",wehm\u00FCtig:"wistful",nostalgisch:"nostalgic",bemerkend:"remarking",feststellend:"noting",sachlich:"matter-of-fact",n\u00FCchtern:"plain",flehend:"pleading",bittend:"imploring",warnend:"warning",mahnend:"admonishing",trotzig:"defiant",rebellisch:"rebellious",erschrocken:"startled",verst\u00F6rt:"disturbed"};function _rehEmotionEnglishTag(emotion){const raw=(emotion||"").trim();if(!raw)return"";const lower=raw.split(/[,;]\s*/)[0].trim().toLowerCase();if(_REH_EMOTION_DE_EN[lower])return _REH_EMOTION_DE_EN[lower];const stem=lower.replace(/(e|er|es|en|em)$/,"");if(stem.length>=4){for(const key in _REH_EMOTION_DE_EN)if(key.startsWith(stem))return _REH_EMOTION_DE_EN[key]}return/^[a-z\- ]+$/.test(lower)?lower:""}function _rehInlineTone(text,emotion){if(!_rehBackendIsFish()||/^\s*\[/.test(text))return text;const tag=_rehEmotionEnglishTag(emotion);return tag?`[${tag}] ${text}`:text}const REH_LANG_CODE={English:"EN",German:"DE",French:"FR",Spanish:"ES",Italian:"IT",Auto:"EN"};function rehDefaultLlmUrl(){try{if(typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url)return _appSettings.llm_url}catch{}return"http://localhost:11434/v1"}function rehCollectLlmEndpoints(){const seen=new Set,results=[],add=(url,label)=>{url&&(url=url.trim(),!(!url||seen.has(url))&&(seen.add(url),results.push({url,label:label||url})))};return add(rehDefaultLlmUrl(),"Active LLM"),document.querySelectorAll(".llm-local-url-inp, [data-llm-local-key]").forEach(inp=>{var _a2,_b2,_c2;const v=(_a2=inp.value)==null?void 0:_a2.trim(),def=inp.dataset.llmLocalDefault,key=inp.dataset.llmLocalKey||inp.dataset.dcUrlKey||"",card=inp.closest('[class*="llm-local-card"], [class*="llm-local"]'),name=((_c2=(_b2=card==null?void 0:card.querySelector(".llm-local-name"))==null?void 0:_b2.textContent)==null?void 0:_c2.trim())||key;add(v||def,name)}),document.querySelectorAll(".dc-url-inp").forEach(inp=>{var _a2,_b2,_c2;const card=inp.closest('[class*="llm-local-card"]');if(!card)return;const name=((_b2=(_a2=card.querySelector(".llm-local-name"))==null?void 0:_a2.textContent)==null?void 0:_b2.trim())||"";add(((_c2=inp.value)==null?void 0:_c2.trim())||inp.dataset.dcDefault,name)}),[["http://localhost:11434/v1","Ollama"],["http://localhost:8000/v1","vLLM"],["http://localhost:1234/v1","LM Studio"],["http://localhost:28080/v1","llama-swap"],["http://localhost:14000/v1","LiteLLM"]].forEach(([u,l])=>add(u,l)),results}function rehInitLlmField(){const u=$("reh-llm-url");if(!u)return;u.value||(u.value=rehDefaultLlmUrl());const dl=$("reh-llm-url-list");dl&&(dl.innerHTML=rehCollectLlmEndpoints().map(e=>``).join(""))}(_va=$("reh-llm-refresh"))==null||_va.addEventListener("click",async()=>{var _a2;const url=((_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim())||rehDefaultLlmUrl(),sel=$("reh-llm-model");if(sel){sel.innerHTML='';try{const models=(await(await fetch("/api/conversation/llm-models?url="+encodeURIComponent(url))).json()).models||[];sel.innerHTML=''+models.map(m=>``).join("");const want=typeof _appSettings!="undefined"&&_appSettings?_appSettings.llm_model:"";want&&models.includes(want)&&(sel.value=want),toast(models.length?`Found ${models.length} models`:"No models found",models.length?"success":"error")}catch(e){sel.innerHTML='',toast("Could not list models: "+e.message,"error")}}});const REH_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"};function _pickVoiceAvatar(gender,desc,speaker){const d=((desc||"")+" "+(speaker||"")).toLowerCase();return/\b(robot|android|synthetic|artificial|computer|machine|cyborg|a\.?i\.?|operating system|\bos\b|digital|hologram|drone)\b/.test(d)?"robot":/\b(animal|creature|beast|dragon|monster|cat|dog|wolf|lion|bird|horse|dino|dinosaur|alien)\b/.test(d)?"animal":gender==="M"?"male":gender==="F"?"female":"neutral"}let rehDesignCancelled=!1;(_wa=$("reh-autodesign-cancel"))==null||_wa.addEventListener("click",()=>{rehDesignCancelled=!0}),(_xa=$("reh-autodesign-btn"))==null||_xa.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2;if(!((_a2=$("reh-backend-select"))==null?void 0:_a2.value)){toast("Select a TTS backend first","error");return}const speakers=Object.keys(rehState.cast);if(!speakers.length){toast("No characters to design for","error");return}const script=((_b2=$("reh-script-text"))==null?void 0:_b2.value.trim())||linesToScriptText(),llmUrl=((_c2=$("reh-llm-url"))==null?void 0:_c2.value.trim())||rehDefaultLlmUrl(),llmModel=((_d2=$("reh-llm-model"))==null?void 0:_d2.value)||"",language=((_e2=$("reh-design-lang"))==null?void 0:_e2.value)||"English",langCode=REH_LANG_CODE[language]||"EN",scriptTitle=((_f2=$("reh-script-title"))==null?void 0:_f2.value.trim())||"Script",tag=(typeof _umlautSafe=="function"?_umlautSafe(scriptTitle):scriptTitle).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,24)||"Script",btn=$("reh-autodesign-btn"),prog=$("reh-autodesign-progress"),fill=$("reh-autodesign-fill"),label=$("reh-autodesign-label");btn.disabled=!0,rehDesignCancelled=!1,prog&&(prog.hidden=!1);const setProg=(d,t,msg)=>{fill&&(fill.style.width=(t?d/t*100:0)+"%"),label&&(label.textContent=msg||`${d} / ${t}`)};setProg(0,speakers.length,"Analyzing script with LLM\u2026");let characters;try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script,names:speakers,llm_url:llmUrl,model:llmModel,language})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}characters=(await r.json()).characters||[]}catch(e){toast("Character analysis failed: "+e.message,"error"),btn.disabled=!1,prog&&(prog.hidden=!0);return}const byName={};characters.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)});let done=0;try{const designOnly=speakers.filter(sp=>{var _a3;return((_a3=rehState.cast[sp])==null?void 0:_a3.voice)!=="me"});for(const sp of designOnly){if(rehDesignCancelled){toast("Cancelled","error");break}if(!rehState.cast[sp]){done++;continue}const info=byName[sp.toUpperCase().trim()]||{},gender=(info.gender||"N").toUpperCase().charAt(0).replace(/[^MFN]/,"N")||"N",desc=info.description||`A ${info.age||"adult"} ${gender==="M"?"male":gender==="F"?"female":""} character named ${sp}, natural expressive voice.`,sampleLine=((_g2=rehState.lines.find(l=>l.type==="dialog"&&l.speaker===sp))==null?void 0:_g2.text)||`Hello, I am ${sp}.`,safeName=(typeof _umlautSafe=="function"?_umlautSafe(sp):sp).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,24)||"Char",voiceId=`${langCode}_${gender}_${safeName}_${tag}`.slice(0,90);rehMarkCastDesigning(sp,"designing",null,{gender,language,voiceId,desc,age:info.age||"",step:"Generating voice audio\u2026"}),setProg(done,designOnly.length,`Designing ${sp}\u2026 (${done+1}/${designOnly.length})`);try{const dr=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({instruct:desc,sample_text:stripMarkdown(sampleLine).slice(0,300),language,gender,dialogue:!1})});if(!dr.ok){const e=await dr.json().catch(()=>({}));throw new Error(e.detail||dr.statusText)}const dd=await dr.json(),sr=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:dd.id,voice_id:voiceId,transcript:stripMarkdown(sampleLine).slice(0,300)})});if(!sr.ok){const e=await sr.json().catch(()=>({}));throw new Error(e.detail||sr.statusText)}const saved=await sr.json(),charName=sp===REH_NARRATOR_KEY?"Narrator":sp;typeof saveMeta=="function"&&await saveMeta(saved.voice_id,{gender,name:charName,avatar:_pickVoiceAvatar(gender,desc,sp),origin:"designed",group:`Rehearser: ${scriptTitle}`,note:`Rehearser \xB7 ${scriptTitle} \xB7 ${charName} \u2014 ${desc.slice(0,180)}`,transcript:stripMarkdown(sampleLine).slice(0,300),tag:scriptTitle}).catch(()=>{}),rehState.cast[sp].voice=saved.voice_id,rehState.cast[sp].instruct=rehState.cast[sp].instruct||desc,rehState.cast[sp].soul=rehState.cast[sp].soul||[info.age?`Age: ${info.age}`:"",desc].filter(Boolean).join(" \xB7 "),rehState.cast[sp].voiceData=null,rehState.voices.includes(saved.voice_id)||rehState.voices.push(saved.voice_id),rehMarkCastDesigning(sp,"done")}catch(e){rehMarkCastDesigning(sp,"err",e.message)}done++,setProg(done,designOnly.length)}typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),rehDesignCancelled||toast(`Designed ${done} voice${done!==1?"s":""} \u2014 tagged "${tag}" + Rehearser`,"success")}catch(e){toast("Design all failed: "+((e==null?void 0:e.message)||e),"error")}finally{renderCastList(),populateNarratorSelect(),prog&&(prog.hidden=!0),btn.disabled=!1}});function rehWriteCharacterNote(sp,info){const c=rehState.cast[sp];if(!c||!info)return;info.gender&&!c.gender&&(c.gender=String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/,"N"));const bits=[];info.age&&bits.push(`Age: ${info.age}`),info.description&&bits.push(info.description);const note=bits.join(" \xB7 ");note&&!c.soul&&(c.soul=note),info.description&&!c.instruct&&(c.instruct=info.description)}async function rehResearchCast(speakers){var _a2,_b2,_c2,_d2;const names=speakers.filter(sp=>sp!==REH_NARRATOR_KEY);if(!names.length)return{};let chars=[];try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText(),names,llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});r.ok&&(chars=(await r.json()).characters||[])}catch{}const byName={};return chars.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)}),speakers.forEach(sp=>rehWriteCharacterNote(sp,byName[sp.toUpperCase().trim()])),byName}(_ya=$("reh-matchlib-btn"))==null||_ya.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2;const btn=$("reh-matchlib-btn"),speakers=Object.keys(rehState.cast).filter(sp=>rehState.cast[sp].voice!=="me");if(!speakers.length){toast("No characters to match","error");return}let lib=(window._voices||[]).filter(v=>v.enabled!==!1);if(!lib.length)try{lib=(await fetch("/api/voices").then(r=>r.json())).filter(v=>v.enabled!==!1)}catch{}if(!lib.length){toast("Your voice library is empty \u2014 clone, design or import some voices first","error");return}const candidates=lib.map(v=>({id:v.id,gender:v.gender||"",language:v.lang||"",tags:v.tag||"",description:(v.note||v.name||"").slice(0,140)})),nameFor=sp=>sp===REH_NARRATOR_KEY?"Narrator":sp,byDisplay={};speakers.forEach(sp=>{byDisplay[nameFor(sp).toUpperCase().trim()]=sp});const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Matching\u2026';try{const r=await fetch("/api/match-characters-voices",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText(),names:speakers.map(nameFor),voices:candidates,llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const assignments=(await r.json()).assignments||[],validIds=new Set(candidates.map(c=>c.id));let n=0;assignments.forEach(a=>{const sp=byDisplay[String(a.name||"").toUpperCase().trim()];sp&&a.voice_id&&validIds.has(a.voice_id)&&(rehState.cast[sp].voice=a.voice_id,rehState.cast[sp].voiceData=getVoiceData(a.voice_id),rehState.voices.includes(a.voice_id)||rehState.voices.push(a.voice_id),sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=a.voice_id),n++)}),btn.innerHTML=' Researching characters\u2026',await rehResearchCast(speakers),renderCastList(),populateNarratorSelect(),toast(n?`Matched ${n} character${n!==1?"s":""} & added notes`:"No good matches \u2014 try \u201CDesign all voices\u201D instead",n?"success":"error")}catch(e){toast("Match failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}});const REH_FISH_LANG={English:"en",German:"de",French:"fr",Spanish:"es",Italian:"it",Portuguese:"pt",Dutch:"nl",Auto:""};(_za=$("reh-matchonline-btn"))==null||_za.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2;const btn=$("reh-matchonline-btn"),speakers=Object.keys(rehState.cast).filter(sp=>rehState.cast[sp].voice!=="me"&&sp!==REH_NARRATOR_KEY);if(!speakers.length){toast("No characters to match","error");return}const lang=(_b2=REH_FISH_LANG[((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||"English"])!=null?_b2:"en",prog=$("reh-autodesign-progress"),fill=$("reh-autodesign-fill"),label=$("reh-autodesign-label"),setProg=(d,t,msg)=>{fill&&(fill.style.width=(t?d/t*100:0)+"%"),label&&(label.textContent=msg||`${d} / ${t}`)},orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Analyzing\u2026',prog&&(prog.hidden=!1),rehDesignCancelled=!1;try{let chars=[];try{const ar=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_c2=$("reh-script-text"))==null?void 0:_c2.value.trim())||linesToScriptText(),names:speakers,llm_url:((_d2=$("reh-llm-url"))==null?void 0:_d2.value.trim())||rehDefaultLlmUrl(),model:((_e2=$("reh-llm-model"))==null?void 0:_e2.value)||"",language:((_f2=$("reh-design-lang"))==null?void 0:_f2.value)||"English"})});ar.ok&&(chars=(await ar.json()).characters||[])}catch{}const byName={};chars.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)});const G={M:"male",F:"female",N:"neutral"};let done=0,n=0;for(let i=0;ir.json())).items||[]}catch{}let pick=items.find(v=>v.sample_audio),nameHit=!!pick;if(!pick){const fb=[`language=${lang}`,gender?`gender=${gender}`:"","page_size=12","sort_by=score",`page=${i%4+1}`].filter(Boolean);try{items=(await fetch("/api/fishaudio/voices?"+fb.join("&")).then(r=>r.json())).items||[]}catch{}pick=items.find(v=>v.sample_audio)}let cands=items.filter(v=>v.sample_audio).slice(0,6).map(v=>({title:v.title,sample_audio:v.sample_audio,image:v.image||"",gender:v.gender||"",language:v.language||lang||"",description:v.description||"",sample_text:v.sample_text||v.default_text||""}));if(!nameHit&&cands.length>1){const taken=new Set(Object.values(rehState.cast).map(c=>{var _a3,_b3,_c3;return(_c3=(_b3=(_a3=c.online)==null?void 0:_a3.candidates)==null?void 0:_b3[c.online.picked])==null?void 0:_c3.sample_audio}).filter(Boolean));cands=[...cands.slice(i%cands.length),...cands.slice(0,i%cands.length)].sort((a,b)=>(taken.has(a.sample_audio)?1:0)-(taken.has(b.sample_audio)?1:0))}if(cands.length)try{const vid=await _rehImportFishCandidate(sp,cands[0]);cands[0].voice_id=vid,rehState.cast[sp].voice=vid,rehState.cast[sp].voiceData=getVoiceData(vid),rehState.cast[sp].online={candidates:cands,picked:0},n++}catch(e){logErr("match-online import "+sp,e)}done++,setProg(done,speakers.length)}typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),renderCastList(),populateNarratorSelect(),toast(n?`Imported & matched ${n} online voice${n!==1?"s":""}`:"No online matches found",n?"success":"error")}catch(e){toast("Online match failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,prog&&(prog.hidden=!0)}});function rehMarkCastDesigning(sp,state,msg,info){var _a2;const row=document.querySelector(`.reh-cast-row[data-speaker="${CSS.escape(sp)}"]`);if(!row)return;let badge=row.querySelector(".reh-cast-design-badge");badge||(badge=document.createElement("span"),badge.className="reh-cast-design-badge",(_a2=row.querySelector("strong"))==null||_a2.after(badge)),badge.className="reh-cast-design-badge"+(state==="done"?" done":state==="err"?" err":""),badge.textContent=state==="designing"?"\u2728 designing\u2026":state==="done"?"\u2713 designed":"\u2717 failed",msg&&(badge.title=msg),row.classList.toggle("reh-cast-designing",state==="designing");const wrap=row.closest("div");let panel=wrap==null?void 0:wrap.querySelector(".reh-cast-design-panel");if(state==="designing"&&info){panel||(panel=document.createElement("div"),panel.className="reh-cast-design-panel",wrap.appendChild(panel));const genderIcon=info.gender==="M"?"\u2642":info.gender==="F"?"\u2640":"\u26A7",genderLabel=info.gender==="M"?"Male":info.gender==="F"?"Female":"Neutral";panel.innerHTML=`
${genderIcon} ${escHtml(genderLabel)} ${escHtml(info.language||"EN")} diff --git a/static/index.html b/static/index.html index 90518fb..6c93981 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/rehearser.js b/static/js/rehearser.js index 916cb77..1669a74 100644 --- a/static/js/rehearser.js +++ b/static/js/rehearser.js @@ -1903,7 +1903,21 @@ function _buildInstruct(voiceProfile, emotion, voiceId) { // through. Emotion is the one thing that MUST vary per line; accent is a // constant reminder the voice's own identity should mostly already carry, // so it goes last, not first. - const parts = [e ? emotionTmpl(effectiveEmotion) : '', p, accent].filter(Boolean); + // ...and when an emotion IS present, the accent clause is dropped entirely rather + // than merely demoted. Measured on the deterministic voice_clone backend (identical + // input reproduces bit-identical audio, so these differences are real, not sampling + // noise): with an emotion-only instruct the tones separate cleanly and in the right + // order — whisper 206Hz, sad 170Hz, neutral 225Hz, happy 234Hz, angry 233Hz, scared + // 239Hz, with sad also slower and quieter. Appending "Speak with an authentic native + // German accent." to those same instructs collapsed the whole range to 222-250Hz and + // erased sad almost completely. Two style directives in one instruct compete, and the + // trailing one wins — which is exactly the failure Qwen3-TTS's own prompting guidance + // warns about for conflicting attributes. A cloned voice already carries its accent + // from the reference WAV, so accent reinforcement is the cheaper of the two to lose; + // lines with no emotion still get it. + const parts = e + ? [emotionTmpl(effectiveEmotion), p].filter(Boolean) + : [p, accent].filter(Boolean); return parts.join(' '); } diff --git a/static/js/tts-preview.js b/static/js/tts-preview.js index 798d736..4f270cf 100644 --- a/static/js/tts-preview.js +++ b/static/js/tts-preview.js @@ -213,6 +213,85 @@ $('fetch-tts-voices-btn').addEventListener('click', async () => { // while style-aware backends (VoiceDesign/CustomVoice) keep working through // the instruct field exactly as before. function _ttsIsFishBackend(id) { return /fish/i.test(id || ''); } + +// ── Fish Audio inline [tag] reference ─────────────────────────────────────── +// Fish is the only backend here that reads bracket tags straight out of the text +// (every other engine takes a separate instruct field and would speak "[happy]" +// aloud as literal words), so this panel only appears for Fish backends. +// Tag list per Fish Audio's emotion documentation: 24 basic + 25 advanced. +const FISH_EMOTION_TAGS = { + 'Basic emotions': [ + ['happy','Cheerful, upbeat'], ['sad','Melancholic, downcast'], ['angry','Frustrated, aggressive'], + ['excited','Energetic, enthusiastic'], ['calm','Peaceful, relaxed'], ['nervous','Anxious, uncertain'], + ['confident','Assertive, self-assured'], ['surprised','Shocked, amazed'], ['satisfied','Content, pleased'], + ['delighted','Very pleased, joyful'], ['scared','Frightened, fearful'], ['worried','Concerned, troubled'], + ['upset','Disturbed, distressed'], ['frustrated','Annoyed, exasperated'], ['depressed','Very sad, hopeless'], + ['empathetic','Understanding, caring'], ['embarrassed','Ashamed, awkward'], ['disgusted','Repelled, revolted'], + ['moved','Emotionally touched'], ['proud','Accomplished, satisfied'], ['relaxed','At ease, casual'], + ['grateful','Thankful, appreciative'], ['curious','Inquisitive, interested'], ['sarcastic','Ironic, mocking'], + ], + 'Advanced emotions': [ + ['disdainful','Contemptuous, scornful'], ['unhappy','Discontent, dissatisfied'], ['anxious','Very worried, uneasy'], + ['hysterical','Uncontrollably emotional'], ['indifferent','Uncaring, neutral'], ['uncertain','Doubtful, unsure'], + ['doubtful','Skeptical, questioning'], ['confused','Puzzled, perplexed'], ['disappointed','Let down'], + ['regretful','Sorry, remorseful'], ['guilty','Culpable, responsible'], ['ashamed','Deeply embarrassed'], + ['jealous','Envious, resentful'], ['envious','Wanting what others have'], ['hopeful','Optimistic about future'], + ['optimistic','Positive outlook'], ['pessimistic','Negative outlook'], ['nostalgic','Longing for the past'], + ['lonely','Isolated, alone'], ['bored','Uninterested, weary'], ['contemptuous','Showing contempt'], + ['sympathetic','Showing sympathy'], ['compassionate','Showing deep care'], ['determined','Resolved, decided'], + ['resigned','Accepting defeat'], + ], +}; +const FISH_TAG_SAMPLE_TEXT = `[happy] I got the promotion! +[uncertain] But... it means relocating. +[sad] I'll miss everyone here. +[hopeful] Though it's a great opportunity. +[determined] I'm going to make it work!`; + +function _fishInsertTag(tag) { + const ta = $('preview-text-area'); + if (!ta) return; + const snippet = `[${tag}] `; + const s = ta.selectionStart ?? ta.value.length, e = ta.selectionEnd ?? ta.value.length; + ta.value = ta.value.slice(0, s) + snippet + ta.value.slice(e); + const pos = s + snippet.length; + ta.setSelectionRange(pos, pos); + ta.focus(); +} + +function _fishTagsRender() { + const body = $('fish-tags-body'); + if (!body || body.dataset.built) return; + body.innerHTML = Object.entries(FISH_EMOTION_TAGS).map(([group, tags]) => ` +
+
${escHtml(group)} (${tags.length})
+
+ ${tags.map(([t, d]) => ``).join('')} +
+
`).join(''); + body.querySelectorAll('.fish-tag').forEach(b => + b.addEventListener('click', () => _fishInsertTag(b.dataset.tag))); + body.dataset.built = '1'; +} + +function _fishTagsSync() { + const panel = $('fish-tags-panel'); + if (!panel) return; + panel.hidden = !_ttsIsFishBackend($('tts-backend-select')?.value || ''); +} + +(function initFishTagsPanel() { + const toggle = $('fish-tags-toggle'), body = $('fish-tags-body'); + toggle?.addEventListener('click', () => { + _fishTagsRender(); + body.hidden = !body.hidden; + toggle.textContent = body.hidden ? 'Show tags' : 'Hide tags'; + }); + $('fish-tags-sample')?.addEventListener('click', () => { + const ta = $('preview-text-area'); + if (ta) { ta.value = FISH_TAG_SAMPLE_TEXT; ta.focus(); } + }); +})(); function _ttsApplyEmotionTag(text, emotionValue) { const backend = $('tts-backend-select')?.value || ''; if (!_ttsIsFishBackend(backend) || !emotionValue) return text; @@ -250,6 +329,9 @@ setTimeout(function initPreviewEmotionPicker() { if (styleInput && sel.value) styleInput.value = sel.value; } }); + // Sections are injected asynchronously, so sync the Fish panel once the + // backend
+ + +