From 003e4f9f46167958a964bdbd12a85c36243bf048 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Tue, 11 Aug 2026 22:00:56 +0200 Subject: [PATCH] Fix bundle-breaking TDZ throw, use English emotion instructs, forward Fish gen params (v1.20.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The emotion quick-pickers added in 1.20.5 guarded with `typeof REH_EMOTIONS === 'undefined'`, but REH_EMOTIONS is a const declared later in the bundle's single shared scope — `typeof` on a const in its temporal dead zone throws instead of returning "undefined", which aborted top-level initialization for every module bundled after tts-preview.js. The pickers now read window.REH_EMOTIONS on a deferred macrotask. Also: emotion instructions are now always built in English (spoken text and the native-accent clause stay in the book's language), which controlled A/B testing showed produces a far cleaner prosodic gradient from Qwen3-TTS; and Fish-Speech now receives temperature/top_p/repetition_penalty, which it was the only backend never to have forwarded. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 7 +++++++ VERSION | 2 +- core/tts_helpers.py | 11 ++++++++++- static/dist/main.min.js | 8 ++++---- static/index.html | 6 +++--- static/js/conversation.js | 4 ++-- static/js/reader.js | 12 ++++++++---- static/js/rehearser.js | 21 +++++++++++++++++++-- static/js/tts-preview.js | 15 +++++++++++---- 9 files changed, 65 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 83a097f..d51bfaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.20.6] — 2026-08-11 + +### Fixed +- **Critical: the whole JS bundle stopped initializing partway through**, silently disabling every module loaded after `tts-preview.js` (Rehearser, Reader, Audiobook, character sheets, …). Introduced in 1.20.5 by the new emotion quick-pickers, which guarded with `typeof REH_EMOTIONS === 'undefined'` — but `REH_EMOTIONS` is a `const` declared later in the bundle's single shared scope, and `typeof` on a `const` in its temporal dead zone **throws** rather than returning `"undefined"`. The pickers now read `window.REH_EMOTIONS` and initialize on a deferred macrotask, after the bundle has fully executed. This also invalidated earlier emotion A/B testing, which had been measuring a half-initialized app. +- **Emotion instructions are now always written in English**, even for non-English voices (the spoken text and the native-accent clause stay in the book's own language). Confirmed by controlled A/B testing — same line, same voice, only the instruct language varying — that Qwen3-TTS follows English emotion instructions far more reliably: German instructs produced barely-differentiated output, while English instructs yield a clean, correctly-ordered prosodic gradient (whisper 128 Hz → sad 142 → neutral 179 → scared 203 → happy 225 → angry 269 Hz), with sensible duration changes too (sad slowest, scared fastest). +- **Fish-Speech generation parameters (`temperature` / `top_p` / `repetition_penalty`) were never forwarded.** Every Fish-Speech line synthesized at the server's fixed defaults, ignoring the app's per-backend stability settings — the only backend not routed through the shared `_apply_tts_extra_params` helper. + ## [1.20.5] — 2026-08-11 ### Added diff --git a/VERSION b/VERSION index 7bf9455..e63679c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.5 +1.20.6 diff --git a/core/tts_helpers.py b/core/tts_helpers.py index 44e619f..c182a9a 100644 --- a/core/tts_helpers.py +++ b/core/tts_helpers.py @@ -477,7 +477,16 @@ def _fishspeech_request_audio( "chunk_length": 200, "normalize": True, } - resp = requests.post(f"{base_url}/v1/tts", json=payload, timeout=180) + # Fish-Speech's own request schema (ServeTTSRequest) exposes temperature/top_p/ + # repetition_penalty, but this call never forwarded them — every line synthesized + # at the server's fixed default (temperature 0.8), regardless of Settings. Confirmed + # live as the likely cause of emotion tags barely differentiating from each other + # (a controlled same-line test showed happy/angry/excited/scared/shouting all + # collapsing into nearly identical pitch/loudness — the model wasn't being given + # room to vary). Every other backend already routes through this same helper; Fish + # was the one exception. + _apply_tts_extra_params(payload, settings, "fishspeech") + resp = _post_tts_with_fallback(f"{base_url}/v1/tts", payload, {}, timeout=180) resp.raise_for_status() audio = resp.content if not audio or len(audio) < 256: diff --git a/static/dist/main.min.js b/static/dist/main.min.js index 7baecb5..e2202aa 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -738,7 +738,7 @@ 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}(function(){const sel=$("preview-emotion-select");!sel||typeof REH_EMOTIONS=="undefined"||(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)}}))})(),$("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``}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")} @@ -866,7 +866,7 @@ This warms each voice so the engine caches its .pt and first playback is instant ${esc(status2)} `}).join(""):'No voices benchmarked.')}async function initTurnControls(){try{const settings=await fetchJson("/api/settings");q("bench-turn-llm-url")&&(q("bench-turn-llm-url").value=settings.conv_llm_url||settings.llm_url||"")}catch{}try{const d=await fetchJson("/api/stt-backends"),sel=q("bench-turn-stt");sel&&(sel.innerHTML=(d.backends||[]).map(b=>``).join("")||'')}catch{}try{typeof refreshTtsBackendAvailability=="function"&&await refreshTtsBackendAvailability();const sel=q("bench-turn-tts-backend"),backends=(window._ttsBackends||(typeof _ttsBackends!="undefined"?_ttsBackends:[])||[]).filter(Boolean);sel&&(sel.innerHTML=backends.map(b=>``).join("")||'')}catch{}}async function fetchTurnModels(){var _a2;const btn=q("bench-turn-fetch-llm"),sel=q("bench-turn-llm-model"),url=((_a2=q("bench-turn-llm-url"))==null?void 0:_a2.value.trim())||"";btn&&(btn.disabled=!0);try{const models=(await fetchJson("/api/conversation/llm-models"+(url?"?url="+encodeURIComponent(url):""))).models||[];sel&&(sel.innerHTML=models.length?models.map(m=>``).join(""):'')}catch(e){sel&&(sel.innerHTML=''),say("Model fetch failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}async function fetchTurnVoices(){var _a2;const btn=q("bench-turn-fetch-voices"),sel=q("bench-turn-voice"),backend=((_a2=q("bench-turn-tts-backend"))==null?void 0:_a2.value)||"voice_clone",picker=window.BenchmarkVoicePicker;btn&&(btn.disabled=!0);try{const raw=await fetchJson("/api/tts-voices?backend="+encodeURIComponent(backend)),items=(Array.isArray(raw)?raw:[]).map(v=>{const id=typeof backendVoiceId=="function"?backendVoiceId(v):typeof v=="string"?v:v.id||v.voice||v.name;return id?{id,label:id,meta:(window._voices||[]).find(x=>x&&x.id===id)||(typeof v=="object"?v:null)}:null}).filter(Boolean);picker?picker.populate("bench-turn-voice",items,{placeholder:"Fetch voices",empty:"No voices"}):sel&&(sel.innerHTML=items.length?items.map(v=>``).join(""):'')}catch(e){picker?picker.populate("bench-turn-voice",[],{placeholder:"Fetch failed",empty:"Fetch failed"}):sel&&(sel.innerHTML=''),say("Voice fetch failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function updateTurnStats(stats){const max=stats.total_ms||1;[["stt",stats.stt_ms],["ttft",stats.llm_ttft_ms],["llm",stats.llm_total_ms],["tts",stats.tts_ms],["total",stats.total_ms]].forEach(([key,ms])=>{const val=q("bench-turn-val-"+key),fill=q("bench-turn-fill-"+key);val&&(val.textContent=fmtMs(ms)),fill&&(fill.style.width=max>0?Math.min(100,(ms||0)/max*100)+"%":"0%")})}function addTurnLog(role,text){var _a2;const log=q("bench-turn-log");if(!log)return null;(_a2=log.querySelector(".conv-chat-welcome"))==null||_a2.remove();const wrap=document.createElement("div");wrap.className=`conv-bubble-wrap conv-bubble-wrap--${role}`;const bubble=document.createElement("div");return bubble.className=`conv-bubble conv-bubble--${role}`,bubble.textContent=text||"",wrap.appendChild(bubble),log.appendChild(wrap),log.scrollTop=log.scrollHeight,bubble}function addTurnHistory(total,ok){var _a2;const hist=q("bench-turn-history");if(!hist)return;(_a2=hist.querySelector(".conv-history-empty"))==null||_a2.remove(),turnHistoryCount++;const item=document.createElement("div");item.className="conv-hist-item",item.innerHTML=`#${turnHistoryCount}${fmtMs(total)}`,hist.prepend(item)}async function runTurnBenchmark(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2;const audio=(_b2=(_a2=q("bench-turn-audio"))==null?void 0:_a2.files)==null?void 0:_b2[0],text=((_c2=q("bench-turn-text"))==null?void 0:_c2.value.trim())||"";if(!audio&&!text){say("Choose turn audio or enter fallback text","error");return}const btn=q("bench-turn-run"),st=q("bench-turn-status");btn&&(btn.disabled=!0),st&&(st.textContent="Running conversation turn...");const t0=Date.now(),userBubble=addTurnLog("user",text||"Transcribing audio..."),assistantBubble=addTurnLog("assistant","...");let assistantText="",lastStats=null;try{const fd=new FormData;audio&&fd.append("audio",audio,audio.name),text&&fd.append("text",text),fd.append("stt_backend",((_d2=q("bench-turn-stt"))==null?void 0:_d2.value)||"configured"),fd.append("llm_url",((_e2=q("bench-turn-llm-url"))==null?void 0:_e2.value.trim())||""),fd.append("llm_model",((_f2=q("bench-turn-llm-model"))==null?void 0:_f2.value)||""),fd.append("tts_backend",((_g2=q("bench-turn-tts-backend"))==null?void 0:_g2.value)||"voice_clone"),fd.append("tts_voice",((_h2=q("bench-turn-voice"))==null?void 0:_h2.value)||""),fd.append("system_prompt",((_i2=q("bench-turn-system"))==null?void 0:_i2.value.trim())||"You are a helpful voice assistant."),fd.append("history","[]");const resp=await fetch("/api/conversation/turn",{method:"POST",body:fd});if(!resp.ok)throw new Error("Server error "+resp.status);const reader=resp.body.getReader(),dec=new TextDecoder;let buf="";for(;;){const{done,value}=await reader.read();if(done)break;buf+=dec.decode(value,{stream:!0});const lines=buf.split(` `);buf=lines.pop();for(const line of lines){if(!line.startsWith("data:"))continue;let evt;try{evt=JSON.parse(line.slice(5).trim())}catch{continue}if(evt.type==="transcript"&&userBubble&&(userBubble.textContent=evt.text||"(empty)"),evt.type==="token"&&(assistantText+=evt.delta||"",assistantBubble&&(assistantBubble.textContent=assistantText)),evt.type==="llm_done"&&(assistantText=evt.text||assistantText,assistantBubble&&(assistantBubble.textContent=assistantText)),evt.type==="audio"&&evt.b64){const bytes=Uint8Array.from(atob(evt.b64),c=>c.charCodeAt(0)),url=URL.createObjectURL(new Blob([bytes],{type:evt.mime||"audio/wav"})),audioEl=document.createElement("audio");audioEl.controls=!0,audioEl.src=url,audioEl.addEventListener("ended",()=>URL.revokeObjectURL(url),{once:!0}),(_j2=q("bench-turn-log"))==null||_j2.appendChild(audioEl)}if(evt.type==="stats"&&(lastStats=evt,updateTurnStats(evt)),evt.type==="error")throw new Error(`[${evt.stage||"turn"}] ${evt.message||"Unknown error"}`)}}const total=(_k2=lastStats==null?void 0:lastStats.total_ms)!=null?_k2:Date.now()-t0;addTurnHistory(total,!0),st&&(st.textContent=`Finished in ${fmtMs(total)}.`),say("Turn benchmark complete","success")}catch(e){assistantBubble&&(assistantBubble.textContent=e.message),addTurnHistory(Date.now()-t0,!1),st&&(st.textContent="Turn benchmark failed"),say("Turn benchmark failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function bindBenchmarkSection(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2;initialized||!q("bench-stt-run")||(initialized=!0,document.querySelectorAll(".bench-tab").forEach(btn=>btn.addEventListener("click",()=>setBenchTab(btn.dataset.benchTab))),window.BenchmarkVoicePicker&&(BenchmarkVoicePicker.upgrade("bench-stt-library-voice",{placeholder:"-- choose from voice library --",empty:"No library voices with reference transcripts found"}),BenchmarkVoicePicker.upgrade("perf-voice-select",{placeholder:"-- select after fetch --",empty:"No voices"}),BenchmarkVoicePicker.upgrade("bench-turn-voice",{placeholder:"Fetch voices",empty:"No voices"})),document.addEventListener("click",()=>closeBenchmarkModelPickers()),(_a2=q("bench-stt-refresh"))==null||_a2.addEventListener("click",loadBenchmarkSttEngines),(_b2=q("bench-stt-load-voice"))==null||_b2.addEventListener("click",useBenchmarkLibraryVoice),(_c2=q("bench-stt-audio"))==null||_c2.addEventListener("change",()=>{q("bench-stt-source-id")&&(q("bench-stt-source-id").value="")}),(_d2=q("bench-stt-run"))==null||_d2.addEventListener("click",runSttBenchmark),(_e2=q("bench-tts-run"))==null||_e2.addEventListener("click",runTtsBenchmark),(_f2=q("bench-turn-fetch-llm"))==null||_f2.addEventListener("click",fetchTurnModels),(_g2=q("bench-turn-fetch-voices"))==null||_g2.addEventListener("click",fetchTurnVoices),(_h2=q("bench-turn-run"))==null||_h2.addEventListener("click",runTurnBenchmark),(_i2=q("bench-turn-tts-backend"))==null||_i2.addEventListener("change",()=>{window.BenchmarkVoicePicker?BenchmarkVoicePicker.populate("bench-turn-voice",[],{placeholder:"Fetch voices",empty:"No voices"}):q("bench-turn-voice")&&(q("bench-turn-voice").innerHTML='')}))}function loadBenchmarkSectionData(){bindBenchmarkSection(),!(benchmarkDataLoaded||!q("bench-stt-run"))&&(benchmarkDataLoaded=!0,loadBenchmarkSttEngines(),loadBenchmarkVoiceLibrary(),initTurnControls())}window.loadBenchmarkSectionData=loadBenchmarkSectionData,bindBenchmarkSection(),!initialized&&document.body&&new MutationObserver(()=>bindBenchmarkSection()).observe(document.body,{childList:!0,subtree:!0})}();let sttTtsSourceId=null,sttTtsOutputBlob=null,_sttBackends=[],sttTtsRecorder=null,sttTtsRecordStream=null,sttTtsRecordChunks=[],sttTtsRecordTimer=null,sttTtsRecordSecs=0;function sttTtsSelectedSttBackend(){var _a2;return((_a2=$("stt-tts-stt-backend"))==null?void 0:_a2.value)||"configured"}function sttBackendOptionHtml(selected="configured"){var _a2;if(!_sttBackends.length)return'';const preferred=_sttBackends.some(b=>b.id===selected&&b.available)?selected:((_a2=_sttBackends.find(b=>b.available))==null?void 0:_a2.id)||selected;return _sttBackends.map(b=>{const suffix=b.available?"":" (unavailable)",disabled=b.available?"":" disabled";return``}).join("")}function updateSttBackendHelp(){const selected=sttTtsSelectedSttBackend(),b=_sttBackends.find(item=>item.id===selected)||_sttBackends.find(item=>item.available)||null,help=$("stt-tts-stt-help");if(help){if(!b){help.textContent="No STT engine status loaded yet.";return}help.innerHTML=sttBackendHelpHtml(b)}}async function refreshSttBackends(selected=""){try{_sttBackends=((await fetch("/api/stt-backends").then(r=>r.json())).backends||[]).filter(b=>b&&b.id)}catch{_sttBackends=[]}["stt-tts-stt-backend","clone-stt-backend"].forEach(id=>{const sel=$(id);if(!sel)return;const prev=selected||sel.value||"configured";sel.innerHTML=sttBackendOptionHtml(prev),sel.disabled=!_sttBackends.some(b=>b.available)}),updateSttBackendHelp(),typeof window.updateStatusBar=="function"&&window.updateStatusBar(),typeof window.refreshStatusBarEngines=="function"&&window.refreshStatusBarEngines()}function sttTtsSelectedBackend(){var _a2;return((_a2=$("stt-tts-backend-select"))==null?void 0:_a2.value)||""}function sttTtsDownload(blob,name){if(!blob)return;const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=name,a.click()}async function sttTtsUploadFile(file){if(!file)return;$("stt-tts-source-status").textContent="Uploading "+file.name+"...",sttTtsSourceId=null,sttTtsOutputBlob=null,$("stt-tts-transcribe-btn").disabled=!0,$("stt-tts-copy-preview-btn").disabled=!0,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0;const fd=new FormData;fd.append("file",file);try{const r=await fetch("/api/upload",{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();sttTtsSourceId=d.id;const audio=$("stt-tts-source-audio");audio.src="/api/audio/"+encodeURIComponent(d.id),audio.style.display="",$("stt-tts-source-status").textContent=`${d.filename||file.name} loaded (${Number(d.duration||0).toFixed(1)} s).`,$("stt-tts-transcribe-btn").disabled=!1,toast("Speech audio loaded","success")}catch(e){$("stt-tts-source-status").textContent="Upload failed.",toast("STT source upload failed: "+e.message,"error")}}async function sttTtsFetchVoices(){const backend=sttTtsSelectedBackend();if(!backend)throw new Error("No available TTS backend");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)))}const sel=$("stt-tts-voice-select"),prev=sel.value;return sel.innerHTML='',voices.forEach(v=>{const id=backendVoiceId(v),opt=document.createElement("option");opt.value=opt.textContent=id,sel.appendChild(opt)}),prev&&voices.some(v=>backendVoiceId(v)===prev)&&(sel.value=prev),voices.length}(_ba=$("stt-tts-file"))==null||_ba.addEventListener("change",async()=>{const input=$("stt-tts-file");input.files&&input.files.length&&await sttTtsUploadFile(input.files[0]),input.value=""}),(_ca=$("stt-tts-refresh-stt-btn"))==null||_ca.addEventListener("click",async()=>{const btn=$("stt-tts-refresh-stt-btn");btn.disabled=!0;try{await refreshSttBackends(sttTtsSelectedSttBackend()),toast("STT engines refreshed","success")}finally{btn.disabled=!1}}),(_da=$("stt-tts-stt-backend"))==null||_da.addEventListener("change",updateSttBackendHelp);function sttTtsSetRecording(on){$("stt-tts-rec-start").disabled=on,$("stt-tts-rec-stop").disabled=!on}function sttTtsStopTracks(){sttTtsRecordStream&&sttTtsRecordStream.getTracks().forEach(t=>t.stop()),sttTtsRecordStream=null}(_ea=$("stt-tts-rec-start"))==null||_ea.addEventListener("click",async()=>{try{sttTtsRecordStream=await requestMicrophoneStream(),sttTtsRecordChunks=[],sttTtsRecordSecs=0,$("stt-tts-rec-time").textContent="0:00",$("stt-tts-source-status").textContent="Recording...",sttTtsSetRecording(!0),sttTtsRecordTimer=setInterval(()=>{sttTtsRecordSecs++,$("stt-tts-rec-time").textContent=Math.floor(sttTtsRecordSecs/60)+":"+String(sttTtsRecordSecs%60).padStart(2,"0")},1e3),sttTtsRecorder=new MediaRecorder(sttTtsRecordStream,{audioBitsPerSecond:256e3}),sttTtsRecorder.ondataavailable=e=>{e.data.size&&sttTtsRecordChunks.push(e.data)},sttTtsRecorder.onstop=async()=>{clearInterval(sttTtsRecordTimer),sttTtsRecordTimer=null,sttTtsSetRecording(!1),sttTtsStopTracks();const mime=sttTtsRecorder.mimeType||"audio/webm",blob=new Blob(sttTtsRecordChunks,{type:mime}),ext=mime.includes("ogg")?".ogg":".webm";if(!blob.size){$("stt-tts-source-status").textContent="Recording was empty.",toast("Recording was empty","error");return}await sttTtsUploadFile(new File([blob],"stt-recording"+ext,{type:mime}))},sttTtsRecorder.start(100),toast("Recording started","success")}catch(e){sttTtsSetRecording(!1),sttTtsStopTracks();const message=await microphoneErrorMessage(e);$("stt-tts-source-status").textContent=message,toast(message,"error")}}),(_fa=$("stt-tts-rec-stop"))==null||_fa.addEventListener("click",()=>{sttTtsRecorder&&sttTtsRecorder.state!=="inactive"&&sttTtsRecorder.stop()}),(_ga=$("stt-tts-transcribe-btn"))==null||_ga.addEventListener("click",async()=>{if(!sttTtsSourceId){toast("Load speech audio first","error");return}const btn=$("stt-tts-transcribe-btn");btn.disabled=!0,$("stt-tts-source-status").textContent="Transcribing...";try{const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:sttTtsSourceId,backend:sttTtsSelectedSttBackend()})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("stt-tts-text").value=d.text||"",$("stt-tts-copy-preview-btn").disabled=!(d.text||"").trim();const used=d.backend?" via "+d.backend:"";$("stt-tts-source-status").textContent="Transcription ready"+used+".",typeof updateRefineButtonState=="function"&&updateRefineButtonState(),toast("Transcription ready","success")}catch(e){$("stt-tts-source-status").textContent="Transcription failed.",toast("STT failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_ha=$("stt-tts-copy-preview-btn"))==null||_ha.addEventListener("click",()=>{const text=$("stt-tts-text").value.trim();text&&($("preview-text-area").value=text,switchTab("generation"),toast("Copied transcription to TTS Generation","success"))}),(_ia=$("stt-tts-backend-select"))==null||_ia.addEventListener("change",()=>{$("stt-tts-voice-select").innerHTML='',sttTtsOutputBlob=null,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0,updateBackendHelp()}),(_ja=$("stt-tts-fetch-voices-btn"))==null||_ja.addEventListener("click",async()=>{const btn=$("stt-tts-fetch-voices-btn");btn.disabled=!0;try{const count=await sttTtsFetchVoices();toast("Fetched "+count+" voices","success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_ka=$("stt-tts-generate-btn"))==null||_ka.addEventListener("click",async()=>{const backend=sttTtsSelectedBackend(),voice=$("stt-tts-voice-select").value,text=$("stt-tts-text").value.trim(),instruct=$("stt-tts-style-instruction").value.trim();if(!backend){toast("No available TTS backend","error");return}if(!voice){toast("Select a TTS voice","error");return}if(!text){toast("Transcribe or enter text first","error");return}const btn=$("stt-tts-generate-btn");btn.disabled=!0,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0;try{const source=await createTtsAudioSource(voice,text,backend,$("stt-tts-playback-mode").value,instruct);sttTtsOutputBlob=source.blob;const audio=$("stt-tts-output-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("stt-tts-save-mp3-btn").disabled=!1,$("stt-tts-save-wav-btn").disabled=source.streaming,toast(source.streaming?"Streaming synthesized speech":"Synthesized speech ready","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_la=$("stt-tts-save-mp3-btn"))==null||_la.addEventListener("click",async()=>{const backend=sttTtsSelectedBackend(),voice=$("stt-tts-voice-select").value,text=$("stt-tts-text").value.trim(),instruct=$("stt-tts-style-instruction").value.trim();if(!backend||!voice||!text)return;const btn=$("stt-tts-save-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend);sttTtsDownload(blob,(voice||"stt_tts")+"_stt_tts.mp3"),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_ma=$("stt-tts-save-wav-btn"))==null||_ma.addEventListener("click",()=>{sttTtsOutputBlob&&sttTtsDownload(sttTtsOutputBlob,($("stt-tts-voice-select").value||"stt_tts")+"_stt_tts.wav")});function parseScript(text){const rawLines=text.split(` -`),result=[];let state="action",currentSpeaker=null,dialogBuffer=[],actionBuffer=[];const FADE_IN_RE=/^FADE\s+IN[\s:.\-]*$/i,FIRST_SCENE_RE=/^(?:[A-Z]{0,3}\d{1,4}[A-Z]?\s+)?(INT\.|EXT\.|INT\.\/EXT\.|EXT\.\/INT\.|I\/E\.)/i;let scanLines=rawLines;const firstIdx=rawLines.findIndex(l=>{const s=l.trim();return FADE_IN_RE.test(s)||FIRST_SCENE_RE.test(s)});firstIdx>0&&(scanLines=rawLines.slice(firstIdx));function flushDialog(){if(currentSpeaker&&dialogBuffer.length){const t=dialogBuffer.join(" ").trim();t&&result.push({type:"dialog",speaker:currentSpeaker,text:t,isDirection:!1,emotion:""})}dialogBuffer=[]}function flushAction(){if(actionBuffer.length){const t=actionBuffer.join(" ").trim();t&&result.push({type:"action",speaker:"",text:t,isDirection:!0}),actionBuffer=[]}}for(const rawLine of scanLines){const isPageBreak=rawLine.startsWith("\f"),line=isPageBreak?rawLine.slice(1).trim():rawLine.trim();if(isPageBreak){flushDialog(),flushAction();const pageNum=line&&/^\d+$/.test(line)?parseInt(line,10):null;result.push({type:"pagebreak",speaker:"",text:"",page:pageNum,isDirection:!0}),state="action",currentSpeaker=null;continue}if(!line){flushDialog(),flushAction(),state==="dialog"&&(state="action",currentSpeaker=null);continue}if(/\d{1,2}\/\d{1,2}\/\d{2,4}/.test(line)&&line.length<=60||/^[A-Z]{0,3}\d{1,4}[A-Z]?$/.test(line)||/^\(?CONTINUED\)?:?$/i.test(line)||/^[A-Z]{0,3}\d{1,4}[A-Z]?\s+CONTINUED:?(\s*[A-Z]{0,3}\d{1,4}[A-Z]?)?$/i.test(line)||/^(?:[A-Z]{0,3}\d{1,4}[A-Z]?\s+)?OMITTED(?:\s*[A-Z]{0,3}\d{1,4}[A-Z]?)?$/i.test(line)){flushDialog(),flushAction(),state==="dialog"&&(state="action",currentSpeaker=null);continue}if(line.startsWith("#")){flushDialog(),flushAction();const dir=line.slice(1).trim();dir&&result.push({type:"direction",speaker:"",text:dir,isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^\[.*\]$/.test(line)){state==="dialog"&&(flushDialog(),state="character"),result.push({type:"direction",speaker:currentSpeaker||"",text:line.slice(1,-1),isDirection:!0});continue}{const m=line.match(/^(?:([A-Z]{0,3}\d{1,4}[A-Z]?)\s+)?((?:INT\.\/EXT\.|EXT\.\/INT\.|I\/E\.|INT\.|EXT\.).*)$/i);if(m){flushDialog(),flushAction();let head=m[2].trim();m[1]&&head.toUpperCase().endsWith(m[1].toUpperCase())&&(head=head.slice(0,head.length-m[1].length).trim()),result.push({type:"scene",speaker:"",text:head.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}}if(/^ACT\s+(I{1,4}|V?I{0,3}|[1-9][0-9]?|ONE|TWO|THREE|FOUR|FIVE|SIX|SEVEN|EIGHT|NINE|TEN)(\b.*)?$/i.test(line)){flushDialog(),flushAction(),result.push({type:"act",speaker:"",text:line.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^SCENE\s+(I{1,4}|V?I{0,3}|[1-9][0-9]?|ONE|TWO|THREE|FOUR|FIVE|SIX|SEVEN|EIGHT|NINE|TEN)(\b.*)?$/i.test(line)){flushDialog(),flushAction(),result.push({type:"scene",speaker:"",text:line.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^(FADE\s+(IN|OUT|TO)|CUT\s+TO|SMASH\s+CUT|MATCH\s+CUT|DISSOLVE\s+TO|BLACKOUT|LIGHTS\s+(UP|DOWN|FADE)|CURTAIN|INTERMISSION|END\s+OF\s+(PLAY|ACT))[.:]?\s*$/i.test(line)){flushDialog(),flushAction(),result.push({type:"transition",speaker:"",text:line,isDirection:!0}),state="action",currentSpeaker=null;continue}const colonMatch=line.match(/^([\p{Lu}][\p{Lu}0-9 _\-ß]{0,39}):\s+(.+)$/u);if(colonMatch&&colonMatch[1].trim().length<=24&&colonMatch[1].trim().split(/\s+/).length<=3){flushDialog(),flushAction(),currentSpeaker=colonMatch[1].trim(),dialogBuffer=[colonMatch[2].trim()],state="dialog";continue}if(/^\(.*\)$/.test(line)){state==="dialog"&&(flushDialog(),state="character"),result.push({type:"direction",speaker:currentSpeaker||"",text:line,isDirection:!0});continue}const nameRaw=line.replace(/\s*\([^)]*\)\s*$/,"").trim();if(nameRaw.length>=2&&nameRaw.length<=42&&nameRaw===nameRaw.toUpperCase()&&/^[\p{Lu}][\p{Lu}0-9 '.\-ß]+$/u.test(nameRaw)&&!/^\d+$/.test(nameRaw)&&!/\.$/.test(nameRaw)){flushDialog(),flushAction(),currentSpeaker=nameRaw,state="character";continue}if(state==="character"){dialogBuffer=[line],state="dialog";continue}if(state==="dialog"){dialogBuffer.push(line);continue}actionBuffer.push(line),state="action"}return flushDialog(),flushAction(),result}function detectCharacters(lines){const speakers=[...new Set(lines.filter(l=>l.type==="dialog").map(l=>l.speaker))],cast={};return speakers.forEach((sp,i)=>{cast[sp]={voice:"",color:SPEAKER_COLORS[i%SPEAKER_COLORS.length],instruct:"",voiceData:null}}),cast}const SPEAKER_COLORS=["#89b4fa","#a6e3a1","#f38ba8","#fab387","#f9e2af","#cba6f7","#89dceb","#74c7ec"],REH_EMOTIONS=[{value:"",emoji:"\u{1F610}",label:"Neutral"},{value:"happy, cheerful and upbeat",emoji:"\u{1F60A}",label:"Happy"},{value:"sad, melancholy, somber",emoji:"\u{1F622}",label:"Sad"},{value:"angry, forceful, aggressive",emoji:"\u{1F620}",label:"Angry"},{value:"whisper, hushed and intimate",emoji:"\u{1F92B}",label:"Whisper"},{value:"excited, enthusiastic, energetic",emoji:"\u{1F929}",label:"Excited"},{value:"scared, nervous, trembling voice",emoji:"\u{1F628}",label:"Scared"},{value:"sarcastic, dry, ironic delivery",emoji:"\u{1F60F}",label:"Sarcastic"},{value:"dramatic, theatrical, intense",emoji:"\u{1F3AD}",label:"Dramatic"},{value:"gentle, warm, tender",emoji:"\u{1F970}",label:"Gentle"},{value:"confused, uncertain, hesitant",emoji:"\u{1F615}",label:"Confused"},{value:"bored, flat, disinterested",emoji:"\u{1F611}",label:"Bored"},{value:"surprised, shocked, astonished",emoji:"\u{1F632}",label:"Surprised"},{value:"confident, authoritative, bold",emoji:"\u{1F4AA}",label:"Confident"},{value:"mysterious, dark, ominous",emoji:"\u{1F311}",label:"Mysterious"},{value:"romantic, loving, passionate",emoji:"\u2764\uFE0F",label:"Romantic"},{value:"playful, teasing, mischievous",emoji:"\u{1F608}",label:"Playful"},{value:"calm, composed, measured",emoji:"\u{1F9D8}",label:"Calm"},{value:"commanding, authoritative, military",emoji:"\u2694\uFE0F",label:"Commanding"},{value:"grieving, tearful, broken",emoji:"\u{1F62D}",label:"Grieving"}];let rehCustomEmotions=[];try{rehCustomEmotions=JSON.parse(localStorage.getItem("reh-custom-emotions")||"[]")}catch{}function getEmotionInfo(value){if(!value)return{emoji:"",label:"Pick tone"};const found=[...REH_EMOTIONS,...rehCustomEmotions].find(e=>e.value===value);return found?{emoji:found.emoji,label:found.label}:{emoji:"\u2728",label:value.length>14?value.slice(0,13)+"\u2026":value}}function renderMarkdownInline(text){let s=escHtml(text);return s=s.replace(/\*\*([^*\n]+?)\*\*/g,"$1"),s=s.replace(/\*([^*\n]+?)\*/g,"$1"),s=s.replace(/__([^_\n]+?)__/g,"$1"),s=s.replace(/~~([^~\n]+?)~~/g,"$1"),s=s.replace(/==([^=\n]+?)==/g,'$1'),s}function stripMarkdown(text){return text.replace(/\*\*([^*\n]+?)\*\*/g,"$1").replace(/\*([^*\n]+?)\*/g,"$1").replace(/__([^_\n]+?)__/g,"$1").replace(/~~([^~\n]+?)~~/g,"$1").replace(/==([^=\n]+?)==/g,"$1")}const rehState={lines:[],cast:{},lineIndex:0,clips:[],voices:[],backend:"",playing:!1,repeat:!1,savedId:null,synthCache:new Map,staleLines:new Set,synthCancelled:!1,synthRunning:!1,skipDescriptions:!1,narratorVoice:"",practiceStart:null,practiceEnd:null,bulkMode:!1,bulkSel:new Set,bulkAnchor:null,showHidden:!1,recStream:null,recAudioCtx:null,recAnalyser:null,recSourceNode:null,recGainNode:null,recDestStream:null,recMeterRaf:null,recWaveRing:null,mediaRec:null,recChunks:[],recTimer:null,recSecs:0,lastRecBlob:null};window.rehState=rehState;async function rehDbGetAll(){const r=await fetch("/api/rehearsals");if(!r.ok)throw new Error("rehDbGetAll failed: "+r.status);return(await r.json()).rehearsals||[]}async function rehDbGetById(id){const r=await fetch("/api/rehearsals/"+id);if(r.status!==404){if(!r.ok)throw new Error("rehDbGetById failed: "+r.status);return r.json()}}async function rehDbAdd(record){const r=await fetch("/api/rehearsals",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(_rehSanitize(record))});if(!r.ok)throw new Error("rehDbAdd failed: "+r.status);return(await r.json()).id}async function rehDbPut(record){if(!record.id)return rehDbAdd(record);const r=await fetch("/api/rehearsals/"+record.id,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(_rehSanitize(record))});if(!r.ok)throw new Error("rehDbPut failed: "+r.status)}async function rehDbDelete(id){const r=await fetch("/api/rehearsals/"+id,{method:"DELETE"});if(!r.ok)throw new Error("rehDbDelete failed: "+r.status)}function _rehSanitize(rec){const out={...rec};return out.clips&&(out.clips=(out.clips||[]).map(c=>({lineIndex:c.lineIndex,speaker:c.speaker,type:c.type}))),out}(async function(){try{if((await rehDbGetAll()).length>0)return;const idbRecs=await _rehIdbGetAll().catch(()=>[]);if(!idbRecs.length)return;const r=await fetch("/api/rehearsals/migrate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(idbRecs.map(_rehSanitize))});if(r.ok){const d=await r.json();console.log(`[rehearser] migrated ${d.imported} records from IndexedDB \u2192 SQLite`)}}catch(e){console.warn("[rehearser] migration skipped:",e)}})();function _rehIdbGetAll(){return new Promise(resolve=>{const req=indexedDB.open("reh-library",1);req.onerror=()=>resolve([]),req.onsuccess=e=>{const db=e.target.result;if(!db.objectStoreNames.contains("rehearsals")){db.close(),resolve([]);return}const all=db.transaction("rehearsals","readonly").objectStore("rehearsals").getAll();all.onsuccess=ev=>{db.close(),resolve(ev.target.result||[])},all.onerror=()=>{db.close(),resolve([])}}})}window.rehDbGetById=rehDbGetById,window.rehLoadRecord=loadRecord;async function clipsToJson(clips){return Promise.all(clips.map(async c=>{if(!c.blob)return{lineIndex:c.lineIndex,speaker:c.speaker,type:c.type};const ab=await c.blob.arrayBuffer(),u8=new Uint8Array(ab);let bin="";const CHUNK=8192;for(let i=0;i{if(!c.b64)return c;const bin=atob(c.b64),u8=new Uint8Array(bin.length);for(let i=0;i{cast[sp]={voice:c.voice,color:c.color,instruct:c.instruct||"",lang:c.lang||"",gender:c.gender||"",tags:c.tags||"",soul:c.soul||"",ignored:!!c.ignored,hidden:!!c.hidden}});const emotions={},notes={},ignored={},hidden={};return rehState.lines.forEach((l,i)=>{l.type==="dialog"&&l.emotion&&(emotions[i]=l.emotion),l.type==="dialog"&&l.note&&(notes[i]=l.note),l.ignored&&(ignored[i]=!0),l.hidden&&(hidden[i]=!0)}),{title:((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||((_b2=$("reh-page-title"))==null?void 0:_b2.textContent)||"Untitled",script:rehState.lines.length?linesToScriptText():((_c2=$("reh-script-text"))==null?void 0:_c2.value.trim())||"",cast,emotions,notes,ignored,hidden,backend:rehState.backend,narratorVoice:rehState.narratorVoice,lineIndex:rehState.lineIndex,clips:rehState.clips.map(c=>({lineIndex:c.lineIndex,speaker:c.speaker,type:c.type,blob:c.blob||null})),updated:new Date}}function linesToScriptText(){return rehState.lines.map(line=>{switch(line.type){case"act":case"transition":return` +`),result=[];let state="action",currentSpeaker=null,dialogBuffer=[],actionBuffer=[];const FADE_IN_RE=/^FADE\s+IN[\s:.\-]*$/i,FIRST_SCENE_RE=/^(?:[A-Z]{0,3}\d{1,4}[A-Z]?\s+)?(INT\.|EXT\.|INT\.\/EXT\.|EXT\.\/INT\.|I\/E\.)/i;let scanLines=rawLines;const firstIdx=rawLines.findIndex(l=>{const s=l.trim();return FADE_IN_RE.test(s)||FIRST_SCENE_RE.test(s)});firstIdx>0&&(scanLines=rawLines.slice(firstIdx));function flushDialog(){if(currentSpeaker&&dialogBuffer.length){const t=dialogBuffer.join(" ").trim();t&&result.push({type:"dialog",speaker:currentSpeaker,text:t,isDirection:!1,emotion:""})}dialogBuffer=[]}function flushAction(){if(actionBuffer.length){const t=actionBuffer.join(" ").trim();t&&result.push({type:"action",speaker:"",text:t,isDirection:!0}),actionBuffer=[]}}for(const rawLine of scanLines){const isPageBreak=rawLine.startsWith("\f"),line=isPageBreak?rawLine.slice(1).trim():rawLine.trim();if(isPageBreak){flushDialog(),flushAction();const pageNum=line&&/^\d+$/.test(line)?parseInt(line,10):null;result.push({type:"pagebreak",speaker:"",text:"",page:pageNum,isDirection:!0}),state="action",currentSpeaker=null;continue}if(!line){flushDialog(),flushAction(),state==="dialog"&&(state="action",currentSpeaker=null);continue}if(/\d{1,2}\/\d{1,2}\/\d{2,4}/.test(line)&&line.length<=60||/^[A-Z]{0,3}\d{1,4}[A-Z]?$/.test(line)||/^\(?CONTINUED\)?:?$/i.test(line)||/^[A-Z]{0,3}\d{1,4}[A-Z]?\s+CONTINUED:?(\s*[A-Z]{0,3}\d{1,4}[A-Z]?)?$/i.test(line)||/^(?:[A-Z]{0,3}\d{1,4}[A-Z]?\s+)?OMITTED(?:\s*[A-Z]{0,3}\d{1,4}[A-Z]?)?$/i.test(line)){flushDialog(),flushAction(),state==="dialog"&&(state="action",currentSpeaker=null);continue}if(line.startsWith("#")){flushDialog(),flushAction();const dir=line.slice(1).trim();dir&&result.push({type:"direction",speaker:"",text:dir,isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^\[.*\]$/.test(line)){state==="dialog"&&(flushDialog(),state="character"),result.push({type:"direction",speaker:currentSpeaker||"",text:line.slice(1,-1),isDirection:!0});continue}{const m=line.match(/^(?:([A-Z]{0,3}\d{1,4}[A-Z]?)\s+)?((?:INT\.\/EXT\.|EXT\.\/INT\.|I\/E\.|INT\.|EXT\.).*)$/i);if(m){flushDialog(),flushAction();let head=m[2].trim();m[1]&&head.toUpperCase().endsWith(m[1].toUpperCase())&&(head=head.slice(0,head.length-m[1].length).trim()),result.push({type:"scene",speaker:"",text:head.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}}if(/^ACT\s+(I{1,4}|V?I{0,3}|[1-9][0-9]?|ONE|TWO|THREE|FOUR|FIVE|SIX|SEVEN|EIGHT|NINE|TEN)(\b.*)?$/i.test(line)){flushDialog(),flushAction(),result.push({type:"act",speaker:"",text:line.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^SCENE\s+(I{1,4}|V?I{0,3}|[1-9][0-9]?|ONE|TWO|THREE|FOUR|FIVE|SIX|SEVEN|EIGHT|NINE|TEN)(\b.*)?$/i.test(line)){flushDialog(),flushAction(),result.push({type:"scene",speaker:"",text:line.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^(FADE\s+(IN|OUT|TO)|CUT\s+TO|SMASH\s+CUT|MATCH\s+CUT|DISSOLVE\s+TO|BLACKOUT|LIGHTS\s+(UP|DOWN|FADE)|CURTAIN|INTERMISSION|END\s+OF\s+(PLAY|ACT))[.:]?\s*$/i.test(line)){flushDialog(),flushAction(),result.push({type:"transition",speaker:"",text:line,isDirection:!0}),state="action",currentSpeaker=null;continue}const colonMatch=line.match(/^([\p{Lu}][\p{Lu}0-9 _\-ß]{0,39}):\s+(.+)$/u);if(colonMatch&&colonMatch[1].trim().length<=24&&colonMatch[1].trim().split(/\s+/).length<=3){flushDialog(),flushAction(),currentSpeaker=colonMatch[1].trim(),dialogBuffer=[colonMatch[2].trim()],state="dialog";continue}if(/^\(.*\)$/.test(line)){state==="dialog"&&(flushDialog(),state="character"),result.push({type:"direction",speaker:currentSpeaker||"",text:line,isDirection:!0});continue}const nameRaw=line.replace(/\s*\([^)]*\)\s*$/,"").trim();if(nameRaw.length>=2&&nameRaw.length<=42&&nameRaw===nameRaw.toUpperCase()&&/^[\p{Lu}][\p{Lu}0-9 '.\-ß]+$/u.test(nameRaw)&&!/^\d+$/.test(nameRaw)&&!/\.$/.test(nameRaw)){flushDialog(),flushAction(),currentSpeaker=nameRaw,state="character";continue}if(state==="character"){dialogBuffer=[line],state="dialog";continue}if(state==="dialog"){dialogBuffer.push(line);continue}actionBuffer.push(line),state="action"}return flushDialog(),flushAction(),result}function detectCharacters(lines){const speakers=[...new Set(lines.filter(l=>l.type==="dialog").map(l=>l.speaker))],cast={};return speakers.forEach((sp,i)=>{cast[sp]={voice:"",color:SPEAKER_COLORS[i%SPEAKER_COLORS.length],instruct:"",voiceData:null}}),cast}const SPEAKER_COLORS=["#89b4fa","#a6e3a1","#f38ba8","#fab387","#f9e2af","#cba6f7","#89dceb","#74c7ec"],REH_EMOTIONS=[{value:"",emoji:"\u{1F610}",label:"Neutral"},{value:"happy, cheerful and upbeat",emoji:"\u{1F60A}",label:"Happy"},{value:"sad, melancholy, somber",emoji:"\u{1F622}",label:"Sad"},{value:"angry, forceful, aggressive",emoji:"\u{1F620}",label:"Angry"},{value:"whisper, hushed and intimate",emoji:"\u{1F92B}",label:"Whisper"},{value:"excited, enthusiastic, energetic",emoji:"\u{1F929}",label:"Excited"},{value:"scared, nervous, trembling voice",emoji:"\u{1F628}",label:"Scared"},{value:"sarcastic, dry, ironic delivery",emoji:"\u{1F60F}",label:"Sarcastic"},{value:"dramatic, theatrical, intense",emoji:"\u{1F3AD}",label:"Dramatic"},{value:"gentle, warm, tender",emoji:"\u{1F970}",label:"Gentle"},{value:"confused, uncertain, hesitant",emoji:"\u{1F615}",label:"Confused"},{value:"bored, flat, disinterested",emoji:"\u{1F611}",label:"Bored"},{value:"surprised, shocked, astonished",emoji:"\u{1F632}",label:"Surprised"},{value:"confident, authoritative, bold",emoji:"\u{1F4AA}",label:"Confident"},{value:"mysterious, dark, ominous",emoji:"\u{1F311}",label:"Mysterious"},{value:"romantic, loving, passionate",emoji:"\u2764\uFE0F",label:"Romantic"},{value:"playful, teasing, mischievous",emoji:"\u{1F608}",label:"Playful"},{value:"calm, composed, measured",emoji:"\u{1F9D8}",label:"Calm"},{value:"commanding, authoritative, military",emoji:"\u2694\uFE0F",label:"Commanding"},{value:"grieving, tearful, broken",emoji:"\u{1F62D}",label:"Grieving"}];window.REH_EMOTIONS=REH_EMOTIONS;let rehCustomEmotions=[];try{rehCustomEmotions=JSON.parse(localStorage.getItem("reh-custom-emotions")||"[]")}catch{}function getEmotionInfo(value){if(!value)return{emoji:"",label:"Pick tone"};const found=[...REH_EMOTIONS,...rehCustomEmotions].find(e=>e.value===value);return found?{emoji:found.emoji,label:found.label}:{emoji:"\u2728",label:value.length>14?value.slice(0,13)+"\u2026":value}}function renderMarkdownInline(text){let s=escHtml(text);return s=s.replace(/\*\*([^*\n]+?)\*\*/g,"$1"),s=s.replace(/\*([^*\n]+?)\*/g,"$1"),s=s.replace(/__([^_\n]+?)__/g,"$1"),s=s.replace(/~~([^~\n]+?)~~/g,"$1"),s=s.replace(/==([^=\n]+?)==/g,'$1'),s}function stripMarkdown(text){return text.replace(/\*\*([^*\n]+?)\*\*/g,"$1").replace(/\*([^*\n]+?)\*/g,"$1").replace(/__([^_\n]+?)__/g,"$1").replace(/~~([^~\n]+?)~~/g,"$1").replace(/==([^=\n]+?)==/g,"$1")}const rehState={lines:[],cast:{},lineIndex:0,clips:[],voices:[],backend:"",playing:!1,repeat:!1,savedId:null,synthCache:new Map,staleLines:new Set,synthCancelled:!1,synthRunning:!1,skipDescriptions:!1,narratorVoice:"",practiceStart:null,practiceEnd:null,bulkMode:!1,bulkSel:new Set,bulkAnchor:null,showHidden:!1,recStream:null,recAudioCtx:null,recAnalyser:null,recSourceNode:null,recGainNode:null,recDestStream:null,recMeterRaf:null,recWaveRing:null,mediaRec:null,recChunks:[],recTimer:null,recSecs:0,lastRecBlob:null};window.rehState=rehState;async function rehDbGetAll(){const r=await fetch("/api/rehearsals");if(!r.ok)throw new Error("rehDbGetAll failed: "+r.status);return(await r.json()).rehearsals||[]}async function rehDbGetById(id){const r=await fetch("/api/rehearsals/"+id);if(r.status!==404){if(!r.ok)throw new Error("rehDbGetById failed: "+r.status);return r.json()}}async function rehDbAdd(record){const r=await fetch("/api/rehearsals",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(_rehSanitize(record))});if(!r.ok)throw new Error("rehDbAdd failed: "+r.status);return(await r.json()).id}async function rehDbPut(record){if(!record.id)return rehDbAdd(record);const r=await fetch("/api/rehearsals/"+record.id,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(_rehSanitize(record))});if(!r.ok)throw new Error("rehDbPut failed: "+r.status)}async function rehDbDelete(id){const r=await fetch("/api/rehearsals/"+id,{method:"DELETE"});if(!r.ok)throw new Error("rehDbDelete failed: "+r.status)}function _rehSanitize(rec){const out={...rec};return out.clips&&(out.clips=(out.clips||[]).map(c=>({lineIndex:c.lineIndex,speaker:c.speaker,type:c.type}))),out}(async function(){try{if((await rehDbGetAll()).length>0)return;const idbRecs=await _rehIdbGetAll().catch(()=>[]);if(!idbRecs.length)return;const r=await fetch("/api/rehearsals/migrate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(idbRecs.map(_rehSanitize))});if(r.ok){const d=await r.json();console.log(`[rehearser] migrated ${d.imported} records from IndexedDB \u2192 SQLite`)}}catch(e){console.warn("[rehearser] migration skipped:",e)}})();function _rehIdbGetAll(){return new Promise(resolve=>{const req=indexedDB.open("reh-library",1);req.onerror=()=>resolve([]),req.onsuccess=e=>{const db=e.target.result;if(!db.objectStoreNames.contains("rehearsals")){db.close(),resolve([]);return}const all=db.transaction("rehearsals","readonly").objectStore("rehearsals").getAll();all.onsuccess=ev=>{db.close(),resolve(ev.target.result||[])},all.onerror=()=>{db.close(),resolve([])}}})}window.rehDbGetById=rehDbGetById,window.rehLoadRecord=loadRecord;async function clipsToJson(clips){return Promise.all(clips.map(async c=>{if(!c.blob)return{lineIndex:c.lineIndex,speaker:c.speaker,type:c.type};const ab=await c.blob.arrayBuffer(),u8=new Uint8Array(ab);let bin="";const CHUNK=8192;for(let i=0;i{if(!c.b64)return c;const bin=atob(c.b64),u8=new Uint8Array(bin.length);for(let i=0;i{cast[sp]={voice:c.voice,color:c.color,instruct:c.instruct||"",lang:c.lang||"",gender:c.gender||"",tags:c.tags||"",soul:c.soul||"",ignored:!!c.ignored,hidden:!!c.hidden}});const emotions={},notes={},ignored={},hidden={};return rehState.lines.forEach((l,i)=>{l.type==="dialog"&&l.emotion&&(emotions[i]=l.emotion),l.type==="dialog"&&l.note&&(notes[i]=l.note),l.ignored&&(ignored[i]=!0),l.hidden&&(hidden[i]=!0)}),{title:((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||((_b2=$("reh-page-title"))==null?void 0:_b2.textContent)||"Untitled",script:rehState.lines.length?linesToScriptText():((_c2=$("reh-script-text"))==null?void 0:_c2.value.trim())||"",cast,emotions,notes,ignored,hidden,backend:rehState.backend,narratorVoice:rehState.narratorVoice,lineIndex:rehState.lineIndex,clips:rehState.clips.map(c=>({lineIndex:c.lineIndex,speaker:c.speaker,type:c.type,blob:c.blob||null})),updated:new Date}}function linesToScriptText(){return rehState.lines.map(line=>{switch(line.type){case"act":case"transition":return` `+line.text+` `;case"scene":return` `+line.text+` @@ -1018,7 +1018,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 tmpl=_BUILD_INSTRUCT_TEMPLATES[langCode]||_BUILD_INSTRUCT_TEMPLATES.EN;return[e?tmpl(e):"",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,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")} @@ -1146,7 +1146,7 @@ ${lines.trim()} `:" ")+s.text:s.text,cur.words.push(...s.words)}return cur&&units.push(cur),units}function readerMarkParagraphBreaks(pageWords){if(!pageWords.length)return;const lines=[];let curLine=null,curTop=null;for(const w of pageWords)curLine&&Math.abs(w.top-curTop)a-b),median=sorted[Math.floor(sorted.length/2)]||0;if(!(median<=0))for(let i=1;imedian*1.35&&(lines[i][0].para=!0)}async function loadTesseract(){window.Tesseract||await new Promise((resolve,reject)=>{const s=document.createElement("script");s.src="/static/js/tesseract/tesseract.min.js",s.onload=resolve,s.onerror=reject,document.head.appendChild(s)})}async function readerGetOcrWorker(){return readerState.ocrWorker||(await loadTesseract(),readerState.ocrWorker=await Tesseract.createWorker(READER_OCR_LANGS,1,{workerPath:"/static/js/tesseract/worker.min.js",corePath:"/static/js/tesseract/tesseract-core-simd-lstm.wasm.js",langPath:"/static/js/tesseract/lang",gzip:!0})),readerState.ocrWorker}function _readerTimeout(promise,ms,label){return new Promise(function(resolve,reject){const t=setTimeout(function(){reject(new Error((label||"operation")+" timed out after "+ms+"ms"))},ms);promise.then(function(v){clearTimeout(t),resolve(v)},function(e){clearTimeout(t),reject(e)})})}async function _readerOcrAttempt(worker,canvas,psm){var _a2;await worker.setParameters({tessedit_pageseg_mode:psm});const{data}=await _readerTimeout(worker.recognize(canvas),2e4,"Heading OCR"),text=(data.text||"").replace(/\s+/g," ").trim();return!text||((_a2=data.confidence)!=null?_a2:0)40){const y0=Math.round(cropH*.45),inset=Math.round(crop.width*.03);tightCrop=document.createElement("canvas"),tightCrop.width=Math.max(crop.width-inset*2,1),tightCrop.height=cropH-y0,tightCrop.getContext("2d").putImageData(crop.getContext("2d").getImageData(inset,y0,tightCrop.width,tightCrop.height),0,0),text=await _readerOcrAttempt(worker,tightCrop,"7")}if(text||(text=await _readerOcrAttempt(worker,crop,"3")),!text)return[];const tokens=text.split(" ").filter(t=>t&&/[a-zäöüßàâçéèêëîïôûùüÿñæœ0-9]/i.test(t));if(!tokens.length)return[];const bandH=Math.max(gapPx-4,10),avgCharW=Math.max((base.width-16)/text.length,4),words=[];let x=8;for(const tok of tokens){const w=Math.max(tok.length*avgCharW,10);words.push({page:pageIdx,x,top:4,w,h:bandH,text:tok,ocr:!0}),x+=w+avgCharW}return words}catch(e){return console.warn("Heading OCR failed",e),[]}finally{crop&&(crop.width=0,crop.height=0),tightCrop&&(tightCrop.width=0,tightCrop.height=0)}}async function readerLoadPdfSkeleton(file){await loadPdfJs();const seq=readerState._seq,ab=await file.arrayBuffer(),pdf=await pdfjsLib.getDocument({data:ab}).promise;if(seq!==readerState._seq)return null;readerState.mode="pdf",readerState.pdfDoc=pdf;const doc=$("reader-doc"),nPages=pdf.numPages,page1=await pdf.getPage(1);if(seq!==readerState._seq)return null;const base1=page1.getViewport({scale:1});for(let p=1;p<=nPages;p++){const pageDiv=document.createElement("div");pageDiv.className="reader-page",pageDiv.style.width=base1.width+"px",pageDiv.style.height=base1.height+"px";const overlay=document.createElement("div");overlay.className="reader-overlay",pageDiv.appendChild(overlay),doc.appendChild(pageDiv),readerState.pages.push({pageDiv,overlay,page:null,base:base1,rendered:!1,renderTask:null}),p%50===0&&await readerYield()}return readerApplyZoom("fit-width"),readerSetupLazyRaster(),{pdf,page1,nPages}}async function readerExtractPdfText(loaded){var _a2;const seq=readerState._seq,pdf=loaded.pdf,page1=loaded.page1,nPages=loaded.nPages,progress=readerShowParseProgress(nPages),ocrHeadingsEnabled=((_a2=$("reader-ocr-headings"))==null?void 0:_a2.checked)!==!1;for(let p=1;p<=nPages;p++){const page=p===1?page1:await pdf.getPage(p);if(seq!==readerState._seq){progress.done();return}const base=page.getViewport({scale:1}),pageIdx=p-1,pgState=readerState.pages[pageIdx];pgState.page=page,pgState.base=base,pgState.pageDiv.style.width=base.width*readerState.scale+"px",pgState.pageDiv.style.height=base.height*readerState.scale+"px",readerIsPageNearView(pgState)&&readerRenderPage(pageIdx);let content;try{content=await _readerTimeout(page.getTextContent(),2e4,"Page text extraction")}catch(e){console.warn("getTextContent failed/timed out on page",p,e),content={items:[]}}if(seq!==readerState._seq)return;await readerYield();const pageWords=[];for(let itemIdx=0;itemIdxMath.max(base.height*READER_OCR_MIN_GAP_RATIO,READER_OCR_MIN_GAP_PX)){const ocrWords=await readerOcrPageHeading(page,base,pageIdx,gapPx);if(seq!==readerState._seq)return;ocrWords.length&&pageWords.unshift(...ocrWords)}}readerMarkParagraphBreaks(pageWords);const pageBase=readerBuildSentences(pageWords),pageUnits=readerGroupUnits(pageBase,readerState.chunkMode);pageUnits.length||pageUnits.push({text:"",words:[{page:pageIdx,x:0,top:0,w:0,h:0,text:""}],status:"pending",_stat:null,paraStart:!1});const startUnit=readerState.sentences.length;readerState.baseSentences.push(...pageBase),readerState.sentences.push(...pageUnits);const idxs=[];for(let u=startUnit;usetTimeout(r,0))}if(progress.done(),readerState.ocrWorker){try{readerState.ocrWorker.terminate()}catch{}readerState.ocrWorker=null}}async function readerLoadPdf(file){const loaded=await readerLoadPdfSkeleton(file);loaded&&await readerExtractPdfText(loaded)}function readerShowParseProgress(total){const doc=$("reader-doc");if(!doc)return{update(){},done(){}};const el=document.createElement("div");el.className="reader-parsing",el.innerHTML=' Reading PDF\u2026 page 0 / '+total+"",el.style.cssText="position:fixed; top:80px; left:50%; transform:translateX(-50%); z-index:100; background:var(--accent); color:#fff; padding:14px 28px; border-radius:32px; font-size:18px; display:inline-flex; align-items:center; gap:12px; box-shadow:0 6px 24px rgba(0,0,0,.35); font-weight:700;",doc.insertBefore(el,doc.firstChild);const label=el.querySelector("span:last-child");return{update(p){label&&(label.textContent="Reading PDF\u2026 page "+p+" / "+total)},done(){el.remove()}}}function readerComputeScale(mode){const doc=$("reader-doc");if(!doc||!readerState.pages.length)return 1;let maxW=0,maxH=0;for(const p of readerState.pages)p.base&&(p.base.width>maxW&&(maxW=p.base.width),p.base.height>maxH&&(maxH=p.base.height));if(!maxW)return 1;const padW=36,gap=16,availW=(doc.clientWidth||900)-padW,availH=(doc.clientHeight||600)-36;return mode==="fit-width"?Math.max(.2,availW/maxW):mode==="fit-height"?Math.max(.2,availH/maxH):mode==="two"?Math.max(.2,(availW-gap)/2/maxW):readerState.scale}function readerApplyZoom(mode){if(!readerState.pages.length)return;mode&&(readerState.zoomMode=mode),mode&&mode!=="custom"&&(readerState.scale=readerComputeScale(mode));const scale=readerState.scale;$("reader-doc").classList.toggle("two-page",readerState.zoomMode==="two"),readerState.pages.forEach(pg=>{pg.pageDiv.style.width=pg.base.width*scale+"px",pg.pageDiv.style.height=pg.base.height*scale+"px";const old=pg.pageDiv.querySelector("canvas.reader-canvas");if(old&&old.remove(),pg.renderTask){try{pg.renderTask.cancel()}catch{}pg.renderTask=null}pg.rendered=!1}),readerState.pages.forEach(pg=>pg.overlay.querySelectorAll(".reader-stat").forEach(el=>{const i=parseInt(el.dataset.si);isNaN(i)||readerPaintStatus(i)})),readerPaintSearchHighlights(),readerState.idx{entries.forEach(en=>{if(!en.isIntersecting)return;const idx=readerState.pages.findIndex(p=>p.pageDiv===en.target);idx>=0&&readerRenderPage(idx)})},{root:$("reader-doc"),rootMargin:"600px 0px"}),readerState.pages.forEach(p=>readerState.io.observe(p.pageDiv))}function readerRenderVisible(){$("reader-doc")&&(readerState.pages.forEach((pg,i)=>{readerIsPageNearView(pg,800)&&readerRenderPage(i)}),readerEvictCanvases())}function readerIsPageNearView(pg,margin=800){const doc=$("reader-doc");if(!doc||!(pg!=null&&pg.pageDiv))return!1;const dr=doc.getBoundingClientRect(),r=pg.pageDiv.getBoundingClientRect();return r.bottom>dr.top-margin&&r.top{if(!pg.rendered)return;const r=pg.pageDiv.getBoundingClientRect();if(r.bottomdr.bottom+READER_CANVAS_MARGIN){if(pg.renderTask){try{pg.renderTask.cancel()}catch{}pg.renderTask=null}const c=pg.pageDiv.querySelector("canvas.reader-canvas");c&&c.remove(),pg.rendered=!1}}))}const READER_MAX_CANVAS_PIXELS=2e6,READER_MAX_CANVAS_SIDE=1800;async function readerRenderPage(idx){var _a2;const pg=readerState.pages[idx];if(!pg||pg.rendered||!pg.page)return;pg.rendered=!0;const scale=readerState.scale,viewport=pg.page.getViewport({scale});let renderViewport=viewport;const overArea=viewport.width*viewport.height>READER_MAX_CANVAS_PIXELS,overSide=viewport.width>READER_MAX_CANVAS_SIDE||viewport.height>READER_MAX_CANVAS_SIDE;if(overArea||overSide){let fit=overArea?Math.sqrt(READER_MAX_CANVAS_PIXELS/(viewport.width*viewport.height)):1;fit=Math.min(fit,READER_MAX_CANVAS_SIDE/Math.max(viewport.width,viewport.height)),renderViewport=pg.page.getViewport({scale:scale*fit})}const canvas=document.createElement("canvas");canvas.className="reader-canvas",canvas.width=Math.floor(renderViewport.width),canvas.height=Math.floor(renderViewport.height),canvas.style.width=Math.floor(viewport.width)+"px",canvas.style.height=Math.floor(viewport.height)+"px",pg.pageDiv.insertBefore(canvas,pg.overlay),readerCreatePageStatus(idx);try{pg.renderTask=pg.page.render({canvasContext:canvas.getContext("2d"),viewport:renderViewport}),await _readerTimeout(pg.renderTask.promise,15e3,"Page render")}catch{pg.rendered=!1,canvas.remove();try{(_a2=pg.renderTask)==null||_a2.cancel()}catch{}}finally{pg.renderTask=null}}async function readerLoadText(text){readerState.mode="text",readerState.docText=text;const doc=$("reader-doc"),pane=document.createElement("div");pane.className="reader-text",doc.appendChild(pane);const words=[],paras=text.replace(/\r\n/g,` `).split(/\n{2,}/),frag=document.createDocumentFragment();let renderedWords=0;for(let pi=0;pi{const span=e.target.closest(".reader-word");if(!span)return;const si=readerState.sentences.findIndex(s=>s.words.some(w=>w.el===span));si<0||(readerState.selecting?readerPickSentence(si):readerJumpTo(si))})}function readerSentBBox(sentence){var _a2,_b2;const pageIdx=(_b2=(_a2=sentence.words[0])==null?void 0:_a2.page)!=null?_b2:0,on=sentence.words.filter(w=>w.page===pageIdx);return{page:pageIdx,x:Math.min(...on.map(w=>w.x)),top:Math.min(...on.map(w=>w.top)),w:Math.max(...on.map(w=>w.x+w.w))-Math.min(...on.map(w=>w.x)),h:Math.max(...on.map(w=>w.top+w.h))-Math.min(...on.map(w=>w.top))}}function readerBuildUnitIndex(){readerState.unitsByPage=new Map,readerState.mode==="pdf"&&readerState.sentences.forEach((s,i)=>{var _a2,_b2;const pg=(_b2=(_a2=s.words[0])==null?void 0:_a2.page)!=null?_b2:0;readerState.unitsByPage.has(pg)||readerState.unitsByPage.set(pg,[]),readerState.unitsByPage.get(pg).push(i)})}function readerCreatePageStatus(pageIdx){var _a2;if(readerState.mode!=="pdf")return;const pg=readerState.pages[pageIdx];if(!pg)return;const idxs=((_a2=readerState.unitsByPage)==null?void 0:_a2.get(pageIdx))||[];for(const i of idxs){const s=readerState.sentences[i];if(!s||s._stat)continue;const el=document.createElement("div");el.className="reader-stat",el.dataset.si=i,pg.overlay.appendChild(el),s._stat=el,readerPaintStatus(i)}}function readerEffectiveStatus(s){return s.status}function readerSetStatus(idx,status2){var _a2;const s=readerState.sentences[idx];s&&(s.status=status2,status2!=="pending"&&!readerState.synthStarted&&(readerState.synthStarted=!0,(_a2=$("reader-doc"))==null||_a2.classList.add("reader-synth-started")),readerPaintStatus(idx))}function readerPaintStatus(idx){const s=readerState.sentences[idx];if(!s)return;const eff=readerEffectiveStatus(s);if(readerState.mode==="pdf"){if(!s._stat)return;const b=readerSentBBox(s),scale=readerState.scale;Object.assign(s._stat.style,{left:b.x*scale+"px",top:b.top*scale+"px",width:b.w*scale+"px",height:b.h*scale+"px"}),s._stat.className="reader-stat reader-stat-"+eff}else s.words.forEach(w=>{w.el&&(w.el.classList.remove("stat-pending","stat-synth","stat-ready","stat-reading"),w.el.classList.add("stat-"+eff))})}let _readerWordBox=null;function readerEnsureBox(){_readerWordBox||(_readerWordBox=document.createElement("div"),_readerWordBox.className="reader-hl-word")}function readerClearHighlights(){readerState.mode==="text"?document.querySelectorAll(".reader-word.is-word").forEach(el=>el.classList.remove("is-word")):_readerWordBox&&(_readerWordBox.hidden=!0)}function readerEnsureVisible(el){const doc=$("reader-doc");if(!doc||!el)return;const dr=doc.getBoundingClientRect(),er=el.getBoundingClientRect();if(er.topdr.bottom-50){const delta=er.top-dr.top-doc.clientHeight*.38;doc.scrollTo({top:doc.scrollTop+delta,behavior:"smooth"})}}function readerHighlightSentence(sentence){var _a2;sentence&&(readerState.mode==="text"?readerEnsureVisible((_a2=sentence.words[0])==null?void 0:_a2.el):sentence._stat&&readerEnsureVisible(sentence._stat))}function readerHighlightWord(sentence,wi){const w=sentence.words[wi];if(!w)return;if(readerState.mode==="text"){document.querySelectorAll(".reader-word.is-word").forEach(el=>el.classList.remove("is-word")),w.el&&(w.el.classList.add("is-word"),readerEnsureVisible(w.el));return}readerEnsureBox();const pg=readerState.pages[w.page];if(!pg)return;const scale=readerState.scale;_readerWordBox.parentNode!==pg.overlay&&pg.overlay.appendChild(_readerWordBox),Object.assign(_readerWordBox.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:w.w*scale+"px",height:w.h*scale+"px"}),_readerWordBox.hidden=!1,readerEnsureVisible(_readerWordBox)}function readerSetSelecting(on){readerState.selecting=!!on,readerState.selAnchored=!1;const doc=$("reader-doc");doc&&doc.classList.toggle("reader-selecting",readerState.selecting);const btn=$("reader-select-toggle");btn&&btn.classList.toggle("active",readerState.selecting);const lbl=$("reader-select-label");lbl&&(lbl.textContent=readerState.selecting?"Done":"Select range");const ic=$("reader-select-icon");ic&&(ic.className="mdi "+(readerState.selecting?"mdi-check":"mdi-cursor-default-click-outline")),readerSelHint(readerState.selecting?"Click the start sentence\u2026":""),readerState.selecting||readerPaintSelection()}function readerSelHint(html){const h=$("reader-sel-hint");h&&(h.innerHTML=html,h.hidden=!html)}function readerPickSentence(si){if(!readerState.selAnchored)readerState.selStart=si,readerState.selEnd=si,readerState.selAnchored=!0,readerSelHint("Start at sentence "+(si+1)+" \u2014 now click the end sentence");else{readerState.selEnd=si,readerState.selAnchored=!1;const r=readerSelRange();readerSelHint("Selected "+(r[1]-r[0]+1)+" sentences \u2014 click a new start, or Done")}readerPaintSelection(),readerUpdateScopeLabel()}function readerClearSelection(){readerState.selStart=readerState.selEnd=null,readerState.selAnchored=!1,readerPaintSelection(),readerUpdateScopeLabel(),readerState.selecting&&readerSelHint("Click the start sentence\u2026")}function readerSelRange(){return readerState.selStart===null||readerState.selEnd===null?null:[Math.min(readerState.selStart,readerState.selEnd),Math.max(readerState.selStart,readerState.selEnd)]}function readerPaintSelection(){const range=readerSelRange(),inSel=i=>range&&i>=range[0]&&i<=range[1],anchorI=readerState.selAnchored&&range&&range[0]===range[1]?range[0]:-1;readerState.sentences.forEach((s,i)=>{const sel=inSel(i)&&i!==anchorI,anchor=i===anchorI;readerState.mode==="pdf"?s._stat&&(s._stat.classList.toggle("sel",sel),s._stat.classList.toggle("sel-anchor",anchor)):s.words.forEach(w=>{w.el&&(w.el.classList.toggle("in-sel",sel),w.el.classList.toggle("sel-anchor",anchor))})})}function readerScopeIndices(){var _a2,_b2;const all=readerState.sentences.map((_,i)=>i),range=readerSelRange();if(range)return all.filter(i=>i>=range[0]&&i<=range[1]);if(readerState.mode==="pdf"){const n=readerState.pages.length;let from=parseInt((_a2=$("reader-page-from"))==null?void 0:_a2.value)||1,to=parseInt((_b2=$("reader-page-to"))==null?void 0:_b2.value)||n;return from=Math.max(1,Math.min(from,n)),to=Math.max(from,Math.min(to,n)),all.filter(i=>{var _a3,_b3;const p=((_b3=(_a3=readerState.sentences[i].words[0])==null?void 0:_a3.page)!=null?_b3:0)+1;return p>=from&&p<=to})}return all}function readerUpdateScopeLabel(){const scopeEl=$("reader-synth-scope"),range=readerSelRange();scopeEl&&(range?scopeEl.textContent="selection":readerState.mode==="pdf"?scopeEl.textContent="pages":scopeEl.textContent="all");const info=$("reader-sel-info"),txt=$("reader-sel-text");info&&(info.hidden=!range),txt&&range&&(txt.textContent="sentences "+(range[0]+1)+"\u2013"+(range[1]+1)+" ("+(range[1]-range[0]+1)+")")}function _readerTextForSynth(text){var _a2,_b2;const backend=((_a2=$("reader-backend-select"))==null?void 0:_a2.value)||"",emotion=((_b2=$("reader-emotion-select"))==null?void 0:_b2.value)||"";if(!/fish/i.test(backend)||!emotion||/^\s*\[/.test(text))return text;const tag=typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(emotion):"";return tag?`[${tag}] ${text}`:text}(function(){const sel=$("reader-emotion-select");!sel||typeof REH_EMOTIONS=="undefined"||(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=$("reader-backend-select"))==null?void 0:_a2.value)||"";if(!/fish/i.test(backend)){const instructInput=$("reader-instruct");instructInput&&sel.value&&(instructInput.value=sel.value)}}))})();async function readerSynthIndices(targets){var _a2,_b2,_c2;if(targets=targets.filter(i=>!readerState.blobCache.has(i)),!targets.length||readerState.synthRunning)return{done:0,failed:[]};const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice)return toast("Pick a voice first","error"),{done:0,failed:[]};if(!backend)return toast("No TTS backend selected","error"),{done:0,failed:[]};const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.synthRunning=!0,readerState.synthCancel=!1;const prog=$("reader-synth-prog");prog&&(prog.hidden=!1);const total=targets.length;let done=0;const failed=[],update=()=>{const f=$("reader-synth-fill");f&&(f.style.width=done/total*100+"%");const l=$("reader-synth-label");l&&(l.textContent=done+" / "+total)};update();try{const queue=targets.slice(),worker=async()=>{var _a3;for(;queue.length&&!readerState.synthCancel;){const i=queue.shift();if(readerState.blobCache.has(i)){done++,update();continue}readerState.sentences[i].status==="pending"&&readerSetStatus(i,"synth");try{const blob=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[i].text),READER_FMT,instruct,backend,!1,readerGenParams());readerState.blobCache.set(i,blob),readerState.sentences[i].status==="synth"&&readerSetStatus(i,"ready")}catch(e){failed.push(i),((_a3=readerState.sentences[i])==null?void 0:_a3.status)==="synth"&&readerSetStatus(i,"pending"),console.error("[reader] synth failed for sentence",i,e)}done++,update()}},N=Math.min(2,targets.length);await Promise.all(Array.from({length:N},worker))}finally{readerState.synthRunning=!1,prog&&(prog.hidden=!0)}return{done,failed}}async function readerSynthAll(){const targets=readerScopeIndices().filter(i=>!readerState.blobCache.has(i));if(!targets.length){toast("Selected range is already synthesised","success");return}const{done,failed}=await readerSynthIndices(targets);if(readerState.synthCancel){toast("Synthesis cancelled ("+done+" done)","error");return}if(failed.length){toast(done-failed.length+" / "+done+" sentences synthesised \u2014 "+failed.length+" failed, see red markers","error");return}toast("Synthesised "+done+" sentences","success")}function readerSafeName(s){return(s||"audio").replace(/[\/\\:*?"<>|]+/g,"_").replace(/\s+/g," ").trim().slice(0,60)||"audio"}function readerPad(n){return String(n).padStart(2,"0")}function readerPageOf(i){var _a2,_b2;return readerState.mode==="pdf"?((_b2=(_a2=readerState.sentences[i].words[0])==null?void 0:_a2.page)!=null?_b2:0)+1:1}function readerDownload(blob,filename){const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),setTimeout(()=>{URL.revokeObjectURL(a.href),a.remove()},1500)}const _readerDelay=ms=>new Promise(r=>setTimeout(r,ms));async function readerExport(mode){if(!readerState.sentences.length){toast("Import a document first","error");return}const indices=readerScopeIndices(),missing=indices.filter(i=>!readerState.blobCache.has(i));let failedCount=0;if(missing.length){toast("Synthesising "+missing.length+" missing sentence(s) before export\u2026","success");const{failed}=await readerSynthIndices(missing);if(readerState.synthCancel){toast("Export cancelled","error");return}failedCount=failed.length}const ready=indices.filter(i=>readerState.blobCache.has(i));if(!ready.length){toast("Nothing to export","error");return}if(failedCount){toast(failedCount+" sentence(s) failed to synthesise \u2014 fix them (see red markers) before exporting, or missing audio will silently drop from the file","error");return}const title=readerSafeName(readerState.title);if(mode==="sentence"){const perPage={};for(const i of ready){const pg=readerPageOf(i);perPage[pg]=(perPage[pg]||0)+1;const name=readerState.mode==="pdf"?`${title} - p${readerPad(pg)} - ${readerPad(perPage[pg])}.mp3`:`${title} - ${readerPad(perPage[pg])}.mp3`;readerDownload(readerState.blobCache.get(i),name),await _readerDelay(350)}toast("Exported "+ready.length+" MP3 files","success");return}const byPage=new Map;ready.forEach(i=>{const pg=readerPageOf(i);byPage.has(pg)||byPage.set(pg,[]),byPage.get(pg).push(i)});let files=0;for(const[pg,idxs]of[...byPage.entries()].sort((a,b)=>a[0]-b[0])){const blob=new Blob(idxs.map(i=>readerState.blobCache.get(i)),{type:"audio/mpeg"}),name=readerState.mode==="pdf"?`${title} - p${readerPad(pg)}.mp3`:`${title}.mp3`;readerDownload(blob,name),files++,await _readerDelay(400)}toast("Exported "+files+(readerState.mode==="pdf"?" page MP3 file(s)":" MP3"),"success")}function readerRechunk(mode){readerState.chunkMode=mode,readerState.baseSentences.length&&(readerStopPlayback(),readerState.blobCache.clear(),readerState.bufCache.clear(),readerState.gainCache.clear(),readerState.savedAudioIdx=new Set,readerState.pages.forEach(p=>p.overlay.querySelectorAll(".reader-stat").forEach(e=>e.remove())),readerClearSelection(),readerState.sentences=readerGroupUnits(readerState.baseSentences,mode),readerBuildUnitIndex(),readerState.pages.forEach((p,i)=>{p.rendered&&readerCreatePageStatus(i)}),readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),readerState.idx=0,readerUpdateScopeLabel(),readerUpdateProgress(),readerHighlightSentence(readerState.sentences[0]),toast("Voice consistency: "+mode+" \u2014 audio cleared, re-synthesise","success"))}function readerGenParams(){var _a2,_b2,_c2;const out={},seed=(_a2=$("reader-seed"))==null?void 0:_a2.value.trim(),temp=(_b2=$("reader-temp"))==null?void 0:_b2.value.trim(),nspd=(_c2=$("reader-tts-speed"))==null?void 0:_c2.value.trim();return seed!==""&&seed!=null&&!isNaN(+seed)&&(out.seed=parseInt(seed,10)),temp!==""&&temp!=null&&!isNaN(+temp)&&(out.temperature=parseFloat(temp)),nspd!==""&&nspd!=null&&!isNaN(+nspd)&&parseFloat(nspd)!==1&&(out.speed=parseFloat(nspd)),Object.keys(out).length?out:null}function readerComputeGain(idx,buf){if(readerState.gainCache.has(idx))return readerState.gainCache.get(idx);const ch=buf.getChannelData(0),step=Math.max(1,Math.floor(ch.length/8e3));let sum=0,n=0,peak=1e-6;for(let i=0;ipeak&&(peak=Math.abs(v))}let gain=.1/(Math.sqrt(sum/Math.max(1,n))||1e-4);return gain=Math.max(.5,Math.min(gain,4)),gain=Math.min(gain,.99/peak),readerState.gainCache.set(idx,gain),gain}async function readerFetchServerAudio(idx){if(!readerState.savedId||!readerState.savedAudioIdx.has(idx))return null;try{const r=await fetch(`${READER_API}/${readerState.savedId}/audio/${idx}`);if(r.ok){const b=await r.blob();return readerState.blobCache.set(idx,b),b}}catch{}return null}async function readerGetBuffer(idx){var _a2,_b2,_c2;if(readerState.bufCache.has(idx))return readerState.bufCache.get(idx);let blob=readerState.blobCache.get(idx)||await readerFetchServerAudio(idx);if(!blob){const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice)throw new Error("Pick a voice first");if(!backend)throw new Error("No TTS backend selected");const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.sentences[idx].status!=="reading"&&readerSetStatus(idx,"synth"),blob=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[idx].text),READER_FMT,instruct,backend,!1,readerGenParams()),readerState.blobCache.set(idx,blob),readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"ready")}const buf=await readerCtx().decodeAudioData(await blob.arrayBuffer());return readerState.bufCache.set(idx,buf),buf}function readerEvictBuffers(center){if(!(readerState.bufCache.size<=READER_BUF_WINDOW*2+1))for(const i of readerState.bufCache.keys())Math.abs(i-center)>READER_BUF_WINDOW&&readerState.bufCache.delete(i)}async function readerPrefetch(idx){var _a2,_b2,_c2;if(!(idx<0||idx>=readerState.sentences.length)){if(!readerState.blobCache.has(idx)&&!await readerFetchServerAudio(idx)){const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice||!backend)return;const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.sentences[idx].status==="pending"&&readerSetStatus(idx,"synth");try{const b=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[idx].text),READER_FMT,instruct,backend,!1,readerGenParams());readerState.blobCache.set(idx,b),readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"ready")}catch{readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"pending");return}}if(!readerState.bufCache.has(idx)&&Math.abs(idx-readerState.idx)<=READER_BUF_WINDOW)try{readerState.bufCache.set(idx,await readerCtx().decodeAudioData(await readerState.blobCache.get(idx).arrayBuffer()))}catch{}}}function readerClearReading(){const i=readerState.readingIdx;i>=0&&i=readerState.sentences.length){readerStopPlayback(),readerState.idx=0,readerUpdateProgress();return}const idx=readerState.idx,sentence=readerState.sentences[idx];readerUpdateProgress(),readerSaveResume(),readerState.readingIdx=idx,readerSetStatus(idx,"reading"),readerHighlightSentence(sentence);let buf;try{buf=await readerGetBuffer(idx)}catch(e){toast(e.message||String(e),"error"),readerSetStatus(idx,"pending"),readerStopPlayback();return}if(!readerState.playing||readerState.idx!==idx)return;readerEvictBuffers(idx);const timings=computeWordTimings(sentence.text,buf.duration),ctx=readerCtx();readerStopSource();const src=ctx.createBufferSource();if(src.buffer=buf,src.playbackRate.value=readerState.speed,readerState.normalize){const g=ctx.createGain();g.gain.value=readerComputeGain(idx,buf),src.connect(g),g.connect(ctx.destination)}else src.connect(ctx.destination);readerState.currentSource=src;const t0=ctx.currentTime;readerPrefetch(idx+1),src.onended=()=>{readerState.currentSource===src&&(readerState.currentSource=null,readerState.raf&&(cancelAnimationFrame(readerState.raf),readerState.raf=null),readerSetStatus(idx,"ready"),readerState.readingIdx===idx&&(readerState.readingIdx=-1),readerState.playing&&(readerState.idx++,readerPlayCurrent()))},src.start(0);const tick=()=>{if(readerState.currentSource!==src)return;const elapsed=(ctx.currentTime-t0)*readerState.speed;let active=0;for(let i=timings.length-1;i>=0;i--)if(elapsed>=timings[i].start){active=i;break}readerHighlightWord(sentence,Math.min(active,sentence.words.length-1)),readerState.raf=requestAnimationFrame(tick)};readerState.raf=requestAnimationFrame(tick)}function readerStopSource(){if(readerState.currentSource){try{readerState.currentSource.onended=null,readerState.currentSource.stop(0)}catch{}readerState.currentSource=null}readerState.raf&&(cancelAnimationFrame(readerState.raf),readerState.raf=null)}function readerStopPlayback(){readerState.playing=!1,readerStopSource(),readerClearHighlights(),readerClearReading(),readerUpdatePlayBtn(),typeof window.setNavBusy=="function"&&window.setNavBusy("s-reader",!1)}function readerPlay(){var _a2;if(!readerState.sentences.length){toast("Import a document first","error");return}if(!((_a2=$("reader-voice-select"))!=null&&_a2.value)){toast("Pick a voice first","error");return}readerCtx(),readerState.playing=!0,readerUpdatePlayBtn(),typeof window.setNavBusy=="function"&&window.setNavBusy("s-reader",!0),readerPlayCurrent()}function readerPause(){readerState.playing=!1,readerStopSource(),readerSaveResume(),readerPersistProgress(),readerClearReading(),readerUpdatePlayBtn()}function readerJumpTo(idx){readerState.idx=Math.max(0,Math.min(idx,readerState.sentences.length-1)),readerStopSource(),readerClearHighlights(),readerClearReading(),readerUpdateProgress(),readerSaveResume(),readerState.playing?readerPlayCurrent():readerHighlightSentence(readerState.sentences[readerState.idx])}function readerUpdatePlayBtn(){const btn=$("reader-play");if(!btn)return;const ic=btn.querySelector(".mdi");ic&&(ic.className="mdi "+(readerState.playing?"mdi-pause":"mdi-play"))}function readerUpdateProgress(){const total=readerState.sentences.length,cur=total?readerState.idx+1:0,lbl=$("reader-progress-label");lbl&&(lbl.textContent=cur+" / "+total);const fill=$("reader-progress-fill");fill&&(fill.style.width=(total?readerState.idx/total*100:0)+"%")}function readerSaveResume(){if(!(!readerState.title||!readerState.sentences.length))try{localStorage.setItem(READER_RESUME_KEY,JSON.stringify({title:readerState.title,idx:readerState.idx,total:readerState.sentences.length}))}catch{}}function readerLoadResume(){try{const r=JSON.parse(localStorage.getItem(READER_RESUME_KEY)||"null");if(r&&r.title===readerState.title&&r.idx>0&&r.idx({}))).detail||r.statusText);const fresh=!readerState.savedId;if(readerState.savedId=(await r.json()).id,fresh||!readerState.sourceUploaded){const ext=readerState.mode==="pdf"?"pdf":"txt",body=readerState.mode==="pdf"?readerState.fileBlob:new Blob([readerState.docText||""],{type:"text/plain"});(await fetch(`${READER_API}/${readerState.savedId}/source?ext=${ext}`,{method:"PUT",body})).ok&&(readerState.sourceUploaded=!0)}let uploaded=0;for(let i=0;i{const file=inp.files[0];if(!file)return;if(!(await fetch(`${READER_API}/${id}/source?ext=pdf`,{method:"PUT",body:file})).ok){toast("Re-upload failed","error");return}toast("PDF restored \u2014 opening\u2026","success"),readerState.savedId=id,readerState.sourceUploaded=!0,readerState.fileBlob=file,await readerLoadPdf(file)},inp.click(),toast("PDF source missing \u2014 please re-select the original file","error");return}readerState.fileBlob=sourceBlob,await readerLoadPdf(sourceBlob)}else{const text=sourceBlob?await sourceBlob.text():rec.text||"";readerState.docText=text,await readerLoadText(text)}}catch(e){toast("Open failed: "+(e.message||e),"error");return}if(!readerState.sentences.length){toast("Document had no readable text","error");return}readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),readerState.savedId=id,readerState.sourceUploaded=!0,readerState.savedAudioIdx=new Set,rec.sentenceCount===readerState.sentences.length?(rec.audioIdx||[]).forEach(i=>{io.value===value)){const o=document.createElement("option");o.value=o.textContent=value,sel.appendChild(o)}sel.value=value,id==="reader-voice-select"&&window.VoicePicker&&VoicePicker.setValue(id,value)}}async function readerRenderLibrary(){const card=$("reader-library-card"),list=$("reader-lib-list");if(!card||!list)return;let all=[];try{const r=await fetch(READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='
No saved books yet.
';return}list.innerHTML=all.map(rec=>{const total=rec.sentenceCount||0,synth=rec.synthCount||0,readPct=total?Math.round((rec.idx||0)/total*100):0,synthPct=total?Math.round(synth/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"";let h=0;const titleStr=rec.title||"Untitled";for(let i=0;i +`);for(let li=0;li{const span=e.target.closest(".reader-word");if(!span)return;const si=readerState.sentences.findIndex(s=>s.words.some(w=>w.el===span));si<0||(readerState.selecting?readerPickSentence(si):readerJumpTo(si))})}function readerSentBBox(sentence){var _a2,_b2;const pageIdx=(_b2=(_a2=sentence.words[0])==null?void 0:_a2.page)!=null?_b2:0,on=sentence.words.filter(w=>w.page===pageIdx);return{page:pageIdx,x:Math.min(...on.map(w=>w.x)),top:Math.min(...on.map(w=>w.top)),w:Math.max(...on.map(w=>w.x+w.w))-Math.min(...on.map(w=>w.x)),h:Math.max(...on.map(w=>w.top+w.h))-Math.min(...on.map(w=>w.top))}}function readerBuildUnitIndex(){readerState.unitsByPage=new Map,readerState.mode==="pdf"&&readerState.sentences.forEach((s,i)=>{var _a2,_b2;const pg=(_b2=(_a2=s.words[0])==null?void 0:_a2.page)!=null?_b2:0;readerState.unitsByPage.has(pg)||readerState.unitsByPage.set(pg,[]),readerState.unitsByPage.get(pg).push(i)})}function readerCreatePageStatus(pageIdx){var _a2;if(readerState.mode!=="pdf")return;const pg=readerState.pages[pageIdx];if(!pg)return;const idxs=((_a2=readerState.unitsByPage)==null?void 0:_a2.get(pageIdx))||[];for(const i of idxs){const s=readerState.sentences[i];if(!s||s._stat)continue;const el=document.createElement("div");el.className="reader-stat",el.dataset.si=i,pg.overlay.appendChild(el),s._stat=el,readerPaintStatus(i)}}function readerEffectiveStatus(s){return s.status}function readerSetStatus(idx,status2){var _a2;const s=readerState.sentences[idx];s&&(s.status=status2,status2!=="pending"&&!readerState.synthStarted&&(readerState.synthStarted=!0,(_a2=$("reader-doc"))==null||_a2.classList.add("reader-synth-started")),readerPaintStatus(idx))}function readerPaintStatus(idx){const s=readerState.sentences[idx];if(!s)return;const eff=readerEffectiveStatus(s);if(readerState.mode==="pdf"){if(!s._stat)return;const b=readerSentBBox(s),scale=readerState.scale;Object.assign(s._stat.style,{left:b.x*scale+"px",top:b.top*scale+"px",width:b.w*scale+"px",height:b.h*scale+"px"}),s._stat.className="reader-stat reader-stat-"+eff}else s.words.forEach(w=>{w.el&&(w.el.classList.remove("stat-pending","stat-synth","stat-ready","stat-reading"),w.el.classList.add("stat-"+eff))})}let _readerWordBox=null;function readerEnsureBox(){_readerWordBox||(_readerWordBox=document.createElement("div"),_readerWordBox.className="reader-hl-word")}function readerClearHighlights(){readerState.mode==="text"?document.querySelectorAll(".reader-word.is-word").forEach(el=>el.classList.remove("is-word")):_readerWordBox&&(_readerWordBox.hidden=!0)}function readerEnsureVisible(el){const doc=$("reader-doc");if(!doc||!el)return;const dr=doc.getBoundingClientRect(),er=el.getBoundingClientRect();if(er.topdr.bottom-50){const delta=er.top-dr.top-doc.clientHeight*.38;doc.scrollTo({top:doc.scrollTop+delta,behavior:"smooth"})}}function readerHighlightSentence(sentence){var _a2;sentence&&(readerState.mode==="text"?readerEnsureVisible((_a2=sentence.words[0])==null?void 0:_a2.el):sentence._stat&&readerEnsureVisible(sentence._stat))}function readerHighlightWord(sentence,wi){const w=sentence.words[wi];if(!w)return;if(readerState.mode==="text"){document.querySelectorAll(".reader-word.is-word").forEach(el=>el.classList.remove("is-word")),w.el&&(w.el.classList.add("is-word"),readerEnsureVisible(w.el));return}readerEnsureBox();const pg=readerState.pages[w.page];if(!pg)return;const scale=readerState.scale;_readerWordBox.parentNode!==pg.overlay&&pg.overlay.appendChild(_readerWordBox),Object.assign(_readerWordBox.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:w.w*scale+"px",height:w.h*scale+"px"}),_readerWordBox.hidden=!1,readerEnsureVisible(_readerWordBox)}function readerSetSelecting(on){readerState.selecting=!!on,readerState.selAnchored=!1;const doc=$("reader-doc");doc&&doc.classList.toggle("reader-selecting",readerState.selecting);const btn=$("reader-select-toggle");btn&&btn.classList.toggle("active",readerState.selecting);const lbl=$("reader-select-label");lbl&&(lbl.textContent=readerState.selecting?"Done":"Select range");const ic=$("reader-select-icon");ic&&(ic.className="mdi "+(readerState.selecting?"mdi-check":"mdi-cursor-default-click-outline")),readerSelHint(readerState.selecting?"Click the start sentence\u2026":""),readerState.selecting||readerPaintSelection()}function readerSelHint(html){const h=$("reader-sel-hint");h&&(h.innerHTML=html,h.hidden=!html)}function readerPickSentence(si){if(!readerState.selAnchored)readerState.selStart=si,readerState.selEnd=si,readerState.selAnchored=!0,readerSelHint("Start at sentence "+(si+1)+" \u2014 now click the end sentence");else{readerState.selEnd=si,readerState.selAnchored=!1;const r=readerSelRange();readerSelHint("Selected "+(r[1]-r[0]+1)+" sentences \u2014 click a new start, or Done")}readerPaintSelection(),readerUpdateScopeLabel()}function readerClearSelection(){readerState.selStart=readerState.selEnd=null,readerState.selAnchored=!1,readerPaintSelection(),readerUpdateScopeLabel(),readerState.selecting&&readerSelHint("Click the start sentence\u2026")}function readerSelRange(){return readerState.selStart===null||readerState.selEnd===null?null:[Math.min(readerState.selStart,readerState.selEnd),Math.max(readerState.selStart,readerState.selEnd)]}function readerPaintSelection(){const range=readerSelRange(),inSel=i=>range&&i>=range[0]&&i<=range[1],anchorI=readerState.selAnchored&&range&&range[0]===range[1]?range[0]:-1;readerState.sentences.forEach((s,i)=>{const sel=inSel(i)&&i!==anchorI,anchor=i===anchorI;readerState.mode==="pdf"?s._stat&&(s._stat.classList.toggle("sel",sel),s._stat.classList.toggle("sel-anchor",anchor)):s.words.forEach(w=>{w.el&&(w.el.classList.toggle("in-sel",sel),w.el.classList.toggle("sel-anchor",anchor))})})}function readerScopeIndices(){var _a2,_b2;const all=readerState.sentences.map((_,i)=>i),range=readerSelRange();if(range)return all.filter(i=>i>=range[0]&&i<=range[1]);if(readerState.mode==="pdf"){const n=readerState.pages.length;let from=parseInt((_a2=$("reader-page-from"))==null?void 0:_a2.value)||1,to=parseInt((_b2=$("reader-page-to"))==null?void 0:_b2.value)||n;return from=Math.max(1,Math.min(from,n)),to=Math.max(from,Math.min(to,n)),all.filter(i=>{var _a3,_b3;const p=((_b3=(_a3=readerState.sentences[i].words[0])==null?void 0:_a3.page)!=null?_b3:0)+1;return p>=from&&p<=to})}return all}function readerUpdateScopeLabel(){const scopeEl=$("reader-synth-scope"),range=readerSelRange();scopeEl&&(range?scopeEl.textContent="selection":readerState.mode==="pdf"?scopeEl.textContent="pages":scopeEl.textContent="all");const info=$("reader-sel-info"),txt=$("reader-sel-text");info&&(info.hidden=!range),txt&&range&&(txt.textContent="sentences "+(range[0]+1)+"\u2013"+(range[1]+1)+" ("+(range[1]-range[0]+1)+")")}function _readerTextForSynth(text){var _a2,_b2;const backend=((_a2=$("reader-backend-select"))==null?void 0:_a2.value)||"",emotion=((_b2=$("reader-emotion-select"))==null?void 0:_b2.value)||"";if(!/fish/i.test(backend)||!emotion||/^\s*\[/.test(text))return text;const tag=typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(emotion):"";return tag?`[${tag}] ${text}`:text}setTimeout(function(){const sel=$("reader-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=$("reader-backend-select"))==null?void 0:_a2.value)||"";if(!/fish/i.test(backend)){const instructInput=$("reader-instruct");instructInput&&sel.value&&(instructInput.value=sel.value)}}))},0);async function readerSynthIndices(targets){var _a2,_b2,_c2;if(targets=targets.filter(i=>!readerState.blobCache.has(i)),!targets.length||readerState.synthRunning)return{done:0,failed:[]};const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice)return toast("Pick a voice first","error"),{done:0,failed:[]};if(!backend)return toast("No TTS backend selected","error"),{done:0,failed:[]};const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.synthRunning=!0,readerState.synthCancel=!1;const prog=$("reader-synth-prog");prog&&(prog.hidden=!1);const total=targets.length;let done=0;const failed=[],update=()=>{const f=$("reader-synth-fill");f&&(f.style.width=done/total*100+"%");const l=$("reader-synth-label");l&&(l.textContent=done+" / "+total)};update();try{const queue=targets.slice(),worker=async()=>{var _a3;for(;queue.length&&!readerState.synthCancel;){const i=queue.shift();if(readerState.blobCache.has(i)){done++,update();continue}readerState.sentences[i].status==="pending"&&readerSetStatus(i,"synth");try{const blob=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[i].text),READER_FMT,instruct,backend,!1,readerGenParams());readerState.blobCache.set(i,blob),readerState.sentences[i].status==="synth"&&readerSetStatus(i,"ready")}catch(e){failed.push(i),((_a3=readerState.sentences[i])==null?void 0:_a3.status)==="synth"&&readerSetStatus(i,"pending"),console.error("[reader] synth failed for sentence",i,e)}done++,update()}},N=Math.min(2,targets.length);await Promise.all(Array.from({length:N},worker))}finally{readerState.synthRunning=!1,prog&&(prog.hidden=!0)}return{done,failed}}async function readerSynthAll(){const targets=readerScopeIndices().filter(i=>!readerState.blobCache.has(i));if(!targets.length){toast("Selected range is already synthesised","success");return}const{done,failed}=await readerSynthIndices(targets);if(readerState.synthCancel){toast("Synthesis cancelled ("+done+" done)","error");return}if(failed.length){toast(done-failed.length+" / "+done+" sentences synthesised \u2014 "+failed.length+" failed, see red markers","error");return}toast("Synthesised "+done+" sentences","success")}function readerSafeName(s){return(s||"audio").replace(/[\/\\:*?"<>|]+/g,"_").replace(/\s+/g," ").trim().slice(0,60)||"audio"}function readerPad(n){return String(n).padStart(2,"0")}function readerPageOf(i){var _a2,_b2;return readerState.mode==="pdf"?((_b2=(_a2=readerState.sentences[i].words[0])==null?void 0:_a2.page)!=null?_b2:0)+1:1}function readerDownload(blob,filename){const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),setTimeout(()=>{URL.revokeObjectURL(a.href),a.remove()},1500)}const _readerDelay=ms=>new Promise(r=>setTimeout(r,ms));async function readerExport(mode){if(!readerState.sentences.length){toast("Import a document first","error");return}const indices=readerScopeIndices(),missing=indices.filter(i=>!readerState.blobCache.has(i));let failedCount=0;if(missing.length){toast("Synthesising "+missing.length+" missing sentence(s) before export\u2026","success");const{failed}=await readerSynthIndices(missing);if(readerState.synthCancel){toast("Export cancelled","error");return}failedCount=failed.length}const ready=indices.filter(i=>readerState.blobCache.has(i));if(!ready.length){toast("Nothing to export","error");return}if(failedCount){toast(failedCount+" sentence(s) failed to synthesise \u2014 fix them (see red markers) before exporting, or missing audio will silently drop from the file","error");return}const title=readerSafeName(readerState.title);if(mode==="sentence"){const perPage={};for(const i of ready){const pg=readerPageOf(i);perPage[pg]=(perPage[pg]||0)+1;const name=readerState.mode==="pdf"?`${title} - p${readerPad(pg)} - ${readerPad(perPage[pg])}.mp3`:`${title} - ${readerPad(perPage[pg])}.mp3`;readerDownload(readerState.blobCache.get(i),name),await _readerDelay(350)}toast("Exported "+ready.length+" MP3 files","success");return}const byPage=new Map;ready.forEach(i=>{const pg=readerPageOf(i);byPage.has(pg)||byPage.set(pg,[]),byPage.get(pg).push(i)});let files=0;for(const[pg,idxs]of[...byPage.entries()].sort((a,b)=>a[0]-b[0])){const blob=new Blob(idxs.map(i=>readerState.blobCache.get(i)),{type:"audio/mpeg"}),name=readerState.mode==="pdf"?`${title} - p${readerPad(pg)}.mp3`:`${title}.mp3`;readerDownload(blob,name),files++,await _readerDelay(400)}toast("Exported "+files+(readerState.mode==="pdf"?" page MP3 file(s)":" MP3"),"success")}function readerRechunk(mode){readerState.chunkMode=mode,readerState.baseSentences.length&&(readerStopPlayback(),readerState.blobCache.clear(),readerState.bufCache.clear(),readerState.gainCache.clear(),readerState.savedAudioIdx=new Set,readerState.pages.forEach(p=>p.overlay.querySelectorAll(".reader-stat").forEach(e=>e.remove())),readerClearSelection(),readerState.sentences=readerGroupUnits(readerState.baseSentences,mode),readerBuildUnitIndex(),readerState.pages.forEach((p,i)=>{p.rendered&&readerCreatePageStatus(i)}),readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),readerState.idx=0,readerUpdateScopeLabel(),readerUpdateProgress(),readerHighlightSentence(readerState.sentences[0]),toast("Voice consistency: "+mode+" \u2014 audio cleared, re-synthesise","success"))}function readerGenParams(){var _a2,_b2,_c2;const out={},seed=(_a2=$("reader-seed"))==null?void 0:_a2.value.trim(),temp=(_b2=$("reader-temp"))==null?void 0:_b2.value.trim(),nspd=(_c2=$("reader-tts-speed"))==null?void 0:_c2.value.trim();return seed!==""&&seed!=null&&!isNaN(+seed)&&(out.seed=parseInt(seed,10)),temp!==""&&temp!=null&&!isNaN(+temp)&&(out.temperature=parseFloat(temp)),nspd!==""&&nspd!=null&&!isNaN(+nspd)&&parseFloat(nspd)!==1&&(out.speed=parseFloat(nspd)),Object.keys(out).length?out:null}function readerComputeGain(idx,buf){if(readerState.gainCache.has(idx))return readerState.gainCache.get(idx);const ch=buf.getChannelData(0),step=Math.max(1,Math.floor(ch.length/8e3));let sum=0,n=0,peak=1e-6;for(let i=0;ipeak&&(peak=Math.abs(v))}let gain=.1/(Math.sqrt(sum/Math.max(1,n))||1e-4);return gain=Math.max(.5,Math.min(gain,4)),gain=Math.min(gain,.99/peak),readerState.gainCache.set(idx,gain),gain}async function readerFetchServerAudio(idx){if(!readerState.savedId||!readerState.savedAudioIdx.has(idx))return null;try{const r=await fetch(`${READER_API}/${readerState.savedId}/audio/${idx}`);if(r.ok){const b=await r.blob();return readerState.blobCache.set(idx,b),b}}catch{}return null}async function readerGetBuffer(idx){var _a2,_b2,_c2;if(readerState.bufCache.has(idx))return readerState.bufCache.get(idx);let blob=readerState.blobCache.get(idx)||await readerFetchServerAudio(idx);if(!blob){const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice)throw new Error("Pick a voice first");if(!backend)throw new Error("No TTS backend selected");const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.sentences[idx].status!=="reading"&&readerSetStatus(idx,"synth"),blob=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[idx].text),READER_FMT,instruct,backend,!1,readerGenParams()),readerState.blobCache.set(idx,blob),readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"ready")}const buf=await readerCtx().decodeAudioData(await blob.arrayBuffer());return readerState.bufCache.set(idx,buf),buf}function readerEvictBuffers(center){if(!(readerState.bufCache.size<=READER_BUF_WINDOW*2+1))for(const i of readerState.bufCache.keys())Math.abs(i-center)>READER_BUF_WINDOW&&readerState.bufCache.delete(i)}async function readerPrefetch(idx){var _a2,_b2,_c2;if(!(idx<0||idx>=readerState.sentences.length)){if(!readerState.blobCache.has(idx)&&!await readerFetchServerAudio(idx)){const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice||!backend)return;const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.sentences[idx].status==="pending"&&readerSetStatus(idx,"synth");try{const b=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[idx].text),READER_FMT,instruct,backend,!1,readerGenParams());readerState.blobCache.set(idx,b),readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"ready")}catch{readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"pending");return}}if(!readerState.bufCache.has(idx)&&Math.abs(idx-readerState.idx)<=READER_BUF_WINDOW)try{readerState.bufCache.set(idx,await readerCtx().decodeAudioData(await readerState.blobCache.get(idx).arrayBuffer()))}catch{}}}function readerClearReading(){const i=readerState.readingIdx;i>=0&&i=readerState.sentences.length){readerStopPlayback(),readerState.idx=0,readerUpdateProgress();return}const idx=readerState.idx,sentence=readerState.sentences[idx];readerUpdateProgress(),readerSaveResume(),readerState.readingIdx=idx,readerSetStatus(idx,"reading"),readerHighlightSentence(sentence);let buf;try{buf=await readerGetBuffer(idx)}catch(e){toast(e.message||String(e),"error"),readerSetStatus(idx,"pending"),readerStopPlayback();return}if(!readerState.playing||readerState.idx!==idx)return;readerEvictBuffers(idx);const timings=computeWordTimings(sentence.text,buf.duration),ctx=readerCtx();readerStopSource();const src=ctx.createBufferSource();if(src.buffer=buf,src.playbackRate.value=readerState.speed,readerState.normalize){const g=ctx.createGain();g.gain.value=readerComputeGain(idx,buf),src.connect(g),g.connect(ctx.destination)}else src.connect(ctx.destination);readerState.currentSource=src;const t0=ctx.currentTime;readerPrefetch(idx+1),src.onended=()=>{readerState.currentSource===src&&(readerState.currentSource=null,readerState.raf&&(cancelAnimationFrame(readerState.raf),readerState.raf=null),readerSetStatus(idx,"ready"),readerState.readingIdx===idx&&(readerState.readingIdx=-1),readerState.playing&&(readerState.idx++,readerPlayCurrent()))},src.start(0);const tick=()=>{if(readerState.currentSource!==src)return;const elapsed=(ctx.currentTime-t0)*readerState.speed;let active=0;for(let i=timings.length-1;i>=0;i--)if(elapsed>=timings[i].start){active=i;break}readerHighlightWord(sentence,Math.min(active,sentence.words.length-1)),readerState.raf=requestAnimationFrame(tick)};readerState.raf=requestAnimationFrame(tick)}function readerStopSource(){if(readerState.currentSource){try{readerState.currentSource.onended=null,readerState.currentSource.stop(0)}catch{}readerState.currentSource=null}readerState.raf&&(cancelAnimationFrame(readerState.raf),readerState.raf=null)}function readerStopPlayback(){readerState.playing=!1,readerStopSource(),readerClearHighlights(),readerClearReading(),readerUpdatePlayBtn(),typeof window.setNavBusy=="function"&&window.setNavBusy("s-reader",!1)}function readerPlay(){var _a2;if(!readerState.sentences.length){toast("Import a document first","error");return}if(!((_a2=$("reader-voice-select"))!=null&&_a2.value)){toast("Pick a voice first","error");return}readerCtx(),readerState.playing=!0,readerUpdatePlayBtn(),typeof window.setNavBusy=="function"&&window.setNavBusy("s-reader",!0),readerPlayCurrent()}function readerPause(){readerState.playing=!1,readerStopSource(),readerSaveResume(),readerPersistProgress(),readerClearReading(),readerUpdatePlayBtn()}function readerJumpTo(idx){readerState.idx=Math.max(0,Math.min(idx,readerState.sentences.length-1)),readerStopSource(),readerClearHighlights(),readerClearReading(),readerUpdateProgress(),readerSaveResume(),readerState.playing?readerPlayCurrent():readerHighlightSentence(readerState.sentences[readerState.idx])}function readerUpdatePlayBtn(){const btn=$("reader-play");if(!btn)return;const ic=btn.querySelector(".mdi");ic&&(ic.className="mdi "+(readerState.playing?"mdi-pause":"mdi-play"))}function readerUpdateProgress(){const total=readerState.sentences.length,cur=total?readerState.idx+1:0,lbl=$("reader-progress-label");lbl&&(lbl.textContent=cur+" / "+total);const fill=$("reader-progress-fill");fill&&(fill.style.width=(total?readerState.idx/total*100:0)+"%")}function readerSaveResume(){if(!(!readerState.title||!readerState.sentences.length))try{localStorage.setItem(READER_RESUME_KEY,JSON.stringify({title:readerState.title,idx:readerState.idx,total:readerState.sentences.length}))}catch{}}function readerLoadResume(){try{const r=JSON.parse(localStorage.getItem(READER_RESUME_KEY)||"null");if(r&&r.title===readerState.title&&r.idx>0&&r.idx({}))).detail||r.statusText);const fresh=!readerState.savedId;if(readerState.savedId=(await r.json()).id,fresh||!readerState.sourceUploaded){const ext=readerState.mode==="pdf"?"pdf":"txt",body=readerState.mode==="pdf"?readerState.fileBlob:new Blob([readerState.docText||""],{type:"text/plain"});(await fetch(`${READER_API}/${readerState.savedId}/source?ext=${ext}`,{method:"PUT",body})).ok&&(readerState.sourceUploaded=!0)}let uploaded=0;for(let i=0;i{const file=inp.files[0];if(!file)return;if(!(await fetch(`${READER_API}/${id}/source?ext=pdf`,{method:"PUT",body:file})).ok){toast("Re-upload failed","error");return}toast("PDF restored \u2014 opening\u2026","success"),readerState.savedId=id,readerState.sourceUploaded=!0,readerState.fileBlob=file,await readerLoadPdf(file)},inp.click(),toast("PDF source missing \u2014 please re-select the original file","error");return}readerState.fileBlob=sourceBlob,await readerLoadPdf(sourceBlob)}else{const text=sourceBlob?await sourceBlob.text():rec.text||"";readerState.docText=text,await readerLoadText(text)}}catch(e){toast("Open failed: "+(e.message||e),"error");return}if(!readerState.sentences.length){toast("Document had no readable text","error");return}readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),readerState.savedId=id,readerState.sourceUploaded=!0,readerState.savedAudioIdx=new Set,rec.sentenceCount===readerState.sentences.length?(rec.audioIdx||[]).forEach(i=>{io.value===value)){const o=document.createElement("option");o.value=o.textContent=value,sel.appendChild(o)}sel.value=value,id==="reader-voice-select"&&window.VoicePicker&&VoicePicker.setValue(id,value)}}async function readerRenderLibrary(){const card=$("reader-library-card"),list=$("reader-lib-list");if(!card||!list)return;let all=[];try{const r=await fetch(READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='
No saved books yet.
';return}list.innerHTML=all.map(rec=>{const total=rec.sentenceCount||0,synth=rec.synthCount||0,readPct=total?Math.round((rec.idx||0)/total*100):0,synthPct=total?Math.round(synth/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"";let h=0;const titleStr=rec.title||"Untitled";for(let i=0;i
diff --git a/static/index.html b/static/index.html index 095b9c0..90518fb 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/conversation.js b/static/js/conversation.js index 8c7a497..7d60c8f 100644 --- a/static/js/conversation.js +++ b/static/js/conversation.js @@ -154,8 +154,8 @@ $('s-import-voices-file')?.addEventListener('change', async function () { // decides server-side (see _conv_tts_text_and_instruct in // routes/conversation.py) whether it becomes an inline Fish [tag] or an // instruct-field phrase for style-aware backends. - if (ttsEmotionSel && typeof REH_EMOTIONS !== 'undefined' && !ttsEmotionSel.dataset.populated) { - REH_EMOTIONS.forEach(e => { + if (ttsEmotionSel && typeof window.REH_EMOTIONS !== 'undefined' && !ttsEmotionSel.dataset.populated) { + window.REH_EMOTIONS.forEach(e => { if (!e.value) return; const o = document.createElement('option'); o.value = e.value; diff --git a/static/js/reader.js b/static/js/reader.js index 69f9322..8cf2ef6 100644 --- a/static/js/reader.js +++ b/static/js/reader.js @@ -1149,10 +1149,14 @@ function _readerTextForSynth(text) { const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotion) : ''; return tag ? `[${tag}] ${text}` : text; } -(function initReaderEmotionPicker() { +// Deferred to a macrotask — see the matching note in tts-preview.js: REH_EMOTIONS is +// a `const` declared later in the bundle's single shared scope, and `typeof` on a +// const in its temporal dead zone THROWS rather than returning "undefined", which +// aborts the rest of the bundle's initialization. +setTimeout(function initReaderEmotionPicker() { const sel = $('reader-emotion-select'); - if (!sel || typeof REH_EMOTIONS === 'undefined') return; - REH_EMOTIONS.forEach(e => { + if (!sel || typeof window.REH_EMOTIONS === 'undefined') return; + window.REH_EMOTIONS.forEach(e => { if (!e.value) return; const o = document.createElement('option'); o.value = e.value; @@ -1166,7 +1170,7 @@ function _readerTextForSynth(text) { if (instructInput && sel.value) instructInput.value = sel.value; } }); -})(); +}, 0); async function readerSynthIndices(targets) { targets = targets.filter(i => !readerState.blobCache.has(i)); diff --git a/static/js/rehearser.js b/static/js/rehearser.js index 030f826..916cb77 100644 --- a/static/js/rehearser.js +++ b/static/js/rehearser.js @@ -26,6 +26,11 @@ const REH_EMOTIONS = [ { value: 'grieving, tearful, broken', emoji: '😭', label: 'Grieving' }, ]; +// Exposed so the emotion quick-pickers in tts-preview.js / reader.js can read this +// list. They run in the same bundle scope but BEFORE this file, so they must go +// through window (and defer to a macrotask) rather than touch the const directly. +window.REH_EMOTIONS = REH_EMOTIONS; + // Load custom emotions from localStorage let rehCustomEmotions = []; try { rehCustomEmotions = JSON.parse(localStorage.getItem('reh-custom-emotions') || '[]'); } catch(_) {} @@ -1876,7 +1881,19 @@ function _buildInstruct(voiceProfile, emotion, voiceId) { const langCode = String(voiceId || '').split('_')[0].toUpperCase(); const accent = _buildAccentClause(langCode); if (!e && !p && !accent) return ''; - const tmpl = _BUILD_INSTRUCT_TEMPLATES[langCode] || _BUILD_INSTRUCT_TEMPLATES.EN; + // The emotion clause is now ALWAYS built in English, regardless of the voice's + // own language — confirmed via controlled A/B testing (same line, same voice, + // varying only instruct language) that Qwen3-TTS's emotional differentiation is + // dramatically stronger in English than German: sad/angry/shocked/happy formed a + // clean, coherent pitch gradient (274Hz -> 398Hz) in English, but stayed muddled + // together in German even with the exact same wording translated. The spoken + // TEXT and the accent clause below stay fully native-language — only the + // emotion instruction itself changes language. The raw per-line emotion is + // LLM-generated in the book's own language (see the casting prompt's "1-2 + // deutsche Wörter"), so translate it before building the clause. + const emotionEn = e && langCode !== 'EN' && typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(e) : ''; + const effectiveEmotion = emotionEn || e; + const emotionTmpl = _BUILD_INSTRUCT_TEMPLATES.EN; // Emotion leads, accent trails — Qwen3-TTS's own prompting guidance warns // it "does not follow instructions correctly when dealing with // conflicting attributes... favoring one over the other." Putting the @@ -1886,7 +1903,7 @@ 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 ? tmpl(e) : '', p, accent].filter(Boolean); + const parts = [e ? emotionTmpl(effectiveEmotion) : '', p, accent].filter(Boolean); return parts.join(' '); } diff --git a/static/js/tts-preview.js b/static/js/tts-preview.js index a95bd31..798d736 100644 --- a/static/js/tts-preview.js +++ b/static/js/tts-preview.js @@ -220,10 +220,17 @@ function _ttsApplyEmotionTag(text, emotionValue) { const tag = typeof _rehEmotionEnglishTag === 'function' ? _rehEmotionEnglishTag(emotionValue) : ''; return tag ? `[${tag}] ${text}` : text; } -(function initPreviewEmotionPicker() { +// Deferred to a macrotask: REH_EMOTIONS is a `const` declared in rehearser.js, +// which the minified bundle concatenates into ONE shared scope AFTER this file. +// A bare `typeof REH_EMOTIONS` here would not return "undefined" — for a const in +// its temporal dead zone it THROWS, aborting the rest of the bundle's top-level +// initialization (confirmed live: it left every later module's consts permanently +// uninitialized). Running after the current task guarantees the whole bundle has +// finished executing, so the const is initialized either way. +setTimeout(function initPreviewEmotionPicker() { const sel = $('preview-emotion-select'); - if (!sel || typeof REH_EMOTIONS === 'undefined') return; - REH_EMOTIONS.forEach(e => { + if (!sel || typeof window.REH_EMOTIONS === 'undefined') return; + window.REH_EMOTIONS.forEach(e => { if (!e.value) return; const o = document.createElement('option'); o.value = e.value; @@ -243,7 +250,7 @@ function _ttsApplyEmotionTag(text, emotionValue) { if (styleInput && sel.value) styleInput.value = sel.value; } }); -})(); +}, 0); $('tts-backend-select').addEventListener('change', () => { const sel = $('tts-voice-select');