diff --git a/CHANGELOG.md b/CHANGELOG.md index 637258a..4f461ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ## [Unreleased] ### Changed +- **Section-scoped feature loading.** The shared module manifest now builds a 17-module foundation bundle and loads + Rehearser, Reader/Audiobook, Library/Characters, and Studio code only when their sections are opened. Repeated + navigation reuses loaded modules, while deep links load their group before activation. - **P1 Roadmap status.** Documented the completed namespace/manifest foundations and durable background artifacts, while keeping true lazy feature loading, responsibility-level frontend extraction, provider-health consolidation, full workload measurements, and browser image-cache eviction explicitly open. diff --git a/TODO.md b/TODO.md index 9d424b2..7d7415a 100644 --- a/TODO.md +++ b/TODO.md @@ -20,9 +20,9 @@ the first block before investing in architecture or polish. All P0 safety, security, data-integrity, and release-protection work is complete. P1 implementation is active: pagination, shared voice caching, bounded decoded-audio caching, background jobs, canonical character identity, forms, and the frontend -namespace/manifest foundations are working and regression-tested. Remaining P1 work -is intentionally open where it needs a real architecture change or workload -measurement: true feature-module lazy loading, the larger Audiobook/Rehearser/style +namespace/manifest foundations and section-scoped lazy loading are working and +regression-tested. Remaining P1 work is intentionally open where it needs a real +architecture change or workload measurement: the larger Audiobook/Rehearser/style split, centralized provider-health handling, full constrained-device workloads, and browser image/profile-cache eviction. @@ -89,9 +89,9 @@ browser image/profile-cache eviction. - [x] **L — Add character forms inside one character record.** `sheet.forms[]` stores bounded alternate appearances, prompts, and per-form visual assets; the detail-page switcher preserves one character ID and voice, and selected forms feed prompt generation and identity fingerprints. -- [ ] **L — Modularize the frontend.** The shared manifest and compatibility namespace - bridge are complete foundations; responsibility-level extraction, true feature-module - lazy loading, and the Audiobook/Rehearser/style split remain. +- [ ] **L — Modularize the frontend.** The shared manifest, compatibility namespace + bridge, and section-scoped lazy loading are complete foundations; responsibility-level + extraction and the Audiobook/Rehearser/style split remain. ### P2 — after behavior and safety stabilize @@ -132,7 +132,9 @@ browser image/profile-cache eviction. ## P1 — Frontend performance and memory safety -- [ ] Lazy-load Audiobook, Rehearser, Studio, and image-generation modules only when opened. +- [x] Lazy-load Audiobook, Rehearser, Studio, and image-generation modules only when opened. The shared manifest + keeps the foundation in the core bundle and loads Reader/Audiobook, Rehearser, Library/Characters, and Studio + groups once on first navigation; deep links load their group before activation. - [x] Paginate large character and voice tables; render approximately 25–50 visible rows, with a constrained-device regression covering a 250-character production. - [x] Cache `/api/voices` with a shared 30-second session cache, one in-flight request, explicit refresh, and diff --git a/VERSION b/VERSION index afd2e72..0f089f4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.15 +1.24.16 diff --git a/scripts/check-frontend.mjs b/scripts/check-frontend.mjs index 8a39997..377d85e 100644 --- a/scripts/check-frontend.mjs +++ b/scripts/check-frontend.mjs @@ -7,7 +7,8 @@ import { join } from 'node:path'; import { fileURLToPath } from 'node:url'; const root = join(fileURLToPath(new URL('.', import.meta.url)), '..'); -const modules = JSON.parse(readFileSync(join(root, 'static', 'module-manifest.json'), 'utf8')); +const manifest = JSON.parse(readFileSync(join(root, 'static', 'module-manifest.json'), 'utf8')); +const modules = [...new Set([...(manifest.core || []), ...Object.values(manifest.lazy || {}).flat()])]; const seenModules = new Set(); const declarations = new Map(); const declarationPattern = /^(?:async\s+)?(?:function|class|const|let|var)\s+([A-Za-z_$][\w$]*)/gm; diff --git a/scripts/minify.mjs b/scripts/minify.mjs index 239f801..da7a9af 100644 --- a/scripts/minify.mjs +++ b/scripts/minify.mjs @@ -18,8 +18,11 @@ const jsDir = join(root, 'static', 'js'); const outDir = join(root, 'static', 'dist'); mkdirSync(outDir, { recursive: true }); -// Must match loader.js batch C order exactly. -const MODULES = JSON.parse(readFileSync(join(root, 'static', 'module-manifest.json'), 'utf8')); +// Bundle only the foundation. Section-specific modules are loaded on demand by +// loader.js; keeping the same manifest as the runtime prevents drift. +const MANIFEST = JSON.parse(readFileSync(join(root, 'static', 'module-manifest.json'), 'utf8')); +const MODULES = MANIFEST.core; +if (!Array.isArray(MODULES) || !MODULES.length) throw new Error('module manifest has no core modules'); const MAIN = MODULES.map(n => join(jsDir, n + '.js')); const source = MAIN.map(f => `\n/* ==== ${f.split('/').pop()} ==== */\n` + readFileSync(f, 'utf8')).join('\n'); diff --git a/static/dist/main.min.js b/static/dist/main.min.js index 1643905..963be76 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -1,4 +1,4 @@ -var __defProp=Object.defineProperty;var __defNormalProp=(obj,key,value)=>key in obj?__defProp(obj,key,{enumerable:!0,configurable:!0,writable:!0,value}):obj[key]=value;var __publicField=(obj,key,value)=>__defNormalProp(obj,typeof key!="symbol"?key+"":key,value);var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_z,_A,_B,_C,_D,_E,_F,_G,_H,_I,_J,_K,_L,_M,_N,_O,_P,_Q,_R,_S,_T,_U,_V,_W,_X,_Y,_Z,__,_$,_aa,_ba,_ca,_da,_ea,_fa,_ga,_ha,_ia,_ja,_ka,_la,_ma,_na,_oa,_pa,_qa,_ra,_sa,_ta,_ua,_va,_wa,_xa,_ya,_za,_Aa,_Ba,_Ca,_Da,_Ea,_Fa,_Ga,_Ha,_Ia,_Ja,_Ka,_La,_Ma,_Na,_Oa,_Pa,_Qa,_Ra,_Sa,_Ta,_Ua,_Va,_Wa,_Xa,_Ya,_Za,__a,_$a,_ab,_bb,_cb,_db,_eb,_fb,_gb,_hb,_ib,_jb,_kb,_lb,_mb,_nb,_ob,_pb,_qb,_rb,_sb,_tb,_ub,_vb,_wb,_xb,_yb,_zb,_Ab,_Bb,_Cb,_Db,_Eb,_Fb,_Gb,_Hb,_Ib,_Jb,_Kb,_Lb,_Mb,_Nb,_Ob,_Pb,_Qb,_Rb,_Sb,_Tb,_Ub,_Vb,_Wb,_Xb,_Yb,_Zb,__b,_$b,_ac,_bc,_cc,_dc,_ec,_fc,_gc,_hc,_ic,_jc,_kc,_lc,_mc,_nc,_oc,_pc,_qc,_rc,_sc,_tc,_uc,_vc,_wc,_xc,_yc,_zc,_Ac;(function(){"use strict";const _pickers={};function _voiceData(id){return(window._voices||[]).find(v=>v.id===id)||null}function _voiceLang(v,id){const raw=String((v==null?void 0:v.lang)||(v==null?void 0:v.language)||"").trim(),fromMeta=raw&&raw.length<=5?raw:"",fromId=String(id||"").split("_")[0]||"";return(fromMeta||fromId).toUpperCase()}function _voiceGender(v,id){const raw=String((v==null?void 0:v.gender)||(v==null?void 0:v.sex)||"").trim(),first=raw?raw.charAt(0).toUpperCase():"";if(["F","M","N"].includes(first))return first;const fromId=(String(id||"").split("_")[1]||"").charAt(0).toUpperCase();return["F","M","N"].includes(fromId)?fromId:""}function _voiceMetaLabel(id){const v=_voiceData(id);return[_voiceGender(v,id),_voiceLang(v,id)].filter(Boolean).join(" ")}function _voiceOptionLabel(id){const meta=_voiceMetaLabel(id);return meta?`${id} ${meta}`:id}const VOICE_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"},VOICE_AVATAR_COLORS={male:"#3b82f6",female:"#ec4899",neutral:"#6b7280",robot:"#0ea5e9",animal:"#f59e0b"};window.VOICE_AVATAR_ICONS=VOICE_AVATAR_ICONS,window.voiceAvatarIcon=function(avatarKey,size){const icon=VOICE_AVATAR_ICONS[avatarKey];if(!icon)return null;const s=size+"px",r=Math.round(size/2)+"px",bg=VOICE_AVATAR_COLORS[avatarKey]||"#6b7280";return``};function _avatarHtml(id,size){const v=_voiceData(id),s=size+"px",r=Math.round(size/2)+"px";if(v!=null&&v.has_picture)return``;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(v==null?void 0:v.avatar,size):null;if(icon)return icon;const lang=(v==null?void 0:v.lang)||"",color=_langColor(lang,id),init=(id||"?")[0].toUpperCase();return`${init}`}function _langColor(lang,id){const str=(lang||id||"").toLowerCase();if(str.startsWith("de"))return"#3b82f6";if(str.startsWith("en"))return"#10b981";if(str.startsWith("fr"))return"#8b5cf6";if(str.startsWith("es"))return"#f59e0b";if(str.startsWith("it"))return"#ef4444";if(str.startsWith("zh"))return"#ec4899";if(str.startsWith("ja"))return"#f97316";const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i>>0;return palette[h%palette.length]}function _flagSpan(v){return v&&v.flag?`${v.flag}`:""}function _buildItem(id,label){const v=_voiceData(id),meta=_voiceMetaLabel(id),name=id||label||"";return`
+var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_z,_A,_B,_C,_D,_E,_F,_G,_H,_I,_J,_K,_L,_M,_N,_O,_P,_Q,_R,_S,_T,_U,_V,_W,_X,_Y,_Z,__,_$,_aa,_ba,_ca,_da,_ea,_fa,_ga,_ha,_ia,_ja,_ka,_la,_ma,_na,_oa;(function(){"use strict";const _pickers={};function _voiceData(id){return(window._voices||[]).find(v=>v.id===id)||null}function _voiceLang(v,id){const raw=String((v==null?void 0:v.lang)||(v==null?void 0:v.language)||"").trim(),fromMeta=raw&&raw.length<=5?raw:"",fromId=String(id||"").split("_")[0]||"";return(fromMeta||fromId).toUpperCase()}function _voiceGender(v,id){const raw=String((v==null?void 0:v.gender)||(v==null?void 0:v.sex)||"").trim(),first=raw?raw.charAt(0).toUpperCase():"";if(["F","M","N"].includes(first))return first;const fromId=(String(id||"").split("_")[1]||"").charAt(0).toUpperCase();return["F","M","N"].includes(fromId)?fromId:""}function _voiceMetaLabel(id){const v=_voiceData(id);return[_voiceGender(v,id),_voiceLang(v,id)].filter(Boolean).join(" ")}function _voiceOptionLabel(id){const meta=_voiceMetaLabel(id);return meta?`${id} ${meta}`:id}const VOICE_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"},VOICE_AVATAR_COLORS={male:"#3b82f6",female:"#ec4899",neutral:"#6b7280",robot:"#0ea5e9",animal:"#f59e0b"};window.VOICE_AVATAR_ICONS=VOICE_AVATAR_ICONS,window.voiceAvatarIcon=function(avatarKey,size){const icon=VOICE_AVATAR_ICONS[avatarKey];if(!icon)return null;const s=size+"px",r=Math.round(size/2)+"px",bg=VOICE_AVATAR_COLORS[avatarKey]||"#6b7280";return``};function _avatarHtml(id,size){const v=_voiceData(id),s=size+"px",r=Math.round(size/2)+"px";if(v!=null&&v.has_picture)return``;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(v==null?void 0:v.avatar,size):null;if(icon)return icon;const lang=(v==null?void 0:v.lang)||"",color=_langColor(lang,id),init=(id||"?")[0].toUpperCase();return`${init}`}function _langColor(lang,id){const str=(lang||id||"").toLowerCase();if(str.startsWith("de"))return"#3b82f6";if(str.startsWith("en"))return"#10b981";if(str.startsWith("fr"))return"#8b5cf6";if(str.startsWith("es"))return"#f59e0b";if(str.startsWith("it"))return"#ef4444";if(str.startsWith("zh"))return"#ec4899";if(str.startsWith("ja"))return"#f97316";const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i>>0;return palette[h%palette.length]}function _flagSpan(v){return v&&v.flag?`${v.flag}`:""}function _buildItem(id,label){const v=_voiceData(id),meta=_voiceMetaLabel(id),name=id||label||"";return`
${_avatarHtml(id,26)} ${_esc(name)} ${meta?`${_esc(meta)}`:_flagSpan(v)} @@ -875,967 +875,4 @@ This warms each voice so the engine caches its .pt and first playback is instant ${b.speed==null?"\u2014":Number(b.speed).toFixed(2)+"x"} ${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}(_da=$("stt-tts-file"))==null||_da.addEventListener("change",async()=>{const input=$("stt-tts-file");input.files&&input.files.length&&await sttTtsUploadFile(input.files[0]),input.value=""}),(_ea=$("stt-tts-refresh-stt-btn"))==null||_ea.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}}),(_fa=$("stt-tts-stt-backend"))==null||_fa.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}(_ga=$("stt-tts-rec-start"))==null||_ga.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")}}),(_ha=$("stt-tts-rec-stop"))==null||_ha.addEventListener("click",()=>{sttTtsRecorder&&sttTtsRecorder.state!=="inactive"&&sttTtsRecorder.stop()}),(_ia=$("stt-tts-transcribe-btn"))==null||_ia.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}}),(_ja=$("stt-tts-copy-preview-btn"))==null||_ja.addEventListener("click",()=>{const text=$("stt-tts-text").value.trim();text&&($("preview-text-area").value=text,switchTab("generation"),toast("Copied transcription to TTS Generation","success"))}),(_ka=$("stt-tts-backend-select"))==null||_ka.addEventListener("change",()=>{$("stt-tts-voice-select").innerHTML='',sttTtsOutputBlob=null,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0,updateBackendHelp()}),(_la=$("stt-tts-fetch-voices-btn"))==null||_la.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}}),(_ma=$("stt-tts-generate-btn"))==null||_ma.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}}),(_na=$("stt-tts-save-mp3-btn"))==null||_na.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}}),(_oa=$("stt-tts-save-wav-btn"))==null||_oa.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"}];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 _RehAudioCache=class _RehAudioCache extends Map{get(key){if(!super.has(key))return;const value=super.get(key);return super.delete(key),super.set(key,value),value}set(key,value){for(super.delete(key),super.set(key,value);this.size>_RehAudioCache.MAX_ENTRIES;){let oldest=this.keys().next().value;const current=typeof rehState!="undefined"?rehState.lineIndex:null;if(oldest===current&&this.size>1){const iterator=this.keys();iterator.next(),oldest=iterator.next().value}super.delete(oldest)}return this}};__publicField(_RehAudioCache,"MAX_ENTRIES",128);let RehAudioCache=_RehAudioCache;const rehState={lines:[],cast:{},lineIndex:0,clips:[],voices:[],backend:"",playing:!1,repeat:!1,savedId:null,synthCache:new RehAudioCache,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+` -`;case"action":return` -`+line.text+` -`;case"dialog":return` -`+line.speaker+` -`+line.text+` -`;case"direction":return line.speaker?line.text+` -`:` -`+line.text+` -`;case"pagebreak":return line.page?` -\f${line.page} -`:` -\f -`;default:return""}}).join("").trim()}async function saveToLibrary(){const rec=rehCurrentRecord();if(rehState.savedId)rec.id=rehState.savedId,rec.created||(rec.created=new Date),await rehDbPut(rec);else{rec.created=new Date;const newId=await rehDbAdd(rec);rehState.savedId=newId}if(toast("Saved to library","success"),rehState._keepSavedIdOnce=!0,typeof castWriteBack=="function")try{await castWriteBack(rec.title,rehState.cast)}catch{}await renderLibraryList()}async function rehAutoSaveImport(title){var _a2,_b2;const text=(((_a2=$("reh-script-text"))==null?void 0:_a2.value)||"").trim();if(!text)return null;title&&$("reh-script-title")&&!$("reh-script-title").value.trim()&&($("reh-script-title").value=title);const rec={title:((_b2=$("reh-script-title"))==null?void 0:_b2.value.trim())||title||"Untitled",script:text,cast:{},emotions:{},notes:{},ignored:{},hidden:{},backend:rehState.backend||"",narratorVoice:rehState.narratorVoice||"",lineIndex:0,clips:[],created:new Date,updated:new Date};try{const id=await rehDbAdd(rec);return rehState.savedId=id,rehState._keepSavedIdOnce=!0,await renderLibraryList(),id}catch{return null}}async function exportToFile(){const rec=rehCurrentRecord();rec.version=1,rec.created=rec.created||new Date,rec.clips=await clipsToJson(rec.clips);const json=JSON.stringify(rec,null,2),blob=new Blob([json],{type:"application/json"}),a=document.createElement("a"),title=(rec.title||"rehearsal").replace(/[^a-z0-9_\- ]/gi,"_").slice(0,40);a.href=URL.createObjectURL(blob),a.download=title+".reh",a.click()}function exportFountain(){var _a2,_b2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||((_b2=$("reh-page-title"))==null?void 0:_b2.textContent)||"Untitled";let out=`Title: ${title} - -`;rehState.lines.forEach(line=>{const t=stripMarkdown(line.text);switch(line.type){case"act":out+=` -`+t+` -`;break;case"scene":out+=` -`+t+` -`;break;case"action":out+=` -`+t+` -`;break;case"transition":out+=` -`+t+` -`;break;case"dialog":out+=` -`+line.speaker+` -`+t+` -`;break;case"direction":out+=(line.speaker?"":` -`)+t+` -`;break}});const blob=new Blob([out.trim()],{type:"text/plain"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(title.replace(/[^a-z0-9_\- ]/gi,"_")||"script")+".fountain",a.click(),toast("Exported as .fountain","success")}function exportFDX(){var _a2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled",x=s=>(s||"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""");let xml=` -`;xml+=` - -`,rehState.lines.forEach(line=>{const t=x(stripMarkdown(line.text));switch(line.type){case"act":case"scene":xml+=` ${t} -`;break;case"action":xml+=` ${t} -`;break;case"transition":xml+=` ${t} -`;break;case"direction":xml+=` ${t} -`;break;case"dialog":xml+=` ${x(line.speaker)} -`,xml+=` ${t} -`;break}}),xml+=` -`;const blob=new Blob([xml],{type:"text/xml"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(title.replace(/[^a-z0-9_\- ]/gi,"_")||"script")+".fdx",a.click(),toast("Exported as .fdx (Final Draft XML)","success")}async function importFDX(file){const text=await file.text(),paras=new DOMParser().parseFromString(text,"text/xml").querySelectorAll("Paragraph");let result="";return paras.forEach(para=>{const type=para.getAttribute("Type")||"",t=[...para.querySelectorAll("Text")].map(el=>el.textContent).join("");if(t.trim())switch(type){case"Scene Heading":result+=` -`+t+` -`;break;case"Action":result+=` -`+t+` -`;break;case"Character":result+=` -`+t+` -`;break;case"Dialogue":result+=t+` -`;break;case"Parenthetical":result+=t+` -`;break;case"Transition":result+=` -`+t+` -`;break}}),result.trim()}function exportOSF(){var _a2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled",x=s=>(s||"").replace(/&/g,"&").replace(//g,">").replace(/"/g,""");let xml=` - -`;xml+=` ${x(title)} - -`,rehState.lines.forEach(line=>{const t=x(stripMarkdown(line.text));switch(line.type){case"act":case"scene":xml+=` ${t} -`;break;case"action":xml+=` ${t} -`;break;case"transition":xml+=` ${t} -`;break;case"direction":xml+=` ${t} -`;break;case"dialog":xml+=` - ${x(line.speaker)} - ${t} - -`;break}}),xml+=` -`;const blob=new Blob([xml],{type:"text/xml"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(title.replace(/[^a-z0-9_\- ]/gi,"_")||"script")+".osf",a.click(),toast("Exported as .osf (Open Screenplay Format)","success")}async function loadPdfJs(){window.pdfjsLib||(await new Promise((resolve,reject)=>{const s=document.createElement("script");s.src="/static/js/pdf/pdf.min.js",s.onload=resolve,s.onerror=reject,document.head.appendChild(s)}),pdfjsLib.GlobalWorkerOptions.workerSrc="/static/js/pdf/pdf.worker.min.js")}async function importPDFScript(file){toast("Loading PDF reader\u2026","success"),await loadPdfJs();const ab=await file.arrayBuffer(),pdf=await pdfjsLib.getDocument({data:ab}).promise,allLinesByPage=[],allLines=[];for(let p=1;p<=pdf.numPages;p++){const page=await pdf.getPage(p),content=await page.getTextContent(),pageW2=page.getViewport({scale:1}).width,rows=new Map;content.items.forEach(item=>{if(!item.str)return;const y=Math.round(item.transform[5]/3)*3;rows.has(y)||rows.set(y,[]),rows.get(y).push({x:item.transform[4],w:item.width||0,str:item.str})});const pageLines=[];[...rows.entries()].sort((a,b)=>b[0]-a[0]).forEach(([,items])=>{items.sort((a,b)=>a.x-b.x);const text=items.map(i=>i.str).join("").replace(/\s+/g," ").trim();if(!text)return;const x=items[0].x,last=items[items.length-1],right=last.x+(last.w||0);pageLines.push({x,right,pageW:pageW2,text}),allLines.push({x,right,pageW:pageW2,text})}),allLinesByPage.push(pageLines)}if(!allLines.length)return"";const leftMargin=Math.min(...allLines.map(l=>l.x)),pageW=allLines[0].pageW||612,isNoise=t=>{const s=t.trim();return!!(/^\d{1,4}\.?$/.test(s)||/^\(?CONTINUED\)?:?$/i.test(s)||/^\d+\.$/.test(s)||/^(rev\.|revised|draft)\b/i.test(s)&&s.length<24)},out=[];let prevBlank=!0,prevWasAction=!1;const push=(line,blankBefore)=>{blankBefore&&!prevBlank&&out.push(""),out.push(line),prevBlank=!1},blank=()=>{prevBlank||(out.push(""),prevBlank=!0)},nonAction=()=>{prevWasAction=!1};for(let pageIdx=0;pageIdx0&&(blank(),out.push(`\f${pageIdx+1}`),blank());const pageLns=allLinesByPage[pageIdx]||[];for(const ln of pageLns){const t=ln.text.trim();if(!t||isNoise(t))continue;const indent=(ln.x-leftMargin)/pageW,rightFrac=ln.right/pageW,isCaps=t===t.toUpperCase()&&/[A-Z]/.test(t),wordCount=t.split(/\s+/).length;if(/^(INT|EXT|INT\.\/EXT|EXT\.\/INT|I\/E)[\. ]/i.test(t)){blank(),push(t.toUpperCase().replace(/\s+\d+[A-Z]?\.?$/,"")),blank(),nonAction();continue}if(/^(ACT|SCENE)\s+(\d+|[IVXLC]+|ONE|TWO|THREE|FOUR|FIVE)\b/i.test(t)){blank(),push(t.toUpperCase()),blank(),nonAction();continue}if(isCaps&&(rightFrac>.72||indent>.45)&&/(TO:|CUT|FADE|DISSOLVE|SMASH|MATCH|WIPE|BLACKOUT|INTERMISSION)\b/.test(t)){blank(),push(t),blank(),nonAction();continue}if(/^\(.*\)?$/.test(t)||indent>.18&&indent<.3&&/^\(/.test(t)){push(t.startsWith("(")?t:"("+t+")",!1),nonAction();continue}const nameCore=t.replace(/\s*\([^)]*\)\s*$/,"").trim();if(indent>.22&&isCaps&&wordCount<=6&&nameCore.length<=40&&/^[A-Z0-9 .'#\-]+$/.test(nameCore)){blank(),push(t.toUpperCase()),prevBlank=!1,nonAction();continue}if(indent>.1&&indent<.34){push(t,!1),nonAction();continue}prevWasAction?out.push(t):(blank(),push(t)),prevWasAction=!0}}return out.join(` -`).replace(/\n{3,}/g,` - -`).trim()}async function importFromFile(file){try{const text=await file.text(),data=JSON.parse(text);data.clips=clipsFromJson(data.clips||[]),data.created=data.created?new Date(data.created):new Date,data.updated=new Date,delete data.id;const newId=await rehDbAdd(data);toast("Imported: "+(data.title||"Untitled"),"success"),await renderLibraryList(),loadRecord({...data,id:newId})}catch(e){toast("Import failed: "+e.message,"error")}}function loadRecord(rec){typeof navTo=="function"&&navTo("s-rehearser"),$("reh-script-text")&&($("reh-script-text").value=rec.script||""),$("reh-script-title")&&($("reh-script-title").value=rec.title||""),rehState.savedId=rec.id||null,rehState._keepSavedIdOnce=!0,rehState.backend=rec.backend||"",rehState.lineIndex=rec.lineIndex||0,rehState.clips=(rec.clips||[]).map(c=>({...c})),rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.narratorVoice=rec.narratorVoice||"";const lines=parseScript(rec.script||"");rec.emotions&&lines.forEach((l,i)=>{l.type==="dialog"&&rec.emotions[i]&&(l.emotion=rec.emotions[i])}),rec.notes&&lines.forEach((l,i)=>{l.type==="dialog"&&rec.notes[i]&&(l.note=rec.notes[i])}),rec.ignored&&lines.forEach((l,i)=>{rec.ignored[i]&&(l.ignored=!0)}),rec.hidden&&lines.forEach((l,i)=>{rec.hidden[i]&&(l.hidden=!0)}),rehState.lines=lines,rehState.cast={};const detected=detectCharacters(lines);Object.entries(detected).forEach(([sp,def])=>{var _a2,_b2,_c2;const saved=(_a2=rec.cast)==null?void 0:_a2[sp];rehState.cast[sp]={voice:(_b2=saved==null?void 0:saved.voice)!=null?_b2:def.voice,color:(_c2=saved==null?void 0:saved.color)!=null?_c2:def.color,instruct:(saved==null?void 0:saved.instruct)||"",lang:(saved==null?void 0:saved.lang)||"",gender:(saved==null?void 0:saved.gender)||"",tags:(saved==null?void 0:saved.tags)||"",soul:(saved==null?void 0:saved.soul)||"",ignored:!!(saved!=null&&saved.ignored),hidden:!!(saved!=null&&saved.hidden),voiceData:saved!=null&&saved.voice&&saved.voice!=="me"?getVoiceData(saved.voice):null}}),renderCastList(),rehApplySharedCast(rec.title),refreshRehBackends().then(()=>{rec.backend&&$("reh-backend-select")&&($("reh-backend-select").value=rec.backend),rec.narratorVoice&&$("reh-narrator-voice")&&($("reh-narrator-voice").value=rec.narratorVoice)}),showPhase(2)}async function rehApplySharedCast(title){if(typeof castForProduction!="function"||!title)return;let roster;try{roster=await castForProduction(title)}catch{return}if(!roster)return;let changed=!1;Object.keys(rehState.cast||{}).forEach(sp=>{const lookupName=sp===REH_NARRATOR_KEY?"narrator":String(sp).toLowerCase(),shared=roster[lookupName];if(!shared)return;const slot=rehState.cast[sp];shared.voice&&!slot.voice&&(slot.voice=shared.voice,slot.voiceData=shared.voice!=="me"&&typeof getVoiceData=="function"?getVoiceData(shared.voice):null,changed=!0,sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=shared.voice)),shared.gender&&!slot.gender&&(slot.gender=shared.gender,changed=!0),shared.soul&&!slot.soul&&(slot.soul=shared.soul,changed=!0)}),changed&&renderCastList()}function bookCover(title){let h=0;for(let i=0;i{const at=new Date(a.updated||0).getTime(),bt=new Date(b.updated||0).getTime();return at!==bt?bt-at:(b.id||0)-(a.id||0)});for(const rec of ordered){const bucketKey=rehTitleKey(rec.title)||`__reh__${rec.id}`;byTitle.has(bucketKey)||byTitle.set(bucketKey,rec)}return[...byTitle.values()].sort((a,b)=>{const at=new Date(a.updated||0).getTime(),bt=new Date(b.updated||0).getTime();return at!==bt?bt-at:(b.id||0)-(a.id||0)})}async function renderLibraryList(){const list=$("reh-library-list");if(!list)return;let all;try{all=await rehDbGetAll()}catch{all=[]}all=rehUniqueLibraryRecords(all),list.classList.toggle("list-view",rehLibView==="list");const vt=$("reh-lib-view-toggle");if(vt&&(vt.innerHTML=``),!all.length){list.innerHTML='

No saved rehearsals yet.
Parse a script below or drop a file to start.

';return}list.innerHTML=all.map(rec=>{const speakers=Object.keys(rec.cast||{}),meCount=speakers.filter(sp=>{var _a2;return((_a2=rec.cast[sp])==null?void 0:_a2.voice)==="me"}).length,clipCount=(rec.clips||[]).filter(c=>c.type==="me"&&c.blob).length,total=parseScript(rec.script||"").filter(l=>l.type==="dialog").length,pct=total?Math.round((rec.lineIndex||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"\u2014",isCurrent=rec.id===rehState.savedId,title=rec.title||"Untitled",{c1,c2}=bookCover(title),avatars=speakers.slice(0,5).map(sp=>`${sp[0].toUpperCase()}`).join("");return`
- ${isCurrent?'active':""} -
- - -
-
${escHtml(title)}
-
-
${avatars}
- ${total} lines \xB7 ${speakers.length} cast${meCount?" \xB7 "+meCount+" me":""}${clipCount?" \xB7 "+clipCount+"\u{1F3A4}":""} -
-
${pct}% \xB7 ${date}
-
-
`}).join(""),list.querySelectorAll(".reh-book").forEach(book=>book.addEventListener("click",async e=>{if(e.target.closest(".reh-book-act"))return;const id=parseInt(book.dataset.id),rec=await rehDbGetById(id);rec?loadRecord(rec):toast("Rehearsal not found","error")})),list.querySelectorAll(".reh-lib-export").forEach(btn=>btn.addEventListener("click",async e=>{e.stopPropagation();const id=parseInt(btn.dataset.id),rec=await rehDbGetById(id);if(!rec){toast("Rehearsal not found","error");return}rec.version=1,rec.clips=await clipsToJson(rec.clips||[]);const json=JSON.stringify(rec,null,2),a=document.createElement("a"),t=(rec.title||"rehearsal").replace(/[^a-z0-9_\- ]/gi,"_").slice(0,40);a.href=URL.createObjectURL(new Blob([json],{type:"application/json"})),a.download=t+".reh",a.click()})),list.querySelectorAll(".reh-lib-delete").forEach(btn=>btn.addEventListener("click",async e=>{e.stopPropagation();const bookEl=btn.closest(".reh-book");if(bookEl.querySelector(".reh-book-del-confirm"))return;const confirmOverlay=document.createElement("div");confirmOverlay.className="reh-book-del-confirm",confirmOverlay.style.cssText="position:absolute; inset:0; background:rgba(0,0,0,0.85); color:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; border-radius:inherit; z-index:10; padding:12px; text-align:center; box-sizing:border-box;",confirmOverlay.innerHTML=` -
Delete Rehearsal?
-
This cannot be undone.
-
- - -
- `,confirmOverlay.addEventListener("click",ce=>ce.stopPropagation()),confirmOverlay.querySelector("#btn-cancel-del").addEventListener("click",ce=>{ce.stopPropagation(),confirmOverlay.remove()}),confirmOverlay.querySelector("#btn-confirm-del").addEventListener("click",async ce=>{ce.stopPropagation(),await rehDbDelete(parseInt(btn.dataset.id)),rehState.savedId===parseInt(btn.dataset.id)&&(rehState.savedId=null),renderLibraryList()}),bookEl.appendChild(confirmOverlay)}))}(_pa=$("reh-lib-view-toggle"))==null||_pa.addEventListener("click",()=>{rehLibView=rehLibView==="shelf"?"list":"shelf",localStorage.setItem("reh-lib-view",rehLibView),renderLibraryList()});function getVoiceData(voiceId){return(window._voices||[]).find(v=>v.id===voiceId)||null}function _rehVoiceVisibleId(voiceId,include=""){if(!voiceId)return!1;if(include&&voiceId===include)return!0;const v=getVoiceData(voiceId);return!v||v.enabled!==!1}function voiceAvatarHtml(voiceId,color,size=32){const v=getVoiceData(voiceId),s=size+"px",radius=Math.round(size/2);if(v!=null&&v.has_picture)return`${escHtml(voiceId)}`;const ic=v!=null&&v.avatar&&window.VOICE_AVATAR_ICONS?window.VOICE_AVATAR_ICONS[v.avatar]:null;if(ic)return``;const initial=(voiceId||"?")[0].toUpperCase();return`${initial}`}function _rehCharAvatarHtml(speaker,voiceId,color,size=32){const rec=(_rehLibCharsCache||[]).find(r=>String(r.name||"").trim().toLowerCase()===String(speaker||"").trim().toLowerCase());if(rec&&rec.image){const s=size+"px",radius=Math.round(size/2);return`${escHtml(speaker)}`}return voiceAvatarHtml(voiceId,color,size)}function showPhase(n){const impex=$("reh-impex-panel");impex&&(impex.hidden=!0);for(let i=1;i<=4;i++){const el=$("reh-phase-"+i);el&&(el.hidden=i!==n)}syncPhaseTabs(n),typeof window.onRehearserPhaseChange=="function"&&window.onRehearserPhaseChange(n)}window.showRehImpEx=function(){var _a2;stopPlay(),stopRehMic();for(let i=1;i<=4;i++){const el=$("reh-phase-"+i);el&&(el.hidden=!0)}const panel=$("reh-impex-panel");panel&&(panel.hidden=!1);const note=$("reh-impex-export-note");note&&(note.textContent=rehState.lines.length?`Current session: "${((_a2=$("reh-script-title"))==null?void 0:_a2.value)||"Untitled"}" \xB7 ${rehState.lines.filter(l=>l.type==="dialog").length} lines`:"No active session \u2014 load a script first to export.")};function syncPhaseTabs(n){const hasScript=rehState.lines.length>0,hasClips=rehState.clips.length>0;document.querySelectorAll(".reh-subtab").forEach(tab=>{const p=parseInt(tab.dataset.phase);tab.classList.toggle("active",p===n);let enabled=p===1||p===2&&hasScript||p===3&&hasScript||p===4&&(hasClips||n===4);tab.disabled=!enabled})}document.querySelectorAll(".reh-subtab").forEach(tab=>{tab.addEventListener("click",()=>{if(tab.disabled)return;const p=parseInt(tab.dataset.phase);if(p!==3&&(stopPlay(),stopRehMic(),hideRecOverlay()),p===1&&renderLibraryList(),p===3&&rehState.lines.length){buildScriptPage(),showPhase(3),highlightCurrentLine();return}p===4&&renderSummary(),showPhase(p)})}),(_qa=$("reh-file-input"))==null||_qa.addEventListener("change",function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];if(!f)return;const guess=f.name.replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").trim(),r=new FileReader;r.onload=async e=>{$("reh-script-text").value=e.target.result,await rehAutoSaveImport(guess),toast("Script loaded & saved to your library \u2014 click Parse & cast","success")},r.readAsText(f),this.value=""}),(_ra=$("reh-pdf-input"))==null||_ra.addEventListener("change",async function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];if(f){this.value="";try{const text=await importPDFScript(f);$("reh-script-text")&&($("reh-script-text").value=text);const guess=f.name.replace(/\.pdf$/i,"").replace(/[-_]+/g," ").trim();await rehAutoSaveImport(guess),toast("PDF imported & saved to your library \u2014 check formatting, then click Parse & cast","success")}catch(e){toast("PDF import failed: "+e.message,"error")}}}),(_sa=$("reh-fdx-input"))==null||_sa.addEventListener("change",async function(){var _a2,_b2;const f=(_a2=this.files)==null?void 0:_a2[0];if(f){this.value="";try{const text=await importFDX(f);if($("reh-script-text")&&($("reh-script-text").value=text),!((_b2=$("reh-script-title"))!=null&&_b2.value.trim())){const guess=f.name.replace(/\.fdx$/i,"").replace(/[-_]+/g," ").trim();$("reh-script-title")&&($("reh-script-title").value=guess)}toast("FDX imported \u2014 click Parse & cast","success")}catch(e){toast("FDX import failed: "+e.message,"error")}}});async function rehImportAnyFile(f){if(!f)return;const name=f.name.toLowerCase(),setTitle=strip=>{var _a2;if(!((_a2=$("reh-script-title"))!=null&&_a2.value.trim())){const guess=f.name.replace(strip,"").replace(/[-_]+/g," ").trim();$("reh-script-title")&&($("reh-script-title").value=guess)}};try{if(name.endsWith(".reh")||name.endsWith(".json")){await importFromFile(f);return}if(name.endsWith(".pdf")){toast("Reading PDF\u2026","success");const text2=await importPDFScript(f);$("reh-script-text")&&($("reh-script-text").value=text2),setTitle(/\.pdf$/i),await rehAutoSaveImport(),toast("PDF imported & saved to your library \u2014 check formatting, then Parse & cast","success");return}if(name.endsWith(".fdx")||name.endsWith(".osf")||name.endsWith(".xml")){const text2=await importFDX(f);$("reh-script-text")&&($("reh-script-text").value=text2),setTitle(/\.(fdx|osf|xml)$/i),await rehAutoSaveImport(),toast("Script imported & saved to your library \u2014 click Parse & cast","success");return}const text=await f.text();$("reh-script-text")&&($("reh-script-text").value=text),setTitle(/\.(txt|md|fountain)$/i),await rehAutoSaveImport(),toast("Script loaded & saved to your library \u2014 click Parse & cast","success")}catch(e){toast("Import failed: "+e.message,"error")}}(function(){const dz=$("reh-dropzone"),phase=$("reh-phase-1");if(!dz||!phase)return;let depth=0;const isFileDrag=e=>e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files");phase.addEventListener("dragenter",e=>{isFileDrag(e)&&(e.preventDefault(),depth++,dz.classList.add("dragover"),dz.scrollIntoView({behavior:"smooth",block:"nearest"}))}),phase.addEventListener("dragover",e=>{isFileDrag(e)&&e.preventDefault()}),phase.addEventListener("dragleave",e=>{isFileDrag(e)&&(depth=Math.max(0,depth-1),depth===0&&dz.classList.remove("dragover"))}),phase.addEventListener("drop",async e=>{var _a2;if(!isFileDrag(e))return;e.preventDefault(),depth=0,dz.classList.remove("dragover");const f=(_a2=e.dataTransfer.files)==null?void 0:_a2[0];await rehImportAnyFile(f)})})(),(_ta=$("reh-parse-btn"))==null||_ta.addEventListener("click",()=>{var _a2;const text=(_a2=$("reh-script-text"))==null?void 0:_a2.value.trim();if(!text){toast("Paste or upload a script first","error");return}if(rehState.lines=parseScript(text),!rehState.lines.filter(l=>l.type==="dialog").length){toast("No dialog found. Use screenplay format (CAPS name + dialog) or CHAR: text.","error");return}const detected=detectCharacters(rehState.lines);Object.entries(detected).forEach(([sp])=>{rehState.cast[sp]&&(detected[sp]={...detected[sp],...rehState.cast[sp]})}),rehState.cast=detected,rehState._keepSavedIdOnce?rehState._keepSavedIdOnce=!1:rehState.savedId=null,rehState.clips=[],rehState.lineIndex=0,rehState.synthCache.clear(),rehDecodedBuffers.clear(),renderCastList(),showPhase(2),refreshRehBackends()});function castAvatarHtml(sp){const c=rehState.cast[sp];if(!c)return"";if(c.voice&&c.voice!=="me"){const vd=getVoiceData(c.voice);if(vd!=null&&vd.has_picture)return``;const ic=vd!=null&&vd.avatar&&window.VOICE_AVATAR_ICONS?window.VOICE_AVATAR_ICONS[vd.avatar]:null;if(ic)return``}const initial=sp[0].toUpperCase();return`${initial}`}let _rehAudEl=null,_rehAudBtn=null,_rehAudObjectUrl=null;function _rehStopAudition(){if(_rehAudEl)try{_rehAudEl.pause()}catch{}if(_rehAudObjectUrl&&(URL.revokeObjectURL(_rehAudObjectUrl),_rehAudObjectUrl=null),_rehAudBtn){_rehAudBtn.classList.remove("playing");const i=_rehAudBtn.querySelector(".mdi");i&&(i.className="mdi mdi-play")}_rehAudBtn=null}function _rehAudition(url,btn){if(!url){toast("No preview audio for this voice","error");return}if(_rehAudBtn===btn&&_rehAudEl&&!_rehAudEl.paused){_rehStopAudition();return}_rehStopAudition(),_rehAudEl||(_rehAudEl=new Audio,_rehAudEl.addEventListener("ended",_rehStopAudition)),_rehAudEl.src=url,_rehAudEl.play().then(()=>{_rehAudBtn=btn,btn.classList.add("playing");const i=btn.querySelector(".mdi");i&&(i.className="mdi mdi-stop")}).catch(()=>toast("Could not play preview","error"))}const _REH_PREVIEW_TEXT="Hello \u2014 this is how this voice sounds.",_rehSearchCache={};function _rehVoiceRow({name,meta,playUrl,playVoice,isCurrent,useAttrs}){const play=``,right=isCurrent?' Selected':``;return`
${play} -
${escHtml(name)}${escHtml(meta)}
- ${right}
`}function castOnlinePanelHtml(sp,c){const o=c.online;if(!o||!Array.isArray(o.candidates)||c.voice==="me")return"";const curCand=o.candidates.find(x=>x.voice_id&&x.voice_id===c.voice);let curRow;if(curCand)curRow=_rehVoiceRow({name:curCand.title||c.voice,meta:[curCand.gender,curCand.language].filter(Boolean).join(" \xB7 ")||"fish.audio",playUrl:curCand.sample_audio,isCurrent:!0});else if(c.voice){const vd=getVoiceData(c.voice);curRow=_rehVoiceRow({name:(vd==null?void 0:vd.name)||c.voice,meta:(vd==null?void 0:vd.group)||(vd==null?void 0:vd.tag)||"library voice",playVoice:c.voice,isCurrent:!0})}else curRow='
No voice assignedpick one below
';const alts=o.candidates.map((cand,i)=>({cand,i})).filter(({cand})=>cand.voice_id!==c.voice||!cand.voice_id),altRows=alts.map(({cand,i})=>_rehVoiceRow({name:cand.title||"Voice",meta:[cand.gender,cand.language].filter(Boolean).join(" \xB7 ")||"fish.audio",playUrl:cand.sample_audio,useAttrs:` data-act="alt" data-idx="${i}"`})).join("");return`
-
Voice \u2014 try & change - ${alts.length?``:""}
-
${curRow}
- ${alts.length?``:""} -
- - -
- - -
`}async function _rehPreviewLocal(voiceId,btn){var _a2;const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a TTS backend first","error");return}if(_rehAudBtn===btn&&_rehAudEl&&!_rehAudEl.paused){_rehStopAudition();return}_rehStopAudition();const icon=btn.querySelector(".mdi");btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{const blob=await fetchTtsPreviewBlob(voiceId,_REH_PREVIEW_TEXT,"wav","",backend);_rehAudEl||(_rehAudEl=new Audio,_rehAudEl.addEventListener("ended",_rehStopAudition)),_rehAudObjectUrl=URL.createObjectURL(blob),_rehAudEl.src=_rehAudObjectUrl,await _rehAudEl.play(),btn.classList.remove("loading"),_rehAudBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _rehPickOneLiner(sp){const isNarr=sp===REH_NARRATOR_KEY,pool=rehState.lines.filter(l=>isNarr?l.type!=="dialog"&&l.type!=="pagebreak"&&!l.ignored:l.type==="dialog"&&l.speaker===sp).map(l=>({text:stripMarkdown(l.text||"").trim(),emotion:l.emotion||""})).filter(l=>l.text.length>=4);if(!pool.length)return null;const best=pool.map(l=>{const len=l.text.length;let score=len>=20&&len<=110?3:len<20?1:0;return/[.!?]["']?$/.test(l.text)&&(score+=1),score-=Math.abs(len-60)/120,{l,score}}).sort((a,b)=>b.score-a.score)[0].l;return{text:best.text.slice(0,220),emotion:best.emotion}}async function _rehPreviewCastLine(sp,btn){var _a2;const c=rehState.cast[sp];if(!c||!c.voice||c.voice==="me"){toast("Assign a voice first","error");return}const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a TTS backend first","error");return}if(_rehAudBtn===btn&&_rehAudEl&&!_rehAudEl.paused){_rehStopAudition();return}_rehStopAudition();const pick=_rehPickOneLiner(sp);if(!pick){toast("No lines to preview yet","error");return}const icon=btn.querySelector(".mdi");btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{const blob=await fetchTtsPreviewBlob(c.voice,_rehInlineTone(pick.text,pick.emotion),"wav",_buildInstruct(c.instruct,pick.emotion,c.voice),backend);_rehAudEl||(_rehAudEl=new Audio,_rehAudEl.addEventListener("ended",_rehStopAudition)),_rehAudEl.src=URL.createObjectURL(blob),await _rehAudEl.play(),btn.classList.remove("loading"),_rehAudBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _rehAssignLocal(sp,voiceId){const c=rehState.cast[sp];c&&(_rehStopAudition(),c.voice=voiceId,c.voiceData=getVoiceData(voiceId),c.online&&(c.online.picked=c.online.candidates.findIndex(x=>x.voice_id===voiceId)),renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect())}function _rehRenderLocalResults(card,sp,q){var _a2;const box=card.querySelector(".reh-vo-local-results");if(!box)return;const cur=(_a2=rehState.cast[sp])==null?void 0:_a2.voice,ql=(q||"").trim().toLowerCase(),matches=rehState.voices.filter(v=>{if(!_rehVoiceVisibleId(v,cur)||v===cur)return!1;if(!ql)return!0;const vd=getVoiceData(v);return v.toLowerCase().includes(ql)||((vd==null?void 0:vd.name)||"").toLowerCase().includes(ql)||((vd==null?void 0:vd.group)||"").toLowerCase().includes(ql)}).slice(0,40);if(!matches.length){box.innerHTML='
No library voices match.
';return}box.innerHTML=matches.map(v=>{const vd=getVoiceData(v);return _rehVoiceRow({name:(vd==null?void 0:vd.name)||v,meta:(vd==null?void 0:vd.group)||(vd==null?void 0:vd.tag)||"library voice",playVoice:v,useAttrs:` data-act="local" data-voice="${escHtml(v)}"`})}).join("")}async function _rehSearchOnline(card,sp,q){var _a2,_b2;const box=card.querySelector(".reh-vo-search-results");if(!box)return;if(q=(q||"").trim(),!q){box.innerHTML='
Enter a search term first.
';return}box.innerHTML='
Searching fish.audio\u2026
';const lang=(_b2=REH_FISH_LANG[((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||"English"])!=null?_b2:"en";let items=[];try{const qs=[`search=${encodeURIComponent(q)}`,`language=${lang}`,"page_size=10","sort_by=score"];items=(await fetch("/api/fishaudio/voices?"+qs.join("&")).then(r=>r.json())).items||[]}catch{}const cands=items.filter(v=>v.sample_audio).slice(0,10).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(_rehSearchCache[sp]=cands,!cands.length){box.innerHTML=`
No playable voices found for \u201C${escHtml(q)}\u201D.
`;return}box.innerHTML=cands.map((cand,i)=>_rehVoiceRow({name:cand.title||"Voice",meta:[cand.gender,cand.language].filter(Boolean).join(" \xB7 ")||"fish.audio",playUrl:cand.sample_audio,useAttrs:` data-act="search" data-idx="${i}"`})).join("")}async function _rehUseSearchResult(sp,idx,btn){const c=rehState.cast[sp];if(!c)return;const cand=(_rehSearchCache[sp]||[])[idx];if(!cand)return;_rehStopAudition();const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='';try{const vid=await _rehImportFishCandidate(sp,cand);cand.voice_id=vid,c.voice=vid,c.voiceData=getVoiceData(vid),c.online||(c.online={candidates:[],picked:0}),c.online.candidates.push(cand),c.online.picked=c.online.candidates.length-1,typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect(),toast(`Switched ${sp} to ${cand.title||"voice"}`,"success")}catch(err){btn.disabled=!1,btn.innerHTML=orig,toast("Import failed: "+err.message,"error")}}const _rehFishImported={};async function _rehImportFishCandidate(sp,cand){const key=(cand.sample_audio||cand.title||"").toLowerCase();if(key&&_rehFishImported[key]){const id=_rehFishImported[key];return rehState.voices.includes(id)||rehState.voices.push(id),id}const title=(cand.title||"").toLowerCase().trim(),existing=title&&(window._voices||[]).find(v=>(v.group==="fish-audio"||v.tag==="fish-audio")&&(v.name||"").toLowerCase().trim()===title);if(existing)return key&&(_rehFishImported[key]=existing.id),rehState.voices.includes(existing.id)||rehState.voices.push(existing.id),existing.id;const vid=`${(cand.language||"EN").slice(0,2).toUpperCase()}_${(typeof _umlautSafe=="function"?_umlautSafe(cand.title||sp):cand.title||sp).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,36)||"Voice"}`,d=await fetch("/api/quick-import-voice",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:vid,audio_url:cand.sample_audio,transcript:cand.sample_text||""})}).then(r=>r.json());if(!d.voice_id)throw new Error("import returned no id");return typeof saveMeta=="function"&&await saveMeta(d.voice_id,{name:cand.title||sp,tag:"fish-audio",group:"fish-audio",origin:"cloned",gender:(cand.gender||"").charAt(0).toUpperCase(),note:(cand.description||"").slice(0,180)}).catch(e=>logErr("fish saveMeta",e)),cand.image&&await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:d.voice_id,image_url:cand.image})}).catch(e=>logErr("fish picture",e)),rehState.voices.includes(d.voice_id)||rehState.voices.push(d.voice_id),key&&(_rehFishImported[key]=d.voice_id),d.voice_id}async function _rehUseCandidate(sp,idx,btn){const c=rehState.cast[sp],o=c&&c.online;if(!o)return;const cand=o.candidates[idx];if(!cand)return;if(_rehStopAudition(),cand.voice_id){c.voice=cand.voice_id,c.voiceData=getVoiceData(cand.voice_id),o.picked=idx,renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect();return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='';try{const vid=await _rehImportFishCandidate(sp,cand);cand.voice_id=vid,c.voice=vid,c.voiceData=getVoiceData(vid),o.picked=idx,typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect(),toast(`Switched ${sp} to ${cand.title||"voice"}`,"success")}catch(err){btn.disabled=!1,btn.innerHTML=orig,toast("Import failed: "+err.message,"error")}}const REH_NARRATOR_KEY="\u{1F4D6}NARRATOR";function _ensureNarrator(){rehState.cast[REH_NARRATOR_KEY]||(rehState.cast[REH_NARRATOR_KEY]={voice:rehState.narratorVoice||"",color:"#6b7280",instruct:"",voiceData:rehState.narratorVoice?getVoiceData(rehState.narratorVoice):null,_isNarrator:!0});const n=rehState.cast[REH_NARRATOR_KEY];n.voice?(rehState.narratorVoice=n.voice,n.voiceData||(n.voiceData=getVoiceData(n.voice))):rehState.narratorVoice&&(n.voice=rehState.narratorVoice,n.voiceData=getVoiceData(rehState.narratorVoice))}const REH_CAST_LANGS=["English","German","Auto","French","Spanish","Italian","Portuguese","Dutch","Polish"],REH_CAST_GENDERS=[["","\u2014"],["F","\u2640 Female"],["M","\u2642 Male"],["N","\u26A5 Diverse"]];function _castApplyToLines(sp,fn){rehState.lines.forEach((l,i)=>{l.speaker===sp&&l.type==="dialog"&&fn(l,i)})}function _safeDomId(value){let out="";const s=String(value||"voice");for(let i=0;i{_rehLibCharsCache=recs||[],renderCastList(),typeof renderCastStrip=="function"&&renderCastStrip(),typeof buildScriptPage=="function"&&buildScriptPage()}).catch(()=>{}))}function renderCastList(){var _a2;const list=$("reh-cast-list");if(!list)return;!(window._voices||[]).length&&typeof loadVoiceLibrary=="function"&&!rehState._castLibFetch&&(rehState._castLibFetch=!0,loadVoiceLibrary().then(()=>renderCastList()).catch(()=>{})),_ensureNarrator();const narr=REH_NARRATOR_KEY,lineCount=sp=>rehState.lines.filter(l=>l.speaker===sp&&l.type==="dialog").length,allOthers=Object.keys(rehState.cast).filter(s=>s!==narr),others=_castSortFilter(allOthers,lineCount),speakers=[narr,...others],scriptTitle=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"";_rehEnsureLibCharsCache(scriptTitle);const libRecByName=new Map((_rehLibCharsCache||[]).map(r=>[String(r.name||"").trim().toLowerCase(),r])),emotionsFor=sp=>{const seen=new Map;return rehState.lines.forEach(l=>{l.speaker===sp&&l.type==="dialog"&&l.emotion&&!seen.has(l.emotion)&&seen.set(l.emotion,getEmotionInfo(l.emotion))}),[...seen.values()]};list.innerHTML=speakers.map(sp=>{var _a3,_b2;const c=rehState.cast[sp],isNarr=sp===narr,isMe=c.voice==="me",label=isNarr?"Narrator":sp,n=lineCount(sp),sub=isNarr?"scene headings & descriptions":`${n} line${n!==1?"s":""}${scriptTitle?` \xB7 ${escHtml(scriptTitle)}`:""}`,cardCls="reh-cast-card"+(isNarr?" reh-cast-narrator":"")+(c.ignored?" reh-cast-ignored":"")+(c.hidden?" reh-cast-hidden-c":""),langSel=REH_CAST_LANGS.map(l=>`${l}`).join(""),genSel=REH_CAST_GENDERS.map(([v,t])=>``).join(""),voiceSel=``,voiceName=!isMe&&c.voice?((_a3=getVoiceData(c.voice))==null?void 0:_a3.name)||c.voice:isMe?"Your mic":"",libRec=isNarr?null:libRecByName.get(String(sp).trim().toLowerCase());if(libRec){const libVoice=typeof libRec.voice=="object"?((_b2=libRec.voice)==null?void 0:_b2.id)||"":libRec.voice||"";libVoice&&(c.voice=libVoice)}const emotions=isNarr?[]:emotionsFor(sp),emotionsHtml=emotions.length?`
${emotions.map(info=>`${info.emoji} ${escHtml(info.label)}`).join("")}
`:"",controlsHtml=`
- - ${!isMe&&c.voice?``:""} - - - - -
- ${emotionsHtml}`;return libRec?`
- ${typeof _charCardHtml=="function"?_charCardHtml(libRec,_rehLibCharsCache||[]):""} -
${controlsHtml}
-
`:`
-
-
${castAvatarHtml(sp)}
-
-
${isNarr?' ':""}${escHtml(label)}
-
${sub}
-
${escHtml(voiceName||"No voice assigned")}
-
-
- -
`}).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("")}(_ua=$("reh-fetch-voices-btn"))==null||_ua.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}}),(_va=$("reh-narrator-voice"))==null||_va.addEventListener("change",function(){rehState.narratorVoice=this.value}),(_wa=$("reh-back-1-btn"))==null||_wa.addEventListener("click",()=>showPhase(1));const _BUILD_INSTRUCT_TEMPLATES={DE:e=>`Sprich in einem ${e} Tonfall.`,EN:e=>`Speak in a ${e} manner.`},_BUILD_INSTRUCT_LANG_NAMES={DE:"German",FR:"French",ES:"Spanish",IT:"Italian",PT:"Portuguese",NL:"Dutch",PL:"Polish"};function _buildAccentClause(langCode){const langName=_BUILD_INSTRUCT_LANG_NAMES[langCode];return langName?`Speak with an authentic native ${langName} accent \u2014 not American-accented, not an English speaker doing ${langName}.`:langCode==="EN"?"English with a neutral British or international accent, explicitly not American/US-accented.":""}function _buildInstruct(voiceProfile,emotion,voiceId){const p=(voiceProfile||"").trim(),e=(emotion||"").trim(),langCode=String(voiceId||"").split("_")[0].toUpperCase(),accent=_buildAccentClause(langCode);if(!e&&!p&&!accent)return"";const effectiveEmotion=(e&&langCode!=="EN"&&typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(e):"")||e,emotionTmpl=_BUILD_INSTRUCT_TEMPLATES.EN;return(e?[emotionTmpl(effectiveEmotion),p].filter(Boolean):[p,accent].filter(Boolean)).join(" ")}function _rehBackendIsFish(){var _a2;let id="";try{id=typeof backendById=="function"&&((_a2=backendById(rehState.backend))==null?void 0:_a2.id)||rehState.backend||""}catch{id=rehState.backend||""}return/fish/i.test(id)}const _REH_EMOTION_DE_EN={w\u00FCtend:"angry",zornig:"angry",erz\u00FCrnt:"angry",ver\u00E4rgert:"annoyed",gereizt:"irritated",traurig:"sad",melancholisch:"melancholic",betr\u00FCbt:"sad",niedergeschlagen:"dejected",\u00E4ngstlich:"scared",furchtsam:"fearful",ver\u00E4ngstigt:"frightened",panisch:"panicked",nerv\u00F6s:"nervous",fr\u00F6hlich:"happy",gl\u00FCcklich:"happy",freudig:"joyful",heiter:"cheerful",vergn\u00FCgt:"delighted",fl\u00FCsternd:"whispering",leise:"quiet",ged\u00E4mpft:"hushed",aufgeregt:"excited",begeistert:"enthusiastic",euphorisch:"euphoric",\u00FCberrascht:"surprised",erstaunt:"astonished",verbl\u00FCfft:"amazed",genervt:"annoyed",frustriert:"frustrated",verzweifelt:"desperate",hoffnungslos:"hopeless",resigniert:"resigned",entschlossen:"determined",entschieden:"decisive",selbstbewusst:"confident",stolz:"proud",arrogant:"arrogant",sch\u00FCchtern:"shy",verlegen:"embarrassed",unsicher:"uncertain",ironisch:"sarcastic",sarkastisch:"sarcastic",sp\u00F6ttisch:"mocking",h\u00F6hnisch:"scornful",ver\u00E4chtlich:"contemptuous",ernst:"serious",streng:"stern",autorit\u00E4r:"authoritative",befehlend:"commanding",sanft:"gentle",z\u00E4rtlich:"tender",liebevoll:"loving",warm:"warm",kalt:"cold",distanziert:"distant",gleichg\u00FCltig:"indifferent",gelangweilt:"bored",geheimnisvoll:"mysterious",unheimlich:"eerie",d\u00FCster:"ominous",bedrohlich:"threatening",dramatisch:"dramatic",theatralisch:"theatrical",pathetisch:"melodramatic",ruhig:"calm",gelassen:"composed",besonnen:"measured",schockiert:"shocked",entsetzt:"horrified",fassungslos:"stunned",z\u00F6gernd:"hesitant",verwirrt:"confused",ratlos:"bewildered",unschl\u00FCssig:"undecided",weinend:"tearful",schluchzend:"sobbing",trauernd:"grieving",gebrochen:"broken",schroff:"curt",barsch:"gruff",grob:"rough",abweisend:"dismissive",freundlich:"friendly",herzlich:"warm",einladend:"welcoming",spielerisch:"playful",neckend:"teasing",frech:"cheeky",schelmisch:"mischievous",romantisch:"romantic",sehns\u00FCchtig:"longing",verliebt:"infatuated",triumphierend:"triumphant",siegessicher:"victorious",erleichtert:"relieved",beruhigt:"reassured",schuldbewusst:"guilty",reum\u00FCtig:"remorseful",neugierig:"curious",interessiert:"interested",m\u00FCde:"weary",ersch\u00F6pft:"exhausted",wehm\u00FCtig:"wistful",nostalgisch:"nostalgic",bemerkend:"remarking",feststellend:"noting",sachlich:"matter-of-fact",n\u00FCchtern:"plain",flehend:"pleading",bittend:"imploring",warnend:"warning",mahnend:"admonishing",trotzig:"defiant",rebellisch:"rebellious",erschrocken:"startled",verst\u00F6rt:"disturbed"};function _rehEmotionEnglishTag(emotion){const raw=(emotion||"").trim();if(!raw)return"";const lower=raw.split(/[,;]\s*/)[0].trim().toLowerCase();if(_REH_EMOTION_DE_EN[lower])return _REH_EMOTION_DE_EN[lower];const stem=lower.replace(/(e|er|es|en|em)$/,"");if(stem.length>=4){for(const key in _REH_EMOTION_DE_EN)if(key.startsWith(stem))return _REH_EMOTION_DE_EN[key]}return/^[a-z\- ]+$/.test(lower)?lower:""}function _rehInlineTone(text,emotion){if(!_rehBackendIsFish()||/^\s*\[/.test(text))return text;const tag=_rehEmotionEnglishTag(emotion);return tag?`[${tag}] ${text}`:text}const REH_LANG_CODE={English:"EN",German:"DE",French:"FR",Spanish:"ES",Italian:"IT",Auto:"EN"};function rehDefaultLlmUrl(){try{if(typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url)return _appSettings.llm_url}catch{}return"http://localhost:11434/v1"}function rehCollectLlmEndpoints(){const seen=new Set,results=[],add=(url,label)=>{url&&(url=url.trim(),!(!url||seen.has(url))&&(seen.add(url),results.push({url,label:label||url})))};return add(rehDefaultLlmUrl(),"Active LLM"),document.querySelectorAll(".llm-local-url-inp, [data-llm-local-key]").forEach(inp=>{var _a2,_b2,_c2;const v=(_a2=inp.value)==null?void 0:_a2.trim(),def=inp.dataset.llmLocalDefault,key=inp.dataset.llmLocalKey||inp.dataset.dcUrlKey||"",card=inp.closest('[class*="llm-local-card"], [class*="llm-local"]'),name=((_c2=(_b2=card==null?void 0:card.querySelector(".llm-local-name"))==null?void 0:_b2.textContent)==null?void 0:_c2.trim())||key;add(v||def,name)}),document.querySelectorAll(".dc-url-inp").forEach(inp=>{var _a2,_b2,_c2;const card=inp.closest('[class*="llm-local-card"]');if(!card)return;const name=((_b2=(_a2=card.querySelector(".llm-local-name"))==null?void 0:_a2.textContent)==null?void 0:_b2.trim())||"";add(((_c2=inp.value)==null?void 0:_c2.trim())||inp.dataset.dcDefault,name)}),[["http://localhost:11434/v1","Ollama"],["http://localhost:8000/v1","vLLM"],["http://localhost:1234/v1","LM Studio"],["http://localhost:28080/v1","llama-swap"],["http://localhost:14000/v1","LiteLLM"]].forEach(([u,l])=>add(u,l)),results}function rehInitLlmField(){const u=$("reh-llm-url");if(!u)return;u.value||(u.value=rehDefaultLlmUrl());const dl=$("reh-llm-url-list");dl&&(dl.innerHTML=rehCollectLlmEndpoints().map(e=>``).join(""))}(_xa=$("reh-llm-refresh"))==null||_xa.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;(_ya=$("reh-autodesign-cancel"))==null||_ya.addEventListener("click",()=>{rehDesignCancelled=!0}),(_za=$("reh-autodesign-btn"))==null||_za.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",hardGender=gender==="M"?"male":gender==="F"?"female":"gender-neutral",hardAge=info.age||"age unspecified",hardLanguage=info.language||language||"the script language",desc=[`HARD CASTING CONSTRAINTS: ${hardGender} voice, apparent age ${hardAge}, spoken in ${hardLanguage}.`,info.description||`Character named ${sp}; natural expressive audiobook voice.`,"Make this voice audibly distinct from the other cast members: use a memorable vocal fingerprint, not a generic narrator voice."].join(" "),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,age:info.age||"",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}(_Aa=$("reh-matchlib-btn"))==null||_Aa.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(typeof window.fetchVoiceLibrary=="function"?window.fetchVoiceLibrary():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||"",age:v.age||"",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:""};(_Ba=$("reh-matchonline-btn"))==null||_Ba.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")} - ${escHtml(info.voiceId||sp)} - ${info.age?`${escHtml(info.age)}`:""} -
-
${escHtml(info.desc||"")}
-
${escHtml(info.step||"Designing voice\u2026")}
`}else if(state==="done"&&panel){const descEl=panel.querySelector(".reh-cdp-desc"),metaEl=panel.querySelector(".reh-cdp-meta"),stepEl=panel.querySelector(".reh-cdp-step");if(stepEl&&stepEl.remove(),descEl){const t=descEl.textContent;t.length>160&&(descEl.textContent=t.slice(0,157)+"\u2026")}panel.classList.add("done")}else state==="err"&&panel&&panel.remove()}(_Ca=$("reh-start-btn"))==null||_Ca.addEventListener("click",()=>{var _a2,_b2;const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a backend first","error");return}rehState.backend=backend,rehState.narratorVoice=((_b2=$("reh-narrator-voice"))==null?void 0:_b2.value)||"",rehState.lineIndex=0,rehState.clips=[],rehState.playing=!1,rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.practiceStart=null,rehState.practiceEnd=null,buildScriptPage(),showPhase(3),highlightCurrentLine()});const REH_STAGE_FONT_KEY="ttsvc_reh_stage_scale",REH_CAST_COLLAPSED_KEY="ttsvc_reh_cast_collapsed";function rehStageScale(){const v=parseFloat(localStorage.getItem(REH_STAGE_FONT_KEY)||"1");return isNaN(v)?1:Math.max(.7,Math.min(2,v))}function rehApplyStageFont(){const scale=String(rehStageScale()),wrap=document.querySelector(".reh-page-wrap");wrap&&wrap.style.setProperty("--reh-stage-scale",scale);const page=$("reh-a4-page");page&&page.style.setProperty("--reh-stage-scale",scale)}function rehStageFontStep(delta){const next=Math.max(.7,Math.min(2,Math.round((rehStageScale()+delta)*100)/100));localStorage.setItem(REH_STAGE_FONT_KEY,String(next)),rehApplyStageFont()}function rehApplyCastCollapsed(){const row=$("reh-cast-row"),stage=$("reh-stage-area");if(!row)return;const collapsed=localStorage.getItem(REH_CAST_COLLAPSED_KEY)==="1";row.classList.toggle("is-collapsed",collapsed),stage&&stage.classList.toggle("side-collapsed",collapsed);const btn=$("reh-cast-toggle");if(btn){btn.setAttribute("aria-expanded",String(!collapsed)),btn.title=collapsed?"Expand character list":"Collapse to avatars";const icon=btn.querySelector(".mdi");icon&&(icon.className="mdi "+(collapsed?"mdi-chevron-right":"mdi-chevron-left"))}}function rehToggleCast(){const collapsed=localStorage.getItem(REH_CAST_COLLAPSED_KEY)==="1";localStorage.setItem(REH_CAST_COLLAPSED_KEY,collapsed?"0":"1"),rehApplyCastCollapsed()}(_Da=$("reh-font-inc"))==null||_Da.addEventListener("click",()=>rehStageFontStep(.1)),(_Ea=$("reh-font-dec"))==null||_Ea.addEventListener("click",()=>rehStageFontStep(-.1)),(_Fa=$("reh-cast-toggle"))==null||_Fa.addEventListener("click",rehToggleCast);function renderCastStrip(){var _a2,_b2,_c2;const castStrip=$("reh-cast-strip");if(!castStrip)return;const scriptTitle=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"";_rehEnsureLibCharsCache(scriptTitle);const names=Object.keys(rehState.cast),lineCountFor=sp=>rehState.lines.filter(l=>l.speaker===sp&&l.type==="dialog").length,libRecByNameStage=new Map((_rehLibCharsCache||[]).map(r=>[String(r.name||"").trim().toLowerCase(),r])),sortMode=((_b2=$("reh-cast-side-sort"))==null?void 0:_b2.value)||localStorage.getItem("reh_cast_side_sort")||"lines",query=(((_c2=$("reh-cast-side-search"))==null?void 0:_c2.value)||"").trim().toLowerCase();let rows=names.map(sp=>{const displayName=sp===REH_NARRATOR_KEY?"Narrator":sp,libRec=sp===REH_NARRATOR_KEY?null:libRecByNameStage.get(String(sp).trim().toLowerCase()),fallbackName=displayName==="Narrator"?displayName:displayName.replace(/\w\S*/g,w=>w[0].toUpperCase()+w.slice(1).toLowerCase());return{sp,displayName:(libRec==null?void 0:libRec.name)||fallbackName,n:lineCountFor(sp),libRec}});query&&(rows=rows.filter(r=>r.displayName.toLowerCase().includes(query))),rows.sort((a,b)=>sortMode==="name"?a.displayName.localeCompare(b.displayName):b.n-a.n||a.displayName.localeCompare(b.displayName)),castStrip.innerHTML=rows.map(({sp,displayName,n,libRec})=>{const c=rehState.cast[sp],isMe=c.voice==="me",avatarHtml=libRec!=null&&libRec.image?``:`${escHtml((displayName||"?")[0].toUpperCase())}`;return`
- ${avatarHtml} - ${escHtml(displayName)} - ${isMe?'':""} - ${n} -
`}).join(""),castStrip.querySelectorAll(".ab-char-item").forEach(item=>{item.addEventListener("click",()=>{const line=document.querySelector(`.reh-block[data-speaker="${CSS.escape(item.dataset.speaker)}"]`);line&&line.scrollIntoView({behavior:"smooth",block:"center"})})});const lbl=$("reh-cast-toggle-label");lbl&&(lbl.textContent=`Characters (${names.length})`);const searchInp=$("reh-cast-side-search"),sortSel=$("reh-cast-side-sort");searchInp&&!searchInp.dataset.wired&&(searchInp.dataset.wired="1",searchInp.addEventListener("input",()=>renderCastStrip())),sortSel&&!sortSel.dataset.wired&&(sortSel.dataset.wired="1",sortSel.value=sortMode,sortSel.addEventListener("change",()=>{localStorage.setItem("reh_cast_side_sort",sortSel.value),renderCastStrip()}))}function buildScriptPage(){var _a2,_b2;const titleEl=$("reh-page-title");titleEl&&(titleEl.textContent=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Script"),renderCastStrip(),rehApplyStageFont(),rehApplyCastCollapsed(),typeof _lineAudioSyncDots=="function"&&_lineAudioSyncDots().catch(()=>{});const linesEl=$("reh-script-lines");linesEl&&(linesEl.innerHTML=rehState.lines.map((line,i)=>{const isCached=rehState.synthCache.has(i),isStale=rehState.staleLines.has(i),synthDot=``,reSynthBtn=``,editBtn=``,note=rehState.lines[i].note||"",noteArea=`
- ${editBtn} - - -
`;if(line.hidden&&!(rehState.bulkMode&&rehState.showHidden))return"";const _bSel=rehState.bulkSel.has(i),bulkCheck=rehState.bulkMode?``:"",lineFlags=(line.ignored?" reh-line-ignored":"")+(line.hidden?" reh-line-hidden":"")+(_bSel?" reh-selected":"");if(typeof audiobookIsChapter=="function"&&audiobookIsChapter(line)){const label=(typeof stripMarkdown=="function"?stripMarkdown(line.text||""):line.text||"").trim();return`
- ${bulkCheck} - - ${label?escHtml(label):"Chapter"} - - ${editBtn} ${synthDot} -
`}switch(line.type){case"act":return`
${bulkCheck}${escHtml(line.text)} ${editBtn} ${synthDot}
`;case"scene":return`
- ${bulkCheck} - ${escHtml(line.text)} - ${editBtn} ${synthDot} -
`;case"action":{const narrPlayBtn=``;return`
- ${bulkCheck}${narrPlayBtn}${renderMarkdownInline(line.text)} - ${editBtn} ${synthDot} -
`}case"pagebreak":{const pbLabel=line.page?`\u2014 Page ${line.page} \u2014`:"\u2014 Page break \u2014";return`
${bulkCheck}${pbLabel}
`}case"transition":return`
${bulkCheck}${escHtml(line.text)}
`;case"direction":{const indent=line.speaker?"text-align:center;":"";return`
${bulkCheck}${escHtml(line.text)}
`}case"dialog":{const c=rehState.cast[line.speaker]||{voice:"",color:"#89b4fa"},isMe=c.voice==="me",emoInfo=getEmotionInfo(line.emotion||"");return`
- ${bulkCheck} -
-
- - ${escHtml(line.speaker)} - - ${isMe?' Me':' TTS'} - ${isMe?"":``} - ${synthDot} - ${reSynthBtn} -
-
${renderMarkdownInline(line.text)}
-
- ${noteArea} -
`}default:return""}}).join(""),linesEl.querySelectorAll(".reh-emo-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),openEmoPicker(parseInt(btn.dataset.index),btn)})}),linesEl.querySelectorAll(".reh-edit-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),startInlineEdit(parseInt(btn.dataset.index))})}),linesEl.querySelectorAll(".reh-block-dialog").forEach(el=>{el.addEventListener("dblclick",e=>{e.stopPropagation(),startInlineEdit(parseInt(el.closest("[data-index]").dataset.index))})}),linesEl.querySelectorAll(".reh-action-text").forEach(el=>{el.addEventListener("dblclick",e=>{e.stopPropagation(),startInlineEdit(parseInt(el.closest("[data-index]").dataset.index))})}),linesEl.querySelectorAll(".reh-line-play-avatar").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation();const idx=parseInt(btn.dataset.index);if(rehState.playing&&rehState.lineIndex===idx){pausePlay();return}stopPlay(),hideRecOverlay(),rehState.lineIndex=idx,highlightCurrentLine(),startPlay()})}),linesEl.querySelectorAll(".reh-gutter-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation();const idx=parseInt(btn.dataset.index);e.shiftKey?rehState.practiceEnd=rehState.practiceEnd===idx?null:idx:(stopPlay(),hideRecOverlay(),rehState.lineIndex=idx,highlightCurrentLine(),startPlay()),updatePracticeRange()})}),linesEl.querySelectorAll("[data-index]").forEach(el=>{el.addEventListener("click",e=>{if(e.target.closest(".reh-emo-btn, .reh-edit-btn, .reh-gutter-btn, .reh-note-btn, .reh-resynth-btn, .reh-line-play-avatar, textarea, select, .vp-root"))return;const idx=parseInt(el.dataset.index);if(rehState.bulkMode){_toggleBulkSel(idx,e.shiftKey);return}stopPlay(),hideRecOverlay(),rehState.lineIndex=idx,highlightCurrentLine()})}),linesEl.querySelectorAll(".reh-resynth-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),synthOneLine(parseInt(btn.id.replace("reh-rsb-","")))})}),linesEl.querySelectorAll(".reh-note-area").forEach(area=>{const idx=parseInt(area.dataset.index),btn=area.querySelector(".reh-note-btn"),ta=area.querySelector(".reh-note-ta");!btn||!ta||(ta.value.trim()&&ta.classList.add("open"),btn.addEventListener("click",e=>{e.stopPropagation(),ta.classList.toggle("open"),ta.classList.contains("open")&&ta.focus()}),ta.addEventListener("input",()=>{rehState.lines[idx].note=ta.value,btn.classList.toggle("has-note",!!ta.value.trim())}),ta.addEventListener("click",e=>e.stopPropagation()))}),linesEl.classList.toggle("reh-bulk-mode",rehState.bulkMode),(_b2=document.querySelector(".reh-page-wrap"))==null||_b2.classList.toggle("reh-bulk-mode",rehState.bulkMode),rehState.bulkMode&&linesEl.querySelectorAll(".reh-bulk-check").forEach(cb=>{cb.addEventListener("click",e=>{e.stopPropagation(),_toggleBulkSel(parseInt(cb.dataset.bulk),e.shiftKey)})}),applyPageMode(),_checkToneStyleSupport())}function _toggleBulkSel(i,range=!1){if(Number.isFinite(i)){if(range&&rehState.bulkAnchor!==null&&Number.isFinite(rehState.bulkAnchor)){const a=Math.min(rehState.bulkAnchor,i),b=Math.max(rehState.bulkAnchor,i),indices=[];for(let n=a;n<=b;n++)rehState.lines[n]&&!rehState.lines[n].hidden&&indices.push(n);const deselect=indices.length&&indices.every(n=>rehState.bulkSel.has(n));indices.forEach(n=>{deselect?rehState.bulkSel.delete(n):rehState.bulkSel.add(n),_refreshBulkLine(n)}),rehState.bulkAnchor=i}else rehState.bulkSel.has(i)?rehState.bulkSel.delete(i):rehState.bulkSel.add(i),rehState.bulkAnchor=i,_refreshBulkLine(i);_updateBulkCount()}}function _refreshBulkLine(i){const sel=rehState.bulkSel.has(i);document.querySelectorAll(`.reh-bulk-check[data-bulk="${i}"]`).forEach(cb=>{cb.classList.toggle("checked",sel);const icon=cb.querySelector(".mdi");icon&&(icon.className="mdi "+(sel?"mdi-checkbox-marked":"mdi-checkbox-blank-outline"))}),document.querySelectorAll(`[data-index="${i}"]`).forEach(el=>el.classList.toggle("reh-selected",sel))}function _updateBulkCount(){const el=$("reh-bulk-count");el&&(el.textContent=`${rehState.bulkSel.size} selected`)}function setBulkMode(on){rehState.bulkMode=on,on||(rehState.bulkSel.clear(),rehState.bulkAnchor=null);const bar=$("reh-bulk-bar");bar&&(bar.hidden=!on);const btn=$("reh-bulk-toggle");btn&&btn.classList.toggle("active",on),_updateBulkCount(),rehState.lines.length&&buildScriptPage()}function _bulkApply(fn,{keepSelection=!1}={}){if(!rehState.bulkSel.size){toast("No lines selected","error");return}[...rehState.bulkSel].forEach(i=>{const l=rehState.lines[i];l&&fn(l,i)}),keepSelection||rehState.bulkSel.clear(),_updateBulkCount(),buildScriptPage()}function _bulkDelete(){if(!rehState.bulkSel.size){toast("No lines selected","error");return}const victims=[...rehState.bulkSel].sort((a,b)=>b-a);victims.forEach(i=>{rehState.lines.splice(i,1),_reindexLineState(i)}),rehState.bulkSel.clear(),rehState.lineIndex>=rehState.lines.length&&(rehState.lineIndex=Math.max(0,rehState.lines.length-1)),_updateBulkCount(),buildScriptPage(),highlightCurrentLine(),toast(`Deleted ${victims.length} line${victims.length!==1?"s":""}`,"success")}function _reindexLineState(d){const shift=(collection,isMap)=>{const out=isMap?new Map:new Set;for(const entry of collection){const k=isMap?entry[0]:entry;if(k===d)continue;const nk=k>d?k-1:k;isMap?out.set(nk,entry[1]):out.add(nk)}return out};rehState.synthCache=new RehAudioCache(shift(rehState.synthCache,!0)),rehState.staleLines=shift(rehState.staleLines,!1);const sel=shift(rehState.bulkSel,!1);rehState.bulkSel.clear(),sel.forEach(v=>rehState.bulkSel.add(v)),rehState.practiceStart!=null&&rehState.practiceStart>d&&rehState.practiceStart--,rehState.practiceEnd!=null&&rehState.practiceEnd>d&&rehState.practiceEnd--,rehState.lineIndex>d&&rehState.lineIndex--}const PAGE_MODES=["auto","scroll","pdf"];let _pageMode=localStorage.getItem("reh-page-mode")||"pdf";function applyPageMode(){const wrap=document.querySelector(".reh-page-wrap"),a4=$("reh-a4-page");wrap&&(_pageMode==="scroll"?(wrap.querySelectorAll(".reh-paper").forEach(p=>{[...p.children].forEach(c=>{c.classList.contains("reh-paper-num")||a4==null||a4.appendChild(c)}),p.remove()}),wrap.classList.remove("paginated"),a4&&(a4.style.display=""),document.querySelectorAll(".reh-block-pagebreak").forEach(el=>el.style.display="none")):paginateScript(_pageMode==="pdf"?{respectBreaks:!0}:{respectBreaks:!1}),_syncPageModeBtn())}function _syncPageModeBtn(){const btn=$("reh-page-mode-btn");if(!btn)return;const icons={auto:"mdi-file-document-outline",scroll:"mdi-format-align-justify",pdf:"mdi-book-open-page-variant"},labels={auto:"A4 pages",scroll:"Scroll",pdf:"PDF pages"},nextMode=PAGE_MODES[(PAGE_MODES.indexOf(_pageMode)+1)%PAGE_MODES.length];btn.innerHTML=` ${labels[nextMode]||labels.auto}`,btn.title=`Currently: ${labels[_pageMode]} \u2014 click to switch to ${labels[nextMode]}`}function cyclePageMode(){const idx=PAGE_MODES.indexOf(_pageMode);_pageMode=PAGE_MODES[(idx+1)%PAGE_MODES.length],localStorage.setItem("reh-page-mode",_pageMode),applyPageMode(),rehState.lines.length&&buildScriptPage()}function paginateScript({respectBreaks=!1}={}){const wrap=document.querySelector(".reh-page-wrap"),linesEl=$("reh-script-lines"),titleEl=$("reh-page-title");if(!wrap||!linesEl)return;const blocks=[...linesEl.children];if(!blocks.length)return;wrap.classList.add("paginated");const a4=$("reh-a4-page");a4&&(a4.style.display="none"),wrap.querySelectorAll(".reh-paper").forEach(p=>p.remove());const PAGE_CONTENT=1027,TITLE_SPACE=64;let pageNum=0,page=null,used=0,isFirstPage=!0;if(respectBreaks){for(const b of blocks)if(b.classList.contains("reh-block-pagebreak")){const i=parseInt(b.dataset.index),l=rehState.lines[i];l&&l.page&&(pageNum=Math.max(0,l.page-(b===blocks[0]?0:1)));break}}const newPage=forcedNum=>{forcedNum!==void 0?pageNum=forcedNum:pageNum++,page=document.createElement("div"),page.className="reh-paper";const num=document.createElement("div");if(num.className="reh-paper-num",num.textContent=pageNum+".",page.appendChild(num),used=0,isFirstPage&&titleEl){const t=titleEl.cloneNode(!0);t.style.display="",page.appendChild(t),used+=TITLE_SPACE}isFirstPage=!1,wrap.appendChild(page)};newPage();for(const block of blocks){if(respectBreaks&&block.classList.contains("reh-block-pagebreak")){const i=parseInt(block.dataset.index),l=rehState.lines[i];newPage(l&&l.page?l.page:void 0);continue}page.appendChild(block);const cs=getComputedStyle(block),h=block.offsetHeight+(parseFloat(cs.marginTop)||0)+(parseFloat(cs.marginBottom)||0);!respectBreaks&&used+h>PAGE_CONTENT&&used>(isFirstPage?TITLE_SPACE:0)&&(newPage(),page.appendChild(block)),used+=h}}function isPracticeRange(idx){const s=rehState.practiceStart,e=rehState.practiceEnd;return s===null?!1:idx>=s&&(e===null||idx<=e)}function startInlineEdit(idx){const line=rehState.lines[idx];if(!line)return;const dialogEl=document.getElementById("reh-diag-"+idx),actionEl=document.querySelector(`[data-index="${idx}"] .reh-action-text`),sceneEl=document.querySelector(`.reh-scene[data-index="${idx}"] span[style*="flex:1"]`),targetEl=dialogEl||actionEl||sceneEl;if(!targetEl||targetEl.tagName==="TEXTAREA")return;const orig=line.text,ta=document.createElement("textarea");ta.value=orig,ta.style.cssText="width:100%;min-height:54px;font-size:14px;line-height:1.6;padding:5px 8px;border:1px solid var(--accent);border-top:none;border-radius:0 0 3px 3px;resize:vertical;background:#fffff8;font-family:inherit;display:block;box-sizing:border-box;outline:none;";const toolbar=document.createElement("div");toolbar.className="reh-fmt-toolbar",toolbar.innerHTML=` - - - - - - Ctrl+Enter save \xB7 Esc cancel - `,toolbar.querySelectorAll(".reh-fmt-btn").forEach(btn=>{btn.addEventListener("mousedown",e=>{e.preventDefault();const wrap=btn.dataset.b,s=ta.selectionStart,end=ta.selectionEnd,sel=ta.value.slice(s,end)||"text";ta.value=ta.value.slice(0,s)+wrap+sel+wrap+ta.value.slice(end),ta.setSelectionRange(s+wrap.length,s+wrap.length+sel.length),ta.focus()})}),targetEl.style.display="none",targetEl.before(toolbar),toolbar.after(ta),ta.focus(),ta.select();const save=()=>{const newText=ta.value.trim()||orig;line.text=newText,targetEl.innerHTML=renderMarkdownInline(newText),targetEl.style.display="",toolbar.remove(),ta.remove(),rehState.synthCache.delete(idx);const dot=document.getElementById("reh-syd-"+idx);dot&&(dot.style.display="none")};ta.addEventListener("keydown",e=>{e.key==="Escape"&&(ta.value=orig,save()),e.key==="Enter"&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),save())}),ta.addEventListener("blur",save)}function openScriptEditorModal(){var _a2,_b2,_c2;const currentTitle=((_a2=$("reh-page-title"))==null?void 0:_a2.textContent)||((_b2=$("reh-script-title"))==null?void 0:_b2.value)||"Script",currentScript=linesToScriptText()||((_c2=$("reh-script-text"))==null?void 0:_c2.value)||"",modal=document.createElement("div");modal.className="reh-modal-overlay",modal.innerHTML=` -
-
-

Edit Script

- -
-
- - -
- -
- - -
-
- `,document.body.appendChild(modal);const close=()=>modal.remove();modal.querySelector("#_reh-mc").onclick=close,modal.querySelector("#_reh-mcan").onclick=close,modal.addEventListener("click",e=>{e.target===modal&&close()}),modal.querySelector("#_reh-mapp").onclick=()=>{const newTitle=modal.querySelector("#_reh-mt").value.trim()||"Script",newScript=modal.querySelector("#_reh-ms").value,newLines=parseScript(newScript);if(!newLines.filter(l=>l.type==="dialog").length){toast("No dialog found in the edited script","error");return}newLines.forEach(nl=>{if(nl.type!=="dialog")return;const old=rehState.lines.find(ol=>ol.type==="dialog"&&ol.speaker===nl.speaker&&ol.text===nl.text);old&&(nl.emotion=old.emotion)});const detected=detectCharacters(newLines);Object.entries(detected).forEach(([sp])=>{rehState.cast[sp]&&(detected[sp]={...detected[sp],...rehState.cast[sp]})}),rehState.lines=newLines,rehState.cast=detected,rehState.lineIndex=Math.min(rehState.lineIndex,newLines.length-1),rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.practiceStart=null,rehState.practiceEnd=null,$("reh-script-title")&&($("reh-script-title").value=newTitle),$("reh-page-title")&&($("reh-page-title").textContent=newTitle),buildScriptPage(),highlightCurrentLine(),close(),toast("Script updated","success")}}function updatePracticeRange(){const start=rehState.practiceStart,end=rehState.practiceEnd;document.querySelectorAll(".reh-block").forEach(el=>{const idx=parseInt(el.dataset.index);el.classList.toggle("reh-in-range",start!==null&&idx>=start&&(end===null||idx<=end)),el.classList.toggle("reh-range-start",start!==null&&idx===start),el.classList.toggle("reh-range-end",end!==null&&idx===end);const gb=el.querySelector(".reh-gutter-btn");gb&&(gb.textContent=idx===start?"\u25B6":idx===end?"\u25A0":"")});const info=$("reh-practice-info");if(info){const show=end!==null;if(info.classList.toggle("visible",show),info.hidden=!show,show){const f=$("reh-practice-from"),t=$("reh-practice-to");f&&(f.textContent=(start!==null?start:0)+1),t&&(t.textContent=end+1)}}}function clearPracticeRange(){rehState.practiceStart=null,rehState.practiceEnd=null,updatePracticeRange()}function setPracticeRangeFromSelection(){const picked=[...rehState.bulkSel].sort((a,b)=>a-b);if(!picked.length){toast("Select the lines you want to rehearse first","error");return}rehState.practiceStart=picked[0],rehState.practiceEnd=picked[picked.length-1],rehState.lineIndex=rehState.practiceStart,setBulkMode(!1),updatePracticeRange(),highlightCurrentLine(),toast(`Practice range set: lines ${rehState.practiceStart+1}\u2013${rehState.practiceEnd+1}`,"success")}(_Ga=$("reh-practice-clear"))==null||_Ga.addEventListener("click",clearPracticeRange),(_Ha=$("reh-bulk-range"))==null||_Ha.addEventListener("click",setPracticeRangeFromSelection);let rehEmoPicker=null;function openEmoPicker(idx,anchorBtn){closeEmoPicker();const line=rehState.lines[idx],allEmos=[...REH_EMOTIONS,...rehCustomEmotions],rect=anchorBtn.getBoundingClientRect(),left=Math.min(rect.left,window.innerWidth-265),topBelow=rect.bottom+4,topAbove=rect.top-4,top=window.innerHeight-topBelow>=240?topBelow:topAbove-260,pop=document.createElement("div");pop.className="reh-emo-popover",pop.style.cssText=`position:fixed;z-index:1000;top:${top}px;left:${left}px;width:258px;`,pop.innerHTML=`
- -
-
- -
-
`,document.body.appendChild(pop),rehEmoPicker=pop;const searchEl=pop.querySelector(".reh-emo-search"),listEl=pop.querySelector(".reh-emo-list"),applyBtn=pop.querySelector("#_emo-custom-apply");function renderList(q=""){const lq=q.toLowerCase(),filtered=allEmos.filter(e=>!q||e.label.toLowerCase().includes(lq)||e.value.toLowerCase().includes(lq));listEl.innerHTML=filtered.map(e=>`
- ${e.emoji} - ${escHtml(e.label)} - ${line.emotion===e.value?'':""} -
`).join(""),listEl.querySelectorAll(".reh-emo-item").forEach(item=>{item.addEventListener("mousedown",e=>{e.preventDefault(),selectEmotion(idx,item.dataset.value,anchorBtn),closeEmoPicker()})})}searchEl.addEventListener("input",()=>renderList(searchEl.value)),searchEl.addEventListener("keydown",e=>{if(e.key==="Escape"&&closeEmoPicker(),e.key==="Enter"){const val=searchEl.value.trim();val&&(selectEmotion(idx,val,anchorBtn),closeEmoPicker())}}),applyBtn.addEventListener("mousedown",e=>{e.preventDefault();const val=searchEl.value.trim();if(!val)return;if(![...REH_EMOTIONS,...rehCustomEmotions].find(e2=>e2.value===val)){rehCustomEmotions.push({emoji:"\u2728",label:val,value:val,custom:!0});try{localStorage.setItem("reh-custom-emotions",JSON.stringify(rehCustomEmotions))}catch{}}selectEmotion(idx,val,anchorBtn),closeEmoPicker()}),renderList(),searchEl.focus(),setTimeout(()=>document.addEventListener("mousedown",_closePickerOnOutside),50)}function _closePickerOnOutside(e){rehEmoPicker&&!rehEmoPicker.contains(e.target)&&closeEmoPicker()}function closeEmoPicker(){rehEmoPicker&&(rehEmoPicker.remove(),rehEmoPicker=null),document.removeEventListener("mousedown",_closePickerOnOutside)}function _markSynthDot(idx,state){const dot=document.getElementById("reh-syd-"+idx);if(!dot)return;dot.className="reh-synth-dot"+(state==="stale"?" stale":"")+(state==="synthesizing"?" synthesizing":""),dot.style.display=state?"":"none",dot.title=state==="stale"?"Tone changed \u2014 needs re-synthesis":state==="synthesizing"?"Synthesizing\u2026":"Pre-synthesized";const block=dot.closest("[data-index]");block&&block.classList.toggle("reh-line-synthesizing",state==="synthesizing")}function _showReSynthBtn(idx,show){const btn=document.getElementById("reh-rsb-"+idx);btn&&(btn.hidden=!show)}function _rehSyncCastVoiceFromLibrary(rec){if(!window.rehState||!rehState.lines||!rehState.lines.length||!rec||!rec.name||!rec.voice)return;const openBook=typeof _lineAudioBookName=="function"?_lineAudioBookName():"";if(!openBook||String(rec.book||"").trim().toLowerCase()!==openBook.trim().toLowerCase())return;const target=String(rec.name).toUpperCase().trim(),sp=Object.keys(rehState.cast).find(k=>String(k).toUpperCase().trim()===target);if(!sp)return;const c=rehState.cast[sp];!c||c.voice===rec.voice||(c.voice=rec.voice,c.voiceData=getVoiceData(rec.voice),rehState.lines.forEach((line,idx)=>{line.type==="dialog"&&line.speaker===sp&&rehState.synthCache.has(idx)&&(rehState.synthCache.delete(idx),rehState.staleLines.add(idx),typeof _markSynthDot=="function"&&_markSynthDot(idx,"stale"))}),typeof _updateStaleBatchBtn=="function"&&_updateStaleBatchBtn(),typeof renderCastList=="function"&&renderCastList())}window._rehSyncCastVoiceFromLibrary=_rehSyncCastVoiceFromLibrary;async function synthOneLine(idx){const line=rehState.lines[idx];if(!line||line.type!=="dialog")return;const c=rehState.cast[line.speaker];if(!c||!c.voice||c.voice==="me")return;const instruct=_buildInstruct(c.instruct,line.emotion,c.voice);_showReSynthBtn(idx,!1),_markSynthDot(idx,"synthesizing");try{const blob=await fetchTtsPreviewBlob(c.voice,_rehInlineTone(stripMarkdown(line.text),line.emotion),"wav",instruct,_ttsBackendForVoice(c.voice,rehState.backend));rehState.synthCache.set(idx,blob),rehState.staleLines.delete(idx),preDecodeBlob(idx,blob),_markSynthDot(idx,"ok"),toast("Re-synthesized line "+(idx+1),"success")}catch(e){_markSynthDot(idx,null),_showReSynthBtn(idx,!0),toast("Synthesis failed: "+e.message,"error")}}function selectEmotion(idx,value,anchorBtn){rehState.lines[idx].emotion=value,rehState.synthCache.has(idx)&&(rehState.synthCache.delete(idx),rehState.staleLines.add(idx),_markSynthDot(idx,"stale"),_updateStaleBatchBtn()),_showReSynthBtn(idx,!0);const info=getEmotionInfo(value);anchorBtn.className="reh-emo-btn"+(value?" has-emotion":""),anchorBtn.innerHTML=`${info.emoji?info.emoji+" ":""}${escHtml(info.label)} `,value&&_checkToneStyleSupport()}function _rehToneCmpRow(backend,isCurrent){const check=ok=>ok?'':'',action=isCurrent?'current':``;return` - ${escHtml(backend.label)} - ${check(backend.style_aware)} - ${check(backend.uses_wav)} - ${action} - `}function _checkToneStyleSupport(){const warn=$("reh-tone-warn");if(!warn)return;const txtEl=$("reh-tone-warn-txt"),b=typeof backendById=="function"?backendById(rehState.backend):null;if(!b){warn.hidden=!0;return}const all=typeof availableTtsBackends=="function"?availableTtsBackends():[],hasTone=rehState.lines.some(l=>l.type==="dialog"&&l.emotion);if(_rehBackendIsFish())txtEl&&(txtEl.innerHTML=`${escHtml(b.label)} keeps each character\u2019s voice consistent and applies tone. Per-line tones are sent as inline [tags] (e.g. [whisper], [excited], [laughing]). You can also type a custom tone like [professional broadcast tone] \u2014 S2 supports free-form descriptions. Fish-Speech S2 \u2197`),warn.hidden=!1;else if(!b.style_aware&&hasTone){const styleAware=all.find(x=>x.style_aware&&x.uses_wav)||all.find(x=>x.style_aware);txtEl&&(txtEl.innerHTML=styleAware?`${_rehToneCmpRow(b,!0)}${_rehToneCmpRow(styleAware,!1)}
Tone controlVoice stays identical
`:`${escHtml(b.label)} keeps each character\u2019s voice consistent but has weak tone control \u2014 tone picks may have little effect.`),warn.hidden=!1}else if(b.style_aware&&!b.uses_wav){const wavBackend=all.find(x=>x.uses_wav&&x.style_aware)||all.find(x=>x.uses_wav),qwenHint=/qwen|voice design|custom/i.test((b.id||"")+" "+(b.label||""))?'

Qwen3TTS tone is sent as the per-line style/instruct text, so this is the right path for directed delivery.

':"";txtEl&&(txtEl.innerHTML=wavBackend?`${_rehToneCmpRow(b,!0)}${_rehToneCmpRow(wavBackend,!1)}
Tone controlVoice stays identical
${qwenHint}`:`${escHtml(b.label)} gives strong tone but re-generates a fresh voice each line, so a character won\u2019t sound the same throughout.${qwenHint}`),warn.hidden=!1}else warn.hidden=!0}(_Ia=$("reh-tone-warn-close"))==null||_Ia.addEventListener("click",()=>{const w=$("reh-tone-warn");w&&(w.hidden=!0)}),(_Ja=$("reh-tone-warn"))==null||_Ja.addEventListener("click",e=>{const btn=e.target.closest(".reh-tone-switch-btn");if(!btn)return;const id=btn.dataset.backendId,sel=$("reh-backend-select");sel&&[...sel.options].some(o=>o.value===id)&&(sel.value=id),rehState.backend=id,_checkToneStyleSupport();const b=typeof backendById=="function"?backendById(id):null;toast("Switched to "+(b?b.label:id),"success")}),(_Ka=$("reh-tb-play"))==null||_Ka.addEventListener("click",()=>{rehState.playing?pausePlay():startPlay()}),(_La=$("reh-tb-stop"))==null||_La.addEventListener("click",()=>{var _a2;stopPlay(),rehState.lineIndex=(_a2=rehState.practiceStart)!=null?_a2:0,highlightCurrentLine(),hideRecOverlay()}),(_Ma=$("reh-tb-prev"))==null||_Ma.addEventListener("click",()=>{stopPlay(),rehState.lineIndex=Math.max(0,rehState.lineIndex-1),highlightCurrentLine(),hideRecOverlay()}),(_Na=$("reh-tb-next"))==null||_Na.addEventListener("click",()=>{stopPlay(),rehState.lineIndex=Math.min(rehState.lines.length-1,rehState.lineIndex+1),highlightCurrentLine(),hideRecOverlay()}),(_Oa=$("reh-tb-repeat"))==null||_Oa.addEventListener("click",()=>{var _a2;rehState.repeat=!rehState.repeat,(_a2=$("reh-tb-repeat"))==null||_a2.classList.toggle("reh-btn-active",rehState.repeat)}),(_Pa=$("reh-skip-desc-toggle"))==null||_Pa.addEventListener("change",function(){rehState.skipDescriptions=this.checked}),(_Qa=$("reh-edit-script-btn"))==null||_Qa.addEventListener("click",openScriptEditorModal),(_Ra=$("reh-fountain-btn"))==null||_Ra.addEventListener("click",exportFountain),(_Sa=$("reh-fountain-export-p4"))==null||_Sa.addEventListener("click",exportFountain),(_Ta=$("reh-fdx-export-btn"))==null||_Ta.addEventListener("click",exportFDX),(_Ua=$("reh-osf-export-btn"))==null||_Ua.addEventListener("click",exportOSF),(_Va=$("reh-exit-btn"))==null||_Va.addEventListener("click",()=>{stopPlay(),stopRehMic(),rehState.clips.length?(renderSummary(),showPhase(4)):showPhase(2)}),(_Wa=$("reh-page-title"))==null||_Wa.addEventListener("dblclick",function(){this.contentEditable="true",this.style.outline="2px solid var(--accent)",this.style.borderRadius="3px",this.focus();const range=document.createRange();range.selectNodeContents(this),window.getSelection().removeAllRanges(),window.getSelection().addRange(range)}),(_Xa=$("reh-page-title"))==null||_Xa.addEventListener("blur",function(){if(this.contentEditable==="true"){this.contentEditable="false",this.style.outline="";const v=this.textContent.trim()||"Script";this.textContent=v,$("reh-script-title")&&($("reh-script-title").value=v)}}),(_Ya=$("reh-page-title"))==null||_Ya.addEventListener("keydown",function(e){var _a2;e.key==="Enter"&&(e.preventDefault(),this.blur()),e.key==="Escape"&&(this.textContent=((_a2=$("reh-script-title"))==null?void 0:_a2.value)||"Script",this.blur())});let _rehPlayCtx=null;const rehDecodedBuffers=new Map;let rehCurrentSource=null,rehWordHighlightRaf=null;function rehPlayCtx(){return _rehPlayCtx||(_rehPlayCtx=new(window.AudioContext||window.webkitAudioContext)),_rehPlayCtx.state==="suspended"&&_rehPlayCtx.resume().catch(()=>{}),_rehPlayCtx}const REH_DECODE_WINDOW=8;function _rehEvictDecoded(keepIdx){if(rehDecodedBuffers.size<=REH_DECODE_WINDOW*2+4)return;const lo=keepIdx-REH_DECODE_WINDOW,hi=keepIdx+REH_DECODE_WINDOW;for(const k of rehDecodedBuffers.keys())(khi)&&rehDecodedBuffers.delete(k)}async function preDecodeBlob(lineIdx,blob){if(!rehDecodedBuffers.has(lineIdx))try{const ab=await blob.arrayBuffer(),buf=await rehPlayCtx().decodeAudioData(ab);rehDecodedBuffers.set(lineIdx,buf),_rehEvictDecoded(lineIdx)}catch{}}function computeWordTimings(text,durationSec){const words=stripMarkdown(text).split(/\s+/).filter(Boolean);if(words.length<2)return[];const totalChars=words.reduce((s,w)=>s+w.length,0)||1;let t=0;return words.map(w=>{const start=t;return t+=w.length/totalChars*durationSec,{word:w,start,end:t}})}function stopAudioSource(){if(rehCurrentSource){try{rehCurrentSource.stop(0)}catch{}rehCurrentSource=null}rehWordHighlightRaf&&(cancelAnimationFrame(rehWordHighlightRaf),rehWordHighlightRaf=null)}async function playPreDecoded(lineIdx,blob,text){rehDecodedBuffers.has(lineIdx)||await preDecodeBlob(lineIdx,blob),_rehEvictDecoded(lineIdx);const buf=rehDecodedBuffers.get(lineIdx);if(!buf){await playAudioBlobFallback(blob);return}const timings=computeWordTimings(text,buf.duration),dialogEl=document.getElementById("reh-diag-"+lineIdx);return dialogEl&&timings.length>=2&&(dialogEl.innerHTML=timings.map((t,i)=>`${escHtml(t.word)}`).join(" ")),new Promise(resolve=>{stopAudioSource();const ctx=rehPlayCtx(),src=ctx.createBufferSource();src.buffer=buf,src.connect(ctx.destination),rehCurrentSource=src;const t0=ctx.currentTime;src.onended=()=>{rehCurrentSource=null,rehWordHighlightRaf&&(cancelAnimationFrame(rehWordHighlightRaf),rehWordHighlightRaf=null),dialogEl&&timings.length>=2&&(dialogEl.innerHTML=renderMarkdownInline(text)),resolve()},src.start(0);const nextI=findNextCachedLine(lineIdx+1);if(nextI>=0&&preDecodeBlob(nextI,rehState.synthCache.get(nextI)),dialogEl&&timings.length>=2){const tick=()=>{if(rehCurrentSource!==src)return;const elapsed=ctx.currentTime-t0;let active=0;for(let i=timings.length-1;i>=0;i--)if(elapsed>=timings[i].start){active=i;break}dialogEl.querySelectorAll(".reh-word").forEach((span,i)=>{span.classList.toggle("reh-word-active",i===active)}),rehWordHighlightRaf=requestAnimationFrame(tick)};rehWordHighlightRaf=requestAnimationFrame(tick)}})}function findNextCachedLine(fromIdx){for(let i=fromIdx;i{audio.addEventListener("canplay",r,{once:!0}),setTimeout(r,3e3)}),await audio.play().catch(()=>{}),await waitForAudioEnd(audio))}function startPlay(){rehPlayCtx(),_ensureNarrator(),rehState.playing=!0,updatePlayBtn(),playNextLine()}function _rehResetActionPlayIcons(){document.querySelectorAll(".reh-action-play-btn").forEach(btn=>{const icon=btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-play"),btn.title="Play or pause this line"})}function pausePlay(){rehState.playing=!1,updatePlayBtn(),stopAudioSource();const audio=$("reh-tts-audio");audio&&!audio.paused&&audio.pause(),hideStatusBar(),_rehResetActionPlayIcons()}function stopPlay(){rehState.playing=!1,updatePlayBtn(),stopAudioSource();const audio=$("reh-tts-audio");audio&&(audio.pause(),audio.src=""),hideStatusBar(),_rehResetActionPlayIcons()}function hideStatusBar(){const bar=$("reh-tts-status-bar");bar&&(bar.hidden=!0)}let _rehPrefetchRunning=!1;async function _rehPrefetchLines(fromIndex){if(!_rehPrefetchRunning){_rehPrefetchRunning=!0;try{let prefetched=0;for(let i=fromIndex;irehState.practiceEnd){rehState.playing=!1,updatePlayBtn(),rehState.repeat?(rehState.lineIndex=(_a2=rehState.practiceStart)!=null?_a2:0,startPlay()):(rehState.lineIndex=(_b2=rehState.practiceStart)!=null?_b2:0,highlightCurrentLine());return}if(((_c2=rehState.lines[rehState.lineIndex])==null?void 0:_c2.type)==="pagebreak")return rehState.lineIndex++,playNextLine();const _cur=rehState.lines[rehState.lineIndex];if(_cur&&(_cur.ignored||_cur.hidden))return rehState.lineIndex++,playNextLine();if(rehState.skipDescriptions)for(;rehState.lineIndexrehState.practiceEnd);)rehState.lineIndex++;if(rehState.lineIndex>=rehState.lines.length){if(rehState.playing=!1,updatePlayBtn(),rehState.repeat){rehState.lineIndex=0,startPlay();return}toast("Script finished","success");return}const myLineIndex=rehState.lineIndex,stillCurrent=()=>rehState.playing&&rehState.lineIndex===myLineIndex,line=rehState.lines[rehState.lineIndex];if(highlightCurrentLine(),_rehPrefetchLines(myLineIndex+1).catch(e=>console.warn("Prefetch error:",e)),line.type!=="dialog"){const hasText=!!(line.text||"").trim();if(rehState.narratorVoice&&hasText&&!rehState.skipDescriptions){showStatusBar("Narrator: "+line.text.slice(0,50)+(line.text.length>50?"\u2026":""));const cached2=rehState.synthCache.get(rehState.lineIndex);if(cached2)await playPreDecoded(rehState.lineIndex,cached2,line.text);else try{const book=_lineAudioBookName(),cleanNarr=stripMarkdown(line.text),cacheKey=await _lineAudioCacheKey(cleanNarr,rehState.narratorVoice,"");if(!stillCurrent())return;let blob=await _lineAudioCacheGet(book,cacheKey);if(!stillCurrent())return;if(!blob){if(blob=await fetchTtsPreviewBlob(rehState.narratorVoice,cleanNarr,"wav","",_ttsBackendForVoice(rehState.narratorVoice,rehState.backend)),!stillCurrent())return;_lineAudioCachePut(book,cacheKey,blob)}rehState.synthCache.set(myLineIndex,blob),_markSynthDot(myLineIndex,"ok"),await playPreDecoded(myLineIndex,blob,line.text)}catch{await new Promise(r=>setTimeout(r,200))}}else rehState.skipDescriptions||await new Promise(r=>setTimeout(r,line.type==="direction"?150:250));if(!stillCurrent())return;rehState.lineIndex++,playNextLine();return}const cast=rehState.cast[line.speaker]||{voice:""};if(cast.voice==="me"){rehState.playing=!1,updatePlayBtn(),showRecOverlay(line);return}if(!cast.voice){if(showStatusBar(line.speaker+" has no voice \u2014 skipping\u2026"),await new Promise(r=>setTimeout(r,350)),!stillCurrent())return;rehState.lineIndex++,playNextLine();return}const profile=(cast.instruct||"").trim(),instruct=_buildInstruct(profile,line.emotion,cast.voice),cleanTxt=stripMarkdown(line.text),cached=rehState.synthCache.get(rehState.lineIndex);if(cached)showStatusBar(line.speaker+" is speaking\u2026"),await playPreDecoded(rehState.lineIndex,cached,line.text),rehState.clips.push({lineIndex:rehState.lineIndex,speaker:line.speaker,type:"tts",blob:cached});else{showStatusBar("Synthesizing\u2026");try{const toneText=_rehInlineTone(cleanTxt,line.emotion),book=_lineAudioBookName(),cacheKey=await _lineAudioCacheKey(toneText,cast.voice,instruct);if(!stillCurrent())return;let blob=await _lineAudioCacheGet(book,cacheKey);if(!stillCurrent())return;if(!blob){if(blob=await fetchTtsPreviewBlob(cast.voice,toneText,"wav",instruct,_ttsBackendForVoice(cast.voice,rehState.backend)),!stillCurrent())return;_lineAudioCachePut(book,cacheKey,blob)}rehState.synthCache.set(myLineIndex,blob),showStatusBar(line.speaker+" is speaking\u2026"),await playPreDecoded(myLineIndex,blob,line.text),rehState.clips.push({lineIndex:myLineIndex,speaker:line.speaker,type:"tts",blob})}catch(e){showStatusBar("TTS failed: "+e.message),await new Promise(r=>setTimeout(r,1e3))}}stillCurrent()&&(rehState.lineIndex++,playNextLine())}function waitForAudioEnd(audio){return new Promise(resolve=>{if(!audio||audio.paused||audio.ended){resolve();return}audio.addEventListener("ended",resolve,{once:!0}),audio.addEventListener("pause",resolve,{once:!0}),audio.addEventListener("error",resolve,{once:!0})})}function showStatusBar(msg){const bar=$("reh-tts-status-bar");if(!bar)return;bar.hidden=!1;const txt=$("reh-tts-status-txt");txt&&(txt.textContent=msg)}function highlightCurrentLine(){const i=rehState.lineIndex,total=rehState.lines.length,prog=$("reh-tb-progress");prog&&(prog.style.width=(total?i/total*100:0)+"%");const lbl=$("reh-tb-label");lbl&&(lbl.textContent=`${i+1} / ${total}`),document.querySelectorAll("[data-index]").forEach(el=>{el.classList.toggle("reh-line-active",parseInt(el.dataset.index)===i)}),document.querySelectorAll(".reh-action-play-btn").forEach(btn=>{const icon=btn.querySelector(".mdi");if(!icon)return;const isActive=rehState.playing&&parseInt(btn.dataset.index)===i;icon.className=isActive?"mdi mdi-stop":"mdi mdi-play",btn.title=isActive?"Stop":"Play or pause this line"});const active=document.querySelector(`[data-index="${i}"]`);active&&active.scrollIntoView({behavior:"smooth",block:"center"}),updatePlayBtn()}function updatePlayBtn(){const btn=$("reh-tb-play");btn&&(btn.innerHTML=rehState.playing?'':'',btn.title=rehState.playing?"Pause":"Play all")}async function _lineAudioCacheKey(text,voice,instruct){const enc=new TextEncoder().encode(`${text}\0${voice}\0${instruct||""}`),digest=await crypto.subtle.digest("SHA-256",enc);return[...new Uint8Array(digest)].map(b=>b.toString(16).padStart(2,"0")).join("").slice(0,32)}function _lineAudioBookName(){var _a2;return((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled"}async function _lineAudioCacheGet(book,key){try{const r=await fetch(`/api/line-audio/${encodeURIComponent(book)}/${key}`);return r.ok?await r.blob():null}catch{return null}}function _lineAudioCachePut(book,key,blob){fetch(`/api/line-audio/${encodeURIComponent(book)}/${key}`,{method:"POST",body:blob}).catch(()=>{})}let _lineAudioSyncedFor=null;async function _lineAudioSyncDots(){const book=_lineAudioBookName();_ensureNarrator();const idxToKey=new Map;for(let i=0;i[k,i]));try{const r=await fetch(`/api/line-audio/${encodeURIComponent(book)}/check`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({keys:[...idxToKey.values()]})});if(!r.ok)return;const{existing}=await r.json();(existing||[]).forEach(key=>{const idx=keyToIdx.get(key);idx!=null&&(rehState.staleLines.delete(idx),_markSynthDot(idx,"ok"))})}catch{}}async function synthAll(){var _a2;if(rehState.synthRunning)return;if(!rehState.backend){toast("Select a TTS backend first","error");return}_ensureNarrator();const ttsLines=rehState.lines.map((l,i)=>({line:l,idx:i})).filter(({line})=>{if(line.ignored||line.hidden)return!1;if(line.type==="dialog"){const c=rehState.cast[line.speaker];return c&&c.voice&&c.voice!=="me"}return!!(rehState.narratorVoice&&(line.text||"").trim())});if(!ttsLines.length){toast("No TTS lines to synthesize","error");return}rehState.synthRunning=!0,rehState.synthCancelled=!1;const synthBar=$("reh-synth-bar"),fill=$("reh-synth-fill"),label=$("reh-synth-label");synthBar&&(synthBar.hidden=!1);const prog=(d,t)=>{fill&&(fill.style.width=(t?d/t*100:0)+"%"),label&&(label.textContent=`${d} / ${t} synthesized`)};prog(0,ttsLines.length);let done=0;const book=_lineAudioBookName();try{for(const{line,idx}of ttsLines){if(rehState.synthCancelled)break;let voice,instruct;if(line.type==="dialog"){const c=rehState.cast[line.speaker];if(!c||!c.voice||c.voice==="me"){_markSynthDot(idx,null),prog(++done,ttsLines.length);continue}voice=c.voice,instruct=_buildInstruct(c.instruct,line.emotion,c.voice)}else voice=rehState.narratorVoice,instruct="";_markSynthDot(idx,"synthesizing"),(_a2=document.getElementById("reh-syd-"+idx))==null||_a2.scrollIntoView({behavior:"smooth",block:"nearest"});const toneText=_rehInlineTone(stripMarkdown(line.text),line.emotion);try{const cacheKey=await _lineAudioCacheKey(toneText,voice,instruct);let blob=await _lineAudioCacheGet(book,cacheKey);blob||(blob=await fetchTtsPreviewBlob(voice,toneText,"wav",instruct,_ttsBackendForVoice(voice,rehState.backend)),_lineAudioCachePut(book,cacheKey,blob)),rehState.synthCache.set(idx,blob),rehState.staleLines.delete(idx),_markSynthDot(idx,"ok"),_showReSynthBtn(idx,!1),preDecodeBlob(idx,blob)}catch{_markSynthDot(idx,null)}prog(++done,ttsLines.length)}}finally{synthBar&&(synthBar.hidden=!0),rehState.synthRunning=!1}rehState.synthCancelled||toast(`Pre-synthesized ${done} of ${ttsLines.length} lines \u2014 ready for instant playback`,"success")}function _updateStaleBatchBtn(){const btn=$("reh-tb-resynth-stale");btn&&(btn.hidden=rehState.staleLines.size===0)}(_Za=$("reh-tb-synth-all"))==null||_Za.addEventListener("click",()=>synthAll()),(__a=$("reh-tb-resynth-stale"))==null||__a.addEventListener("click",async()=>{if(rehState.synthRunning)return;const stale=[...rehState.staleLines];if(!stale.length)return;rehState.synthRunning=!0,rehState.synthCancelled=!1;const synthBar=$("reh-synth-bar"),fill=$("reh-synth-fill"),label=$("reh-synth-label");synthBar&&(synthBar.hidden=!1);let done=0;const book=_lineAudioBookName();try{for(const idx of stale){if(rehState.synthCancelled)break;const line=rehState.lines[idx];if(!line||line.type!=="dialog"){rehState.staleLines.delete(idx);continue}const c=rehState.cast[line.speaker];if(!c||!c.voice||c.voice==="me"){rehState.staleLines.delete(idx);continue}const instruct=[c.instruct||"",line.emotion||""].filter(Boolean).join(". ");_markSynthDot(idx,"synthesizing");const toneText=_rehInlineTone(stripMarkdown(line.text),line.emotion);try{const cacheKey=await _lineAudioCacheKey(toneText,c.voice,instruct);let blob=await _lineAudioCacheGet(book,cacheKey);blob||(blob=await fetchTtsPreviewBlob(c.voice,toneText,"wav",instruct,_ttsBackendForVoice(c.voice,rehState.backend)),_lineAudioCachePut(book,cacheKey,blob)),rehState.synthCache.set(idx,blob),rehState.staleLines.delete(idx),preDecodeBlob(idx,blob),_markSynthDot(idx,"ok"),_showReSynthBtn(idx,!1)}catch{_markSynthDot(idx,"stale")}fill&&(fill.style.width=++done/stale.length*100+"%"),label&&(label.textContent=`${done} / ${stale.length} synthesized`)}}finally{synthBar&&(synthBar.hidden=!0),rehState.synthRunning=!1,_updateStaleBatchBtn()}rehState.synthCancelled||toast(`Re-synthesized ${done} stale line${done!==1?"s":""}`,"success")}),(_$a=$("reh-synth-cancel"))==null||_$a.addEventListener("click",()=>{rehState.synthCancelled=!0,rehState.synthRunning=!1}),(_ab=$("reh-tb-clean-cache"))==null||_ab.addEventListener("click",async()=>{const btn=$("reh-tb-clean-cache");btn&&(btn.disabled=!0,btn.innerHTML=' Scanning\u2026');try{_ensureNarrator();const keep=[];for(const line of rehState.lines){if(line.ignored||line.hidden)continue;let voice,instruct,text;if(line.type==="dialog"){const c=rehState.cast[line.speaker];if(!c||!c.voice||c.voice==="me")continue;voice=c.voice,instruct=_buildInstruct(c.instruct,line.emotion,c.voice),text=_rehInlineTone(stripMarkdown(line.text),line.emotion)}else{if(!rehState.narratorVoice||!(line.text||"").trim())continue;voice=rehState.narratorVoice,instruct="",text=stripMarkdown(line.text)}keep.push(await _lineAudioCacheKey(text,voice,instruct))}const book=_lineAudioBookName(),r=await fetch(`/api/line-audio/${encodeURIComponent(book)}/prune`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({keep})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();toast(d.deleted?`Cleaned up ${d.deleted} unused cached audio file${d.deleted!==1?"s":""}`:"Nothing to clean up \u2014 every cached file is still in use","success")}catch(e){toast("Cache cleanup failed: "+(e.message||e),"error")}finally{btn&&(btn.disabled=!1,btn.innerHTML=' Clean cache')}});function showRecOverlay(line){const overlay=$("reh-rec-overlay");if(!overlay)return;overlay.hidden=!1;const cue=$("reh-rec-cue"),c=rehState.cast[line.speaker]||{color:"#89b4fa"};cue&&(cue.innerHTML=`${escHtml(line.speaker)} \u2014 your line:
${escHtml(stripMarkdown(line.text))}
`),$("reh-rec-preview")&&($("reh-rec-preview").style.display="none",$("reh-rec-preview").src=""),$("reh-rec-confirm-row")&&($("reh-rec-confirm-row").hidden=!0),$("reh-rec-start")&&($("reh-rec-start").disabled=!1),$("reh-rec-stop")&&($("reh-rec-stop").disabled=!0),$("reh-rec-time")&&($("reh-rec-time").textContent="0:00"),rehState.lastRecBlob=null}function hideRecOverlay(){const overlay=$("reh-rec-overlay");overlay&&(overlay.hidden=!0),stopRehMic()}function rehRenderMeter(level=0,db=-1/0,clipped=!1){const meter=$("reh-mic-meter");if(!meter)return;if(!meter.children.length)for(let i=0;i<18;i++){const b=document.createElement("div");b.className="bar",meter.appendChild(b)}const active=Math.round(Math.max(0,Math.min(1,level))*meter.children.length);[...meter.children].forEach((bar,i)=>{bar.className="bar",bar.style.height=7+Math.min(i,active)*1.55+"px",i-12&&i>11&&bar.classList.add("hot"),clipped&&i>14&&bar.classList.add("clip"))});const el=$("reh-db-readout");el&&(el.textContent=Number.isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB")}function rehStartMeter(){if(!rehState.recAnalyser)return;rehState.recMeterRaf&&cancelAnimationFrame(rehState.recMeterRaf);const data=new Float32Array(rehState.recAnalyser.fftSize),canvas=$("reh-live-wave"),RING=300,ADD=10;rehState.recWaveRing=new Float32Array(RING);const tick=()=>{rehState.recAnalyser.getFloatTimeDomainData(data);let sum=0,peak=0;for(const s of data)sum+=s*s,peak=Math.max(peak,Math.abs(s));const rms=Math.sqrt(sum/data.length),db=rms>0?20*Math.log10(rms):-1/0;if(rehRenderMeter((db+60)/60,db,peak>.98),canvas&&rehState.recWaveRing){const ring=rehState.recWaveRing;ring.copyWithin(0,ADD);for(let i=0;i.98?"#f38ba8":db>-12?"#f9e2af":"#a6e3a1",ctx.lineWidth=1.5;const mid=h/2;for(let i=0;i{try{n&&n.disconnect()}catch{}}),rehState.recStream&&rehState.recStream.getTracks().forEach(t=>t.stop()),rehState.recDestStream&&rehState.recDestStream.getTracks().forEach(t=>t.stop()),rehState.recAudioCtx&&rehState.recAudioCtx.close().catch(()=>{}),Object.assign(rehState,{recStream:null,recDestStream:null,recSourceNode:null,recGainNode:null,recAnalyser:null,recAudioCtx:null,recWaveRing:null}),rehRenderMeter();const wc=$("reh-live-wave");wc&&wc.getContext("2d").clearRect(0,0,wc.width,wc.height)}(_bb=$("reh-rec-start"))==null||_bb.addEventListener("click",async()=>{try{await startRehMic(),rehState.recChunks=[],rehState.recSecs=0,$("reh-rec-time")&&($("reh-rec-time").textContent="0:00"),$("reh-rec-start")&&($("reh-rec-start").disabled=!0),$("reh-rec-stop")&&($("reh-rec-stop").disabled=!1),$("reh-rec-confirm-row")&&($("reh-rec-confirm-row").hidden=!0),rehState.recTimer=setInterval(()=>{rehState.recSecs++,$("reh-rec-time")&&($("reh-rec-time").textContent=Math.floor(rehState.recSecs/60)+":"+String(rehState.recSecs%60).padStart(2,"0"))},1e3),rehState.mediaRec=new MediaRecorder(rehState.recDestStream||rehState.recStream,{audioBitsPerSecond:256e3}),rehState.mediaRec.ondataavailable=e=>{e.data.size&&rehState.recChunks.push(e.data)},rehState.mediaRec.onstop=()=>{clearInterval(rehState.recTimer),$("reh-rec-start")&&($("reh-rec-start").disabled=!1),$("reh-rec-stop")&&($("reh-rec-stop").disabled=!0);const blob=new Blob(rehState.recChunks,{type:rehState.mediaRec.mimeType||"audio/webm"}),url=URL.createObjectURL(blob),p=$("reh-rec-preview");p&&(p.src=url,p.style.display=""),$("reh-rec-confirm-row")&&($("reh-rec-confirm-row").hidden=!1),rehState.lastRecBlob=blob},rehState.mediaRec.start(100)}catch(e){stopRehMic(),toast(await microphoneErrorMessage(e),"error")}}),(_cb=$("reh-rec-stop"))==null||_cb.addEventListener("click",()=>{var _a2;((_a2=rehState.mediaRec)==null?void 0:_a2.state)!=="inactive"&&rehState.mediaRec.stop()}),(_db=$("reh-rec-keep"))==null||_db.addEventListener("click",()=>{var _a2;rehState.lastRecBlob&&rehState.clips.push({lineIndex:rehState.lineIndex,speaker:(_a2=rehState.lines[rehState.lineIndex])==null?void 0:_a2.speaker,type:"me",blob:rehState.lastRecBlob}),stopRehMic(),hideRecOverlay(),rehState.lineIndex++,startPlay()}),(_eb=$("reh-rec-redo"))==null||_eb.addEventListener("click",()=>{stopRehMic();const l=rehState.lines[rehState.lineIndex];l&&showRecOverlay(l)}),(_fb=$("reh-skip-line"))==null||_fb.addEventListener("click",()=>{var _a2;rehState.clips.push({lineIndex:rehState.lineIndex,speaker:(_a2=rehState.lines[rehState.lineIndex])==null?void 0:_a2.speaker,type:"skip"}),stopRehMic(),hideRecOverlay(),rehState.lineIndex++,startPlay()});function renderSummary(){const list=$("reh-summary-list");if(list){if(!rehState.clips.length){list.innerHTML='

No clips in this session.

';return}list.innerHTML=rehState.clips.map((clip,idx)=>{const line=rehState.lines[clip.lineIndex]||{text:"\u2014",speaker:clip.speaker},c=rehState.cast[clip.speaker]||{color:"#89b4fa"},typeLabel=clip.type==="me"?"\u{1F3A4} Recorded":clip.type==="tts"?"\u{1F50A} Synthesized":"\u23ED Skipped",audioHtml=clip.blob?``:"",dlHtml=clip.blob?``:"";return`
- -
-
${escHtml(clip.speaker||"\u2014")} ${typeLabel}
-
${escHtml(stripMarkdown(line.text))}
- ${audioHtml} -
- ${dlHtml} -
`}).join("")}}(_gb=$("reh-new-session-btn"))==null||_gb.addEventListener("click",()=>{stopPlay(),stopRehMic(),rehState.lines=[],rehState.cast={},rehState.clips=[],rehState.lineIndex=0,rehState.savedId=null,rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.narratorVoice="",rehState.practiceStart=null,rehState.practiceEnd=null,$("reh-script-text")&&($("reh-script-text").value=""),$("reh-script-title")&&($("reh-script-title").value=""),renderLibraryList(),showPhase(1)}),(_hb=$("reh-resume-btn"))==null||_hb.addEventListener("click",()=>{showPhase(3),highlightCurrentLine()}),(_ib=$("reh-save-session-btn"))==null||_ib.addEventListener("click",saveToLibrary),(_jb=$("reh-cast-save-btn"))==null||_jb.addEventListener("click",saveToLibrary),(_kb=$("reh-export-session-btn"))==null||_kb.addEventListener("click",exportToFile),(_lb=$("reh-tb-save"))==null||_lb.addEventListener("click",saveToLibrary),(_mb=$("reh-tb-export"))==null||_mb.addEventListener("click",exportToFile),(_nb=$("reh-lib-import-file"))==null||_nb.addEventListener("change",async function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];f&&(await importFromFile(f),this.value="")}),document.querySelectorAll(".reh-impex-file").forEach(inp=>{inp.addEventListener("change",async function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];f&&(await importFromFile(f),this.value="",navRehearserPhase(1))})}),(_ob=$("reh-impex-reh-btn"))==null||_ob.addEventListener("click",exportToFile),(_pb=$("reh-impex-fountain-btn"))==null||_pb.addEventListener("click",()=>{var _a2;(_a2=$("reh-fountain-btn"))==null||_a2.click()}),(_qb=$("reh-impex-fdx-btn"))==null||_qb.addEventListener("click",()=>{var _a2;(_a2=$("reh-fdx-export-btn"))==null||_a2.click()}),(_rb=$("reh-impex-osf-btn"))==null||_rb.addEventListener("click",()=>{var _a2;(_a2=$("reh-osf-export-btn"))==null||_a2.click()});function _downloadText(content,filename){const a=document.createElement("a");a.href=URL.createObjectURL(new Blob([content],{type:"text/plain;charset=utf-8"})),a.download=filename,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),3e3)}function exportAsTxt(){var _a2,_b2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||((_b2=$("reh-page-title"))==null?void 0:_b2.textContent)||"script";_downloadText(linesToScriptText(),title.replace(/[^a-z0-9]/gi,"_")+".txt")}function exportAsMd(){var _a2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Script",lines=rehState.lines.map(l=>{switch(l.type){case"act":return` -# ${l.text} -`;case"scene":return` -## ${l.text} -`;case"transition":return` -*${l.text}* -`;case"direction":return`*(${l.text})*`;case"action":return` -${l.text} -`;case"dialog":return` -**${l.speaker}** -${l.text} -`;default:return""}}).join(` -`);_downloadText(`# ${title} - -${lines.trim()} -`,title.replace(/[^a-z0-9]/gi,"_")+".md")}(_sb=$("reh-impex-txt-btn"))==null||_sb.addEventListener("click",exportAsTxt),(_tb=$("reh-impex-md-btn"))==null||_tb.addEventListener("click",exportAsMd),function(){const inp=$("reh-impex-url"),btn=$("reh-impex-url-btn"),status2=$("reh-impex-url-status");if(!inp||!btn)return;async function fetchUrl(){const url=inp.value.trim();if(!url){toast("Paste a URL first","error");return}btn.disabled=!0,status2&&(status2.textContent="Fetching\u2026",status2.style.color="");try{const r=await fetch("/api/fetch-web-script",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json(),ta=$("reh-script-text");ta&&(ta.value=d.text);const titleInp=$("reh-script-title");titleInp&&d.title&&(titleInp.value=d.title.replace(/-/g," ").replace(/\b\w/g,c=>c.toUpperCase())),status2&&(status2.textContent=`\u2713 Fetched ${(d.chars/1e3).toFixed(0)} K chars \u2014 scroll down to Parse & cast`,status2.style.color="var(--green)"),navRehearserPhase(1),setTimeout(()=>ta==null?void 0:ta.scrollIntoView({behavior:"smooth",block:"nearest"}),300),toast(`Fetched "${d.title}" \u2014 click "Parse & cast" to continue`,"success")}catch(e){status2&&(status2.textContent="\u2717 "+e.message,status2.style.color="var(--red)"),toast("Fetch failed: "+e.message,"error")}finally{btn.disabled=!1}}btn.addEventListener("click",fetchUrl),inp.addEventListener("keydown",e=>{e.key==="Enter"&&fetchUrl()})}(),function(){const modal=$("reh-imsdb-modal"),grid=$("reh-imsdb-grid"),search=$("reh-imsdb-search"),countEl=$("reh-imsdb-count"),closeBtn=$("reh-imsdb-close"),coverBtn=$("reh-imsdb-cover-btn"),listBtn=$("reh-imsdb-list-btn"),urlInp=$("reh-imsdb-url"),urlBtn=$("reh-imsdb-url-fetch");if(!modal||!grid)return;const CACHE_KEY="reh-imsdb-cat-v1",CACHE_TTL=6*3600*1e3;let _catalogue=null,_posterObserver=null,_view=localStorage.getItem("reh-imsdb-view")||"list";function _applyView(){grid.classList.toggle("list-view",_view==="list"),coverBtn&&coverBtn.classList.toggle("active",_view==="cover"),listBtn&&listBtn.classList.toggle("active",_view==="list")}function _ensurePosterObserver(){return _posterObserver||(_posterObserver=new IntersectionObserver(entries=>{entries.forEach(async ent=>{if(!ent.isIntersecting)return;const card=ent.target;_posterObserver.unobserve(card);const title=card.dataset.title;try{const d=await fetch("/api/movie-poster?title="+encodeURIComponent(title)).then(r=>r.json());if(d.poster){const img=document.createElement("img");img.className="reh-imsdb-poster",img.loading="lazy",img.src=d.poster,img.alt=title,img.onload=()=>{var _a2;const wrap=card.querySelector(".reh-imsdb-poster-wrap");wrap&&((_a2=wrap.querySelector(".reh-imsdb-fallback"))==null||_a2.remove(),wrap.appendChild(img))}}d.year&&card.querySelectorAll(".reh-imsdb-year").forEach(y=>{y.textContent=d.year})}catch{}})},{root:grid,rootMargin:"300px"}),_posterObserver)}function _bookColor(title){let h=0;for(let i=0;i>>0;return`hsl(${h%360}, 45%, 42%)`}function renderGrid(items){const obs=_ensurePosterObserver();if(grid.innerHTML="",!items.length){grid.innerHTML='
No matches.
';return}const frag=document.createDocumentFragment();items.slice(0,400).forEach(it=>{const card=document.createElement("div");card.className="reh-imsdb-card",card.dataset.title=it.title,card.dataset.url=it.fetch_url;const color1=_bookColor(it.title),color2=_bookColor(it.title+"_");card.innerHTML=` -
-
- - ${escHtml(it.title)} -
-
-
- ${escHtml(it.title)} - -
`,card.addEventListener("click",()=>importImsdb(it)),frag.appendChild(card),obs.observe(card)}),grid.appendChild(frag),countEl&&(countEl.textContent=`${items.length} script${items.length!==1?"s":""}${items.length>400?" (showing 400)":""}`),_applyView()}async function importAnyWebUrl(url,label="script"){if(url=String(url||"").trim(),!url){toast("Paste a script URL first","error");return}if(!/^https?:\/\//i.test(url)){toast("URL must start with http:// or https://","error");return}urlBtn&&(urlBtn.disabled=!0),grid.innerHTML=`
Fetching ${escHtml(label)}\u2026
`;try{const r=await fetch("/api/fetch-web-script",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json(),ta=$("reh-script-text");ta&&(ta.value=d.text);const ti=$("reh-script-title");ti&&(ti.value=d.title||label),closeModal(),navRehearserPhase(1),setTimeout(()=>ta==null?void 0:ta.scrollIntoView({behavior:"smooth",block:"center"}),250),toast(`Loaded "${d.title||label}" (${(d.chars/1e3).toFixed(0)} K) \u2014 click "Parse & cast"`,"success")}catch(e){grid.innerHTML=`
Fetch failed: ${escHtml(e.message)}
`,_catalogue&&renderGrid(_searchFilter(_catalogue)),toast("Script fetch failed: "+e.message,"error")}finally{urlBtn&&(urlBtn.disabled=!1)}}async function importImsdb(it){await importAnyWebUrl(it.fetch_url,it.title||"IMSDb script")}async function _loadCatalogue(){try{const raw=localStorage.getItem(CACHE_KEY);if(raw){const cached=JSON.parse(raw);if(Date.now()-cached.tsr.json())).items||[];try{localStorage.setItem(CACHE_KEY,JSON.stringify({ts:Date.now(),items:_catalogue}))}catch{}}async function openModal(){if(modal.hidden=!1,urlInp&&(urlInp.value=""),_applyView(),!_catalogue){grid.innerHTML='
Loading catalogue\u2026
';try{await _loadCatalogue()}catch(e){grid.innerHTML=`
Failed to load: ${escHtml(e.message)}
`;return}}renderGrid(_searchFilter(_catalogue)),search==null||search.focus()}function closeModal(){modal.hidden=!0}function _searchFilter(items){const q=(search==null?void 0:search.value.trim().toLowerCase())||"";return q?items.filter(it=>it.title.toLowerCase().includes(q)):items}let _searchTimer=null;search==null||search.addEventListener("input",()=>{clearTimeout(_searchTimer),_searchTimer=setTimeout(()=>{_catalogue&&renderGrid(_searchFilter(_catalogue))},150)}),search==null||search.addEventListener("keydown",e=>{e.key==="Enter"&&/^https?:\/\//i.test(search.value.trim())&&(e.preventDefault(),importAnyWebUrl(search.value.trim(),"pasted URL"))}),urlBtn==null||urlBtn.addEventListener("click",()=>importAnyWebUrl((urlInp==null?void 0:urlInp.value)||"","pasted URL")),urlInp==null||urlInp.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),importAnyWebUrl(urlInp.value,"pasted URL"))}),coverBtn==null||coverBtn.addEventListener("click",()=>{_view="cover",localStorage.setItem("reh-imsdb-view",_view),_applyView()}),listBtn==null||listBtn.addEventListener("click",()=>{_view="list",localStorage.setItem("reh-imsdb-view",_view),_applyView()}),[$("reh-browse-imsdb-btn"),$("reh-browse-imsdb-btn-impex")].forEach(btn=>{btn==null||btn.addEventListener("click",openModal)}),closeBtn==null||closeBtn.addEventListener("click",closeModal),modal.addEventListener("click",e=>{e.target===modal&&closeModal()})}();const trainState={seq:[],turn:0,phase:"idle",cueSource:null,mediaRec:null,recChunks:[],recRaf:null,recStream:null,recCtx:null,recAnalyser:null,recTimer:null,recSecs:0};function _trainNormWords(str){return(str||"").toLowerCase().replace(/[^a-zäöüàáâãèéêëìíîïòóôõùúûüýÿæœßа-яёА-ЯЁ0-9\s]/g,"").trim().split(/\s+/).filter(Boolean)}function _trainLcs(a,b){const R=a.length,C=b.length,dp=Array.from({length:R+1},()=>new Int16Array(C+1));for(let i2=1;i2<=R;i2++)for(let j2=1;j2<=C;j2++)dp[i2][j2]=a[i2-1]===b[j2-1]?dp[i2-1][j2-1]+1:Math.max(dp[i2-1][j2],dp[i2][j2-1]);let i=R,j=C;const seq=[];for(;i>0&&j>0;)a[i-1]===b[j-1]?(seq.unshift([i-1,j-1]),i--,j--):dp[i-1][j]>=dp[i][j-1]?i--:j--;return seq}function _trainCompare(expected,actual){const ew=_trainNormWords(expected),aw=_trainNormWords(actual);if(!ew.length)return{html_exp:"",html_act:"",score:1};const pairs=_trainLcs(ew,aw),matchedE=new Set(pairs.map(p=>p[0])),matchedA=new Set(pairs.map(p=>p[1])),expHtml=ew.map((w,i)=>matchedE.has(i)?`${escHtml(w)}`:`${escHtml(w)}`).join(" "),actHtml=aw.map((w,i)=>matchedA.has(i)?`${escHtml(w)}`:`${escHtml(w)}`).join(" "),score=ew.length?Math.round(matchedE.size/ew.length*100):100;return{html_exp:expHtml,html_act:actHtml,score}}function buildTrainSeq(){const seq=[],lines=rehState.lines;for(let i=0;i=0&&cueLines.length<3;j--){const prev=lines[j];if(prev.type==="scene"||prev.type==="act")break;if(prev.type!=="dialog")continue;const pc=rehState.cast[prev.speaker];if(!pc||pc.voice==="me")break;cueLines.unshift({index:j,speaker:prev.speaker,text:prev.text,emotion:prev.emotion||"",voice:pc.voice,color:pc.color,instruct:_buildInstruct(pc.instruct,prev.emotion,pc.voice),voiceData:pc.voiceData})}seq.push({cueLines,myLine:{index:i,speaker:line.speaker,text:line.text,color:cast.color}})}return seq}function _trainSetState(phase){trainState.phase=phase;const stateEl=$("reh-train-rec-state"),playBtn=$("reh-train-play"),recBtn=$("reh-train-record"),stopBtn=$("reh-train-stop-rec"),cueState=$("reh-train-cue-state"),map={idle:{state:"Read the cue above \xB7 press \u25B6 to hear it",play:!0,rec:!1,stop:!1},playing_cue:{state:"Playing cue\u2026",play:!1,rec:!1,stop:!1},ready:{state:"Press \u{1F3A4} to record your line",play:!1,rec:!0,stop:!1},recording:{state:"Recording\u2026",play:!1,rec:!1,stop:!0},transcribing:{state:"Transcribing\u2026",play:!1,rec:!1,stop:!1},done:{state:"Done \u2014 press \u25B6 to replay or \u23ED for next",play:!0,rec:!0,stop:!1}},m=map[phase]||map.idle;stateEl&&(stateEl.textContent=m.state),cueState&&phase==="playing_cue"?cueState.innerHTML='':cueState&&(cueState.innerHTML=""),playBtn&&(playBtn.disabled=!m.play),recBtn&&(recBtn.disabled=!m.rec,recBtn.hidden=!!m.stop),stopBtn&&(stopBtn.disabled=!m.stop,stopBtn.hidden=!m.stop);const timerEl=$("reh-train-rec-timer"),metersEl=$("reh-train-meters");phase==="recording"?(timerEl&&(timerEl.hidden=!1),metersEl&&(metersEl.hidden=!1)):phase!=="done"&&(timerEl&&(timerEl.hidden=!0),metersEl&&(metersEl.hidden=!0))}async function _trainPlayCueLines(cueLines){_trainSetState("playing_cue");for(const cue of cueLines){if(trainState.phase!=="playing_cue")break;let blob=rehState.synthCache.get(cue.index);if(!blob)try{blob=await fetchTtsPreviewBlob(cue.voice,_rehInlineTone(stripMarkdown(cue.text),cue.emotion),"wav",cue.instruct,_ttsBackendForVoice(cue.voice,rehState.backend))}catch(e){toast("Cue TTS failed: "+e.message,"error");break}if(trainState.phase!=="playing_cue")break;await new Promise(resolve=>{const url=URL.createObjectURL(blob),audio=new Audio(url);audio.onended=()=>{URL.revokeObjectURL(url),resolve()},audio.onerror=()=>{URL.revokeObjectURL(url),resolve()},trainState.cueAudio=audio,audio.play().catch(resolve)})}trainState.phase==="playing_cue"&&_trainSetState("ready")}async function _trainTranscribe(blob){_trainSetState("transcribing");try{const fd=new FormData;fd.append("file",blob,"train_rec.webm"),fd.append("backend",(_appSettings==null?void 0:_appSettings.stt_preferred_backend)||"configured");const r=await fetch("/api/transcribe-bytes",{method:"POST",body:fd});if(!r.ok)throw new Error(r.statusText);return((await r.json()).text||"").trim()}catch(e){return toast("Transcription failed: "+e.message,"error"),""}}async function trainGoTo(idx){trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),_trainStopMic(),trainState.turn=Math.max(0,Math.min(idx,trainState.seq.length-1));const turn=trainState.seq[trainState.turn];if(!turn)return;const progEl=$("reh-train-progress");progEl&&(progEl.textContent=`Turn ${trainState.turn+1} / ${trainState.seq.length}`);const cueCard=$("reh-train-cue"),cueAvEl=$("reh-train-cue-avatar"),cueSpeaker=$("reh-train-cue-speaker"),cueText=$("reh-train-cue-text");if(turn.cueLines.length){const lastCue=turn.cueLines[turn.cueLines.length-1];cueAvEl&&(cueAvEl.innerHTML=voiceAvatarHtml(lastCue.voice,lastCue.color,32)),cueSpeaker&&(cueSpeaker.textContent=lastCue.speaker),cueText&&(cueText.innerHTML=turn.cueLines.map(c=>`
- ${escHtml(c.speaker)} - ${escHtml(c.text)} -
`).join("")),cueCard==null||cueCard.classList.remove("no-cue")}else cueAvEl&&(cueAvEl.innerHTML=''),cueSpeaker&&(cueSpeaker.textContent="No preceding cue"),cueText&&(cueText.textContent="This is the first line \u2014 speak when ready."),cueCard==null||cueCard.classList.add("no-cue");const mySpeaker=$("reh-train-my-speaker"),myText=$("reh-train-my-text");mySpeaker&&(mySpeaker.textContent=turn.myLine.speaker),myText&&(myText.textContent=turn.myLine.text);const resultEl=$("reh-train-result");resultEl&&(resultEl.hidden=!0);const prevAudio=$("reh-train-audio-preview");prevAudio&&(prevAudio.src="",prevAudio.style.display="none"),_trainSetState(turn.cueLines.length?"idle":"ready")}function _trainStartMeter(){const meterEl=$("reh-train-meter"),dbEl=$("reh-train-db"),waveEl=$("reh-train-wave");if(!trainState.recAnalyser||!meterEl)return;const analyser=trainState.recAnalyser,fft=new Uint8Array(analyser.frequencyBinCount),wCtx=waveEl==null?void 0:waveEl.getContext("2d"),W=(waveEl==null?void 0:waveEl.width)||300,H=(waveEl==null?void 0:waveEl.height)||36,BARS=18;function tick(){analyser.getByteFrequencyData(fft);const rms=fft.reduce((s,v)=>s+v*v,0)/fft.length,db=rms>0?20*Math.log10(Math.sqrt(rms)/128):-1/0;dbEl&&(dbEl.textContent=isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB");const slots=Array.from({length:BARS},(_,i)=>{const s=Math.floor(i/BARS*fft.length),e=Math.floor((i+1)/BARS*fft.length);return fft.slice(s,e).reduce((a,b)=>a+b,0)/(e-s)/255});meterEl.innerHTML=slots.map(v=>{const h=Math.max(2,Math.round(v*28)),col=v>.85?"var(--red)":v>.6?"#f59e0b":"var(--green)";return``}).join(""),wCtx&&(analyser.getByteTimeDomainData(fft),wCtx.clearRect(0,0,W,H),wCtx.beginPath(),wCtx.strokeStyle="var(--accent)",wCtx.lineWidth=1.5,fft.forEach((v,i)=>{const x=i/fft.length*W,y=v/255*H;i?wCtx.lineTo(x,y):wCtx.moveTo(x,y)}),wCtx.stroke()),trainState.recRaf=requestAnimationFrame(tick)}trainState.recRaf=requestAnimationFrame(tick)}function _trainStopMic(){if(trainState.recRaf&&(cancelAnimationFrame(trainState.recRaf),trainState.recRaf=null),trainState.recTimer&&(clearInterval(trainState.recTimer),trainState.recTimer=null),trainState.mediaRec&&trainState.mediaRec.state!=="inactive")try{trainState.mediaRec.stop()}catch{}if(trainState.mediaRec=null,trainState.recCtx){try{trainState.recCtx.close()}catch{}trainState.recCtx=null}trainState.recStream&&(trainState.recStream.getTracks().forEach(t=>t.stop()),trainState.recStream=null),trainState.recAnalyser=null,trainState.recChunks=[];const timerEl=$("reh-train-rec-timer");timerEl&&(timerEl.hidden=!0,timerEl.textContent="0:00");const metersEl=$("reh-train-meters");metersEl&&(metersEl.hidden=!0)}async function _trainStartRecording(){if(trainState.phase==="ready")try{const stream=await requestMicrophoneStream({raw:!0});trainState.recStream=stream;const ctx=new AudioContext;trainState.recCtx=ctx;const src=ctx.createMediaStreamSource(stream),analyser=ctx.createAnalyser();analyser.fftSize=256,trainState.recAnalyser=analyser;const dst=ctx.createMediaStreamDestination();src.connect(analyser),analyser.connect(dst),_trainSetState("recording"),_trainStartMeter();const timerEl=$("reh-train-rec-timer");timerEl&&(timerEl.hidden=!1),trainState.recSecs=0,trainState.recTimer=setInterval(()=>{trainState.recSecs++;const m=Math.floor(trainState.recSecs/60),s=trainState.recSecs%60;timerEl&&(timerEl.textContent=`${m}:${String(s).padStart(2,"0")}`)},1e3),trainState.recChunks=[];const mr=new MediaRecorder(dst.stream,{audioBitsPerSecond:128e3});trainState.mediaRec=mr,mr.ondataavailable=e=>{e.data.size>0&&trainState.recChunks.push(e.data)},mr.onstop=async()=>{var _a2;_trainStopMic();const blob=new Blob(trainState.recChunks,{type:"audio/webm"});trainState.recChunks=[];const url=URL.createObjectURL(blob),prevAudio=$("reh-train-audio-preview");prevAudio&&(prevAudio.src=url,prevAudio.style.display="");const transcript=await _trainTranscribe(blob),turn=trainState.seq[trainState.turn],{html_exp,html_act,score}=_trainCompare(((_a2=turn==null?void 0:turn.myLine)==null?void 0:_a2.text)||"",transcript),resultEl=$("reh-train-result"),expEl=$("reh-train-expected-text"),actEl=$("reh-train-actual-text"),scoreEl=$("reh-train-score");if(expEl&&(expEl.innerHTML=html_exp),actEl&&(actEl.innerHTML=html_act||'Nothing detected'),scoreEl){const col=score>=90?"var(--green)":score>=60?"#f59e0b":"var(--red)";scoreEl.innerHTML=`${score}%`}resultEl&&(resultEl.hidden=!1),_trainSetState("done")},mr.start()}catch(e){toast("Microphone error: "+e.message,"error"),_trainSetState("ready")}}function _trainStopRecording(){trainState.mediaRec&&trainState.mediaRec.state==="recording"&&trainState.mediaRec.stop()}function _trainHideStage(hide){[".reh-stage-area","#reh-tts-status-bar","#reh-rec-overlay","#reh-synth-bar"].forEach(sel=>{const el=document.querySelector(sel)||document.getElementById(sel.replace("#",""));el&&(el.style.display=hide?"none":"")})}function enterTrainMode(){const seq=buildTrainSeq();if(!seq.length){toast('No "I play this" lines found \u2014 check "I play this" on at least one character in the Cast tab.',"error");return}trainState.seq=seq,trainState.turn=0,trainState.phase="idle",_trainHideStage(!0);const panel=$("reh-train-panel");panel&&(panel.hidden=!1),trainGoTo(0)}function exitTrainMode(){trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),_trainStopMic(),trainState.phase="idle",trainState.seq=[],_trainHideStage(!1);const panel=$("reh-train-panel");panel&&(panel.hidden=!0)}if((_ub=$("reh-bulk-toggle"))==null||_ub.addEventListener("click",()=>setBulkMode(!rehState.bulkMode)),(_vb=$("reh-bulk-done"))==null||_vb.addEventListener("click",()=>setBulkMode(!1)),(_wb=$("reh-bulk-all"))==null||_wb.addEventListener("click",()=>{document.querySelectorAll("#reh-script-lines .reh-bulk-check").forEach(cb=>rehState.bulkSel.add(parseInt(cb.dataset.bulk))),rehState.bulkAnchor=rehState.bulkSel.size?Math.min(...rehState.bulkSel):null,document.querySelectorAll("#reh-script-lines .reh-bulk-check").forEach(cb=>_refreshBulkLine(parseInt(cb.dataset.bulk))),_updateBulkCount()}),(_xb=$("reh-bulk-none"))==null||_xb.addEventListener("click",()=>{const had=[...rehState.bulkSel];rehState.bulkSel.clear(),rehState.bulkAnchor=null,had.forEach(_refreshBulkLine),_updateBulkCount()}),(_yb=$("reh-bulk-ignore"))==null||_yb.addEventListener("click",()=>_bulkApply(l=>{l.ignored=!0})),(_zb=$("reh-bulk-unignore"))==null||_zb.addEventListener("click",()=>_bulkApply(l=>{l.ignored=!1})),(_Ab=$("reh-bulk-hide"))==null||_Ab.addEventListener("click",()=>_bulkApply(l=>{l.hidden=!0})),(_Bb=$("reh-bulk-delete"))==null||_Bb.addEventListener("click",_bulkDelete),(_Cb=$("reh-bulk-show-hidden"))==null||_Cb.addEventListener("change",e=>{rehState.showHidden=e.target.checked,buildScriptPage()}),(_Db=$("reh-page-mode-btn"))==null||_Db.addEventListener("click",cyclePageMode),(_Eb=$("reh-train-open-btn"))==null||_Eb.addEventListener("click",enterTrainMode),(_Fb=$("reh-train-exit-btn"))==null||_Fb.addEventListener("click",exitTrainMode),(_Gb=$("reh-train-play"))==null||_Gb.addEventListener("click",()=>{const turn=trainState.seq[trainState.turn];turn&&trainState.phase!=="playing_cue"&&(trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),turn.cueLines.length?_trainPlayCueLines(turn.cueLines):_trainSetState("ready"))}),(_Hb=$("reh-train-record"))==null||_Hb.addEventListener("click",()=>{(trainState.phase==="ready"||trainState.phase==="done")&&_trainStartRecording()}),(_Ib=$("reh-train-stop-rec"))==null||_Ib.addEventListener("click",_trainStopRecording),(_Jb=$("reh-train-prev"))==null||_Jb.addEventListener("click",()=>{trainState.turn>0&&trainGoTo(trainState.turn-1)}),(_Kb=$("reh-train-next"))==null||_Kb.addEventListener("click",()=>{trainState.turn{trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),_trainStopMic();const turn=trainState.seq[trainState.turn];if(!turn)return;const resultEl=$("reh-train-result");resultEl&&(resultEl.hidden=!0),turn.cueLines.length?_trainPlayCueLines(turn.cueLines):_trainSetState("ready")}),rehRenderMeter(),_syncPageModeBtn(),renderLibraryList().catch(()=>{}),$("reh-skip-desc-toggle")&&($("reh-skip-desc-toggle").checked=rehState.skipDescriptions),window._rehearserStartImpEx)delete window._rehearserStartImpEx,showRehImpEx();else{const _initPhase=window._rehearserStartPhase||1;delete window._rehearserStartPhase,showPhase(_initPhase)}const readerState={mode:null,title:"",pdfDoc:null,pages:[],sentences:[],idx:0,readingIdx:-1,playing:!1,speed:1,scale:1,zoomMode:"fit-width",audioCtx:null,currentSource:null,raf:null,blobCache:new Map,bufCache:new Map,gainCache:new Map,unitsByPage:new Map,baseSentences:[],chunkMode:"sentence",normalize:!0,io:null,_seq:0,synthRunning:!1,synthCancel:!1,selecting:!1,selStart:null,selEnd:null,selAnchored:!1,fileBlob:null,docText:"",savedId:null,savedAudioIdx:new Set,sourceUploaded:!1,ocrWorker:null,synthStarted:!1};window.readerState=readerState;const READER_RESUME_KEY="reader-resume",READER_FMT="mp3",READER_BUF_WINDOW=3,READER_CANVAS_MARGIN=2200,READER_IMPORT_YIELD_EVERY=350,READER_PDF_INITIAL_PAGES=16,READER_PDF_INITIAL_MAX_WITHOUT_TEXT=64,READER_OCR_LANGS="deu+eng",READER_OCR_MIN_GAP_RATIO=.08,READER_OCR_MIN_GAP_PX=40,READER_OCR_RENDER_SCALE=2.5,READER_OCR_MIN_CONFIDENCE=40;function readerCtx(){return readerState.audioCtx||(readerState.audioCtx=new(window.AudioContext||window.webkitAudioContext)),readerState.audioCtx.state==="suspended"&&readerState.audioCtx.resume().catch(()=>{}),readerState.audioCtx}function readerYield(){return new Promise(resolve=>setTimeout(resolve,0))}window.readerOnShow=async function(){const sel=$("reader-backend-select");if(sel&&(!sel.value||sel.options.length<=1)){if(typeof availableTtsBackends=="function"&&!availableTtsBackends().length&&typeof refreshTtsBackendAvailability=="function")try{await refreshTtsBackendAvailability()}catch{}typeof availableTtsBackends=="function"&&availableTtsBackends().length&&(sel.innerHTML=ttsBackendOptions(sel.value),sel.disabled=!1)}readerUpdateBackendHint(),window.VoicePicker&&VoicePicker.upgrade("reader-voice-select"),readerRenderLibrary()},window.readerStop=function(){readerSaveResume(),readerPersistProgress(),readerStopPlayback()};async function readerFetchVoices(){var _a2;const btn=$("reader-fetch-voices-btn"),backend=(_a2=$("reader-backend-select"))==null?void 0:_a2.value;if(!backend){toast("No available TTS backend","error");return}btn&&(btn.disabled=!0);try{const raw=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json()),ids=(Array.isArray(raw)?raw:[]).map(v=>typeof v=="string"?v:v.id||v.name||v.voice_id).filter(Boolean),sel=$("reader-voice-select");if(!sel)return;const prev=sel.value;window.VoicePicker?(VoicePicker.upgrade("reader-voice-select"),VoicePicker.populate("reader-voice-select",ids),prev&&ids.includes(prev)?VoicePicker.setValue("reader-voice-select",prev):ids.length&&VoicePicker.setValue("reader-voice-select",ids[0])):(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)),toast("Fetched "+ids.length+" voices","success")}catch(e){toast("Fetch failed: "+(e.message||e),"error")}finally{btn&&(btn.disabled=!1)}}function readerUpdateBackendHint(){var _a2;const el=$("reader-backend-hint");if(!el)return;const id=((_a2=$("reader-backend-select"))==null?void 0:_a2.value)||"",b=typeof backendById=="function"?backendById(id):null,steady=id&&(b&&b.style_aware||/design|custom|kokoro|magpie/i.test(id));el.innerHTML=steady?' Good for long-form narration. For maximum steadiness, increase \u201CVoice consistency\u201D.':' Cloned / zero-shot voices re-sample each chunk, so the voice can drift between sections. To steady it: raise \u201CVoice consistency\u201D to paragraph/page, set a fixed Seed with a low Temperature (e.g. 0.3), keep \u201CNormalise loudness\u201D on, or pick a style-aware backend.',el.classList.toggle("reader-hint-warn",!steady)}function readerTitleFromText(text){return(String(text||"").split(/\r?\n/).map(x=>x.trim()).find(Boolean)||"Pasted text").replace(/\s+/g," ").slice(0,80)||"Pasted text"}function readerUpdateToolbarVisibility(loaded=!!readerState.sentences.length){const toolbar=$("reader-toolbar");toolbar&&(toolbar.hidden=!loaded);const pdfOnly=readerState.mode==="pdf",zoom=$("reader-zoom");zoom&&(zoom.hidden=!pdfOnly);const legend=document.querySelector(".reader-legend");legend&&(legend.hidden=!pdfOnly)}function readerFinishLoadedDocument({resume=!0,successPrefix="Loaded"}={}){if(!readerState.sentences.length)return toast("No readable text found in this document","error"),!1;readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),$("reader-transport").hidden=!1,$("reader-dropzone").hidden=!0;const importArea=$("reader-import-area");importArea&&(importArea.hidden=!0),readerUpdateToolbarVisibility(!0),$("reader-synthbar").hidden=!1;const pr=$("reader-page-range");if(pr&&(pr.hidden=readerState.mode!=="pdf",readerState.mode==="pdf")){const n=readerState.pages.length,pf=$("reader-page-from"),pt=$("reader-page-to");pf&&(pf.max=n,pf.value=1),pt&&(pt.max=n,pt.value=n)}readerUpdateScopeLabel();const resumed=resume&&readerLoadResume();return readerUpdateProgress(),readerHighlightSentence(readerState.sentences[readerState.idx]),toast(resumed?"Resumed at sentence "+(readerState.idx+1)+" / "+readerState.sentences.length:successPrefix+" \xB7 "+readerState.sentences.length+" sentences \u2014 press play","success"),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("source"),!0}async function readerImportFile(file){if(!file)return;const name=(file.name||"").toLowerCase();readerResetDoc(),readerState.title=file.name.replace(/\.[^.]+$/,""),readerState.fileBlob=file;const titleEl=$("reader-doc-title");titleEl&&(titleEl.textContent=readerState.title);try{if(name.endsWith(".pdf")){toast("Loading PDF\u2026","success");const loaded=await readerLoadPdfSkeleton(file);if(!loaded)return;readerUpdateToolbarVisibility(!0),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("source"),await readerExtractPdfText(loaded),readerFinishLoadedDocument({resume:!1,successPrefix:"Extracted"})}else{const text=await file.text();readerState.docText=text,await readerLoadText(text),readerFinishLoadedDocument({successPrefix:"Loaded"})}}catch(e){toast("Import failed: "+(e.message||e),"error")}}async function readerImportPastedText(){const ta=$("reader-paste-text"),text=((ta==null?void 0:ta.value)||"").trim();if(!text){toast("Paste some text first","error"),ta==null||ta.focus();return}const btn=$("reader-paste-load");btn&&(btn.disabled=!0),readerResetDoc(),readerState.title=readerTitleFromText(text),readerState.docText=text,readerState.fileBlob=null,readerState.sourceUploaded=!1;const titleEl=$("reader-doc-title");titleEl&&(titleEl.textContent=readerState.title);try{await readerLoadText(text),readerFinishLoadedDocument({resume:!1,successPrefix:"Loaded pasted text"})}catch(e){toast("Import failed: "+(e.message||e),"error")}finally{btn&&(btn.disabled=!1)}}function readerResetDoc(){readerStopPlayback(),readerState._seq++,readerState.mode=null,readerState.pdfDoc=null,readerState.pages=[],readerState.sentences=[],readerState.idx=0,readerState.readingIdx=-1,readerState.scale=1,readerState.zoomMode="fit-width",readerState.blobCache.clear(),readerState.bufCache.clear(),readerState.gainCache.clear(),readerState.unitsByPage=new Map,readerState.baseSentences=[],readerState.fileBlob=null,readerState.docText="",readerState.savedId=null,readerState.savedAudioIdx=new Set,readerState.sourceUploaded=!1,readerState.io&&(readerState.io.disconnect(),readerState.io=null),readerState.synthRunning=!1,readerState.synthCancel=!1,readerState.synthStarted=!1,readerSetSelecting(!1),readerState.selStart=readerState.selEnd=null,readerState.selAnchored=!1;const sb=$("reader-synthbar");sb&&(sb.hidden=!0);const tr=$("reader-transport");tr&&(tr.hidden=!0),readerUpdateToolbarVisibility(!1);const sp=$("reader-synth-prog");sp&&(sp.hidden=!0);const si=$("reader-sel-info");si&&(si.hidden=!0);const doc=$("reader-doc");doc&&(doc.innerHTML="",doc.classList.remove("two-page","reader-selecting","reader-synth-started")),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("source");const importArea=$("reader-import-area");importArea&&(importArea.hidden=!1),readerClearSearch({clearInput:!0})}const READER_NO_LEADING_SPACE_RE=/^[«"'’”)\]]+$/;function readerBuildSentences(words){const sentences=[];let cur=null;const endRe=/[.!?]["'”’)\]]?$/,abbrev=/^(mr|mrs|ms|dr|prof|sr|jr|vs|etc|e\.g|i\.e|no|vol|st|fig)\.?$/i;for(const w of words)w.para&&cur&&cur.words.length&&(sentences.push(cur),cur=null),cur||(cur={text:"",words:[],status:"pending",_stat:null,paraStart:!!w.para}),cur.words.push(w),cur.text+=(cur.text&&!READER_NO_LEADING_SPACE_RE.test(w.text)?" ":"")+w.text,(endRe.test(w.text)&&!abbrev.test(w.text.replace(/[^a-z.]/gi,""))&&cur.words.length>=2||cur.words.length>=45)&&(sentences.push(cur),cur=null);return cur&&cur.words.length&&sentences.push(cur),sentences}function readerGroupUnits(base,mode){if(mode==="sentence"||!base.length)return base.map(s=>({text:s.text,words:s.words,status:"pending",_stat:null,paraStart:!!s.paraStart}));const maxChars=mode==="page"?2e3:500,isPdf=readerState.mode==="pdf",keyOf=s=>{var _a2,_b2,_c2,_d2;return isPdf?(_b2=(_a2=s.words[0])==null?void 0:_a2.page)!=null?_b2:0:(_d2=(_c2=s.words[0])==null?void 0:_c2.para)!=null?_d2:0},units=[];let cur=null;for(const s of base){const k=keyOf(s),boundary=cur&&(mode==="paragraph"&&k!==cur._key||mode==="page"&&isPdf&&k!==cur._key),tooLong=cur&&cur.words.length&&cur.text.length+s.text.length+1>maxChars;(boundary||tooLong)&&(units.push(cur),cur=null),cur||(cur={text:"",words:[],status:"pending",_stat:null,_key:k,paraStart:!!s.paraStart}),cur.text+=cur.text?(s.paraStart?` - -`:" ")+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}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}readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),readerState.savedId=id,readerState.sourceUploaded=!0,typeof window.studioRememberBook=="function"&&window.studioRememberBook(id,rec.title),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 -
- -
-
${escHtml(rec.title)}
-
- ${total} sentences${rec.pageCount?" \xB7 "+rec.pageCount+" pg":""} -
-
${readPct}% read \xB7 ${date}
-
-
`}).join(""),list.querySelectorAll(".reader-book").forEach(el=>{el.addEventListener("click",e=>{e.target.closest(".reader-book-del")||readerOpenLibraryDoc(el.dataset.id)});const delBtn=el.querySelector(".reader-book-del");delBtn&&delBtn.addEventListener("click",e=>{if(e.stopPropagation(),el.querySelector(".reader-book-del-confirm"))return;const confirmOverlay=document.createElement("div");confirmOverlay.className="reader-book-del-confirm",confirmOverlay.style.cssText="position:absolute; inset:0; background:rgba(0,0,0,0.85); color:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; border-radius:inherit; z-index:10; padding:12px; text-align:center; box-sizing:border-box;",confirmOverlay.innerHTML=` -
Delete Book?
-
Audio files will be removed.
-
- - -
- `,confirmOverlay.addEventListener("click",ce=>ce.stopPropagation()),confirmOverlay.querySelector("#btn-cancel-del").addEventListener("click",ce=>{ce.stopPropagation(),confirmOverlay.remove()}),confirmOverlay.querySelector("#btn-confirm-del").addEventListener("click",async ce=>{ce.stopPropagation(),confirmOverlay.innerHTML='';try{if(!(await fetch(`${READER_API}/${el.dataset.id}`,{method:"DELETE"})).ok)throw new Error("Failed to delete book");toast("Book deleted","success"),readerRenderLibrary()}catch(err){toast(err.message,"error"),confirmOverlay.remove()}}),el.appendChild(confirmOverlay)}),el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.style.borderColor="var(--accent)")}),el.addEventListener("dragleave",()=>{el.style.borderColor=""}),el.addEventListener("drop",async e=>{var _a2;if(!(e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")))return;e.preventDefault(),e.stopPropagation(),el.style.borderColor="";const f=(_a2=e.dataTransfer.files)==null?void 0:_a2[0];if(!f||!f.type.startsWith("image/")){toast("Please drop an image file","error");return}try{const id=el.dataset.id,r=await fetch(`${READER_API}/${id}/cover`,{method:"PUT",body:f});if(!r.ok)throw new Error(r.statusText);await fetch(`${READER_API}/${id}/progress`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({updated:new Date().toISOString()})}),toast("Cover image updated","success"),readerRenderLibrary()}catch(err){toast("Failed to upload cover: "+(err.message||err),"error")}})}),list.querySelectorAll(".reader-book-del").forEach(btn=>btn.addEventListener("click",e=>{e.stopPropagation(),confirm("Delete this saved document and its audio?")&&readerDeleteLibraryDoc(btn.dataset.del)}))}(_Mb=$("reader-file-input"))==null||_Mb.addEventListener("change",function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];this.value="",readerImportFile(f)}),(_Nb=$("reader-paste-load"))==null||_Nb.addEventListener("click",readerImportPastedText),(_Ob=$("reader-paste-text"))==null||_Ob.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="Enter"&&(e.preventDefault(),readerImportPastedText())}),(_Pb=$("reader-fetch-voices-btn"))==null||_Pb.addEventListener("click",readerFetchVoices),(_Qb=$("reader-backend-select"))==null||_Qb.addEventListener("change",()=>{const sel=$("reader-voice-select");sel&&(sel.innerHTML=''),window.VoicePicker&&(VoicePicker.upgrade("reader-voice-select"),VoicePicker.populate("reader-voice-select",[])),readerUpdateBackendHint()}),(_Rb=$("reader-chunk-mode"))==null||_Rb.addEventListener("change",function(){readerRechunk(this.value)}),(_Sb=$("reader-normalize"))==null||_Sb.addEventListener("change",function(){readerState.normalize=this.checked}),(_Tb=$("reader-play"))==null||_Tb.addEventListener("click",()=>{readerState.playing?readerPause():readerPlay()}),(_Ub=$("reader-stop"))==null||_Ub.addEventListener("click",()=>{readerStopPlayback(),readerState.idx=0,readerSaveResume(),readerUpdateProgress()}),(_Vb=$("reader-prev"))==null||_Vb.addEventListener("click",()=>readerJumpTo(readerState.idx-1)),(_Wb=$("reader-next"))==null||_Wb.addEventListener("click",()=>readerJumpTo(readerState.idx+1)),(_Xb=$("reader-speed"))==null||_Xb.addEventListener("input",function(){readerState.speed=parseFloat(this.value)||1;const lbl=$("reader-speed-label");lbl&&(lbl.textContent=readerState.speed.toFixed(2).replace(/0$/,"")+"\xD7"),readerState.currentSource&&(readerState.currentSource.playbackRate.value=readerState.speed)}),(_Yb=$("reader-zoom-fitw"))==null||_Yb.addEventListener("click",()=>readerApplyZoom("fit-width")),(_Zb=$("reader-zoom-fith"))==null||_Zb.addEventListener("click",()=>readerApplyZoom("fit-height")),(__b=$("reader-zoom-two"))==null||__b.addEventListener("click",()=>readerApplyZoom("two")),(_$b=$("reader-zoom-in"))==null||_$b.addEventListener("click",()=>{readerState.scale=Math.min(4,readerState.scale*1.2),readerApplyZoom("custom")}),(_ac=$("reader-zoom-out"))==null||_ac.addEventListener("click",()=>{readerState.scale=Math.max(.2,readerState.scale/1.2),readerApplyZoom("custom")});let _readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,_readerSearchTimer=null;const READER_SEARCH_MAX_HITS=2500;function readerSearchTextMap(words){var _a2;let text="";const spans=[];for(let i=0;isp.end>pos&&sp.startsp.wordIndex);idxs.length&&groups.push(idxs),pos=Math.max(pos+1,end)}return groups}function readerRemoveSearchHighlights(){document.querySelectorAll(".reader-search-hit").forEach(el=>el.remove()),document.querySelectorAll(".reader-word.search-hit, .reader-word.search-current").forEach(el=>{el.classList.remove("search-hit","search-current")})}function readerSetSearchStatus(text){const st=$("reader-search-status");st&&(st.textContent=text||"")}function readerClearSearch({clearInput=!1}={}){if(_readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,readerRemoveSearchHighlights(),readerSetSearchStatus(""),clearInput){const inp=$("reader-search");inp&&(inp.value="")}}function readerCollectSearchHits(term){var _a2,_b2,_c2;const hits=[],sents=readerState.sentences||[];for(let si=0;si=READER_SEARCH_MAX_HITS)return hits}}return hits}function readerPaintSearchHighlights(){if(readerRemoveSearchHighlights(),!_readerSearchTerm||!_readerSearchHits.length)return;const scale=readerState.scale||1;_readerSearchHits.forEach((hit,hi)=>{var _a2;const s=readerState.sentences[hit.sentenceIndex];if(s){hit.nodes=[];for(const wi of hit.wordIndexes){const w=(_a2=s.words)==null?void 0:_a2[wi];if(w)if(readerState.mode==="pdf"){const pg=readerState.pages[w.page];if(!(pg!=null&&pg.overlay))continue;const el=document.createElement("div");el.className="reader-search-hit"+(hi===_readerSearchCurrent?" search-current":""),Object.assign(el.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:Math.max(w.w,2)*scale+"px",height:Math.max(w.h,2)*scale+"px"}),pg.overlay.appendChild(el),hit.nodes.push(el)}else w.el&&(w.el.classList.add("search-hit"),hi===_readerSearchCurrent&&w.el.classList.add("search-current"),hit.nodes.push(w.el))}}})}function readerUpdateSearchStatus(){var _a2,_b2;if(!_readerSearchTerm){readerSetSearchStatus("");return}if(!((_a2=readerState.sentences)!=null&&_a2.length)){readerSetSearchStatus("No text loaded");return}if(!_readerSearchHits.length){readerSetSearchStatus("0/0");return}const current=_readerSearchCurrent>=0?_readerSearchCurrent+1:1,hit=_readerSearchHits[Math.max(0,_readerSearchCurrent)],capped=_readerSearchHits.length>=READER_SEARCH_MAX_HITS?"+":"",page=readerState.mode==="pdf"?" \xB7 p."+(((_b2=hit==null?void 0:hit.page)!=null?_b2:0)+1):"";readerSetSearchStatus(current+"/"+_readerSearchHits.length+capped+page)}function readerJumpToSearchHit(idx){var _a2;if(!_readerSearchHits.length)return;_readerSearchCurrent=(idx+_readerSearchHits.length)%_readerSearchHits.length,readerPaintSearchHighlights(),readerUpdateSearchStatus();const hit=_readerSearchHits[_readerSearchCurrent],node=(_a2=hit==null?void 0:hit.nodes)==null?void 0:_a2[0];if(node)readerEnsureVisible(node);else if(readerState.mode==="pdf"){const pg=readerState.pages[hit.page];pg!=null&&pg.pageDiv&&(readerRenderPage(hit.page),pg.pageDiv.scrollIntoView({behavior:"smooth",block:"center"}))}}function readerSearchApply(term,{jump=!0}={}){if(term=String(term||"").trim(),!term){readerClearSearch();return}_readerSearchTerm=term,_readerSearchHits=readerCollectSearchHits(term),_readerSearchCurrent=_readerSearchHits.length?0:-1,readerPaintSearchHighlights(),readerUpdateSearchStatus(),jump&&_readerSearchHits.length&&readerJumpToSearchHit(0)}function readerSearchStep(direction){var _a2;const term=((_a2=$("reader-search"))==null?void 0:_a2.value.trim())||_readerSearchTerm;if(!_readerSearchHits.length||term!==_readerSearchTerm){readerSearchApply(term,{jump:!0});return}readerJumpToSearchHit(_readerSearchCurrent+(direction===-1?-1:1))}(_bc=$("reader-search"))==null||_bc.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),readerSearchStep(e.shiftKey?-1:1)),e.key==="Escape"&&readerClearSearch({clearInput:!0})}),(_cc=$("reader-search"))==null||_cc.addEventListener("input",e=>{clearTimeout(_readerSearchTimer);const term=e.target.value.trim();if(!term){readerClearSearch();return}_readerSearchTimer=setTimeout(()=>readerSearchApply(term,{jump:!0}),160)}),(_dc=$("reader-search-prev"))==null||_dc.addEventListener("click",()=>readerSearchStep(-1)),(_ec=$("reader-search-next"))==null||_ec.addEventListener("click",()=>readerSearchStep(1)),(_fc=$("reader-search-clear"))==null||_fc.addEventListener("click",()=>readerClearSearch({clearInput:!0})),(_gc=$("reader-save-lib"))==null||_gc.addEventListener("click",readerSaveLibrary),(_hc=$("reader-export-btn"))==null||_hc.addEventListener("click",()=>{var _a2;return readerExport(((_a2=$("reader-export-mode"))==null?void 0:_a2.value)||"page")}),(_ic=$("reader-synth-all"))==null||_ic.addEventListener("click",readerSynthAll),(_jc=$("reader-synth-cancel"))==null||_jc.addEventListener("click",()=>{readerState.synthCancel=!0}),(_kc=$("reader-select-toggle"))==null||_kc.addEventListener("click",()=>{readerState.selecting?readerSetSelecting(!1):(readerClearSelection(),readerSetSelecting(!0))}),(_lc=$("reader-sel-clear"))==null||_lc.addEventListener("click",readerClearSelection),document.addEventListener("keydown",e=>{e.key==="Escape"&&readerState.selecting&&readerSetSelecting(!1)}),(_mc=$("reader-page-from"))==null||_mc.addEventListener("change",readerUpdateScopeLabel),(_nc=$("reader-page-to"))==null||_nc.addEventListener("change",readerUpdateScopeLabel),(_oc=$("reader-doc"))==null||_oc.addEventListener("click",e=>{if(!readerState.selecting||readerState.mode!=="pdf")return;const stat=e.target.closest(".reader-stat");if(!stat)return;const si=parseInt(stat.dataset.si);isNaN(si)||readerPickSentence(si)}),(_pc=$("reader-doc"))==null||_pc.addEventListener("scroll",function(){let t=null;return()=>{t||(t=setTimeout(()=>{t=null,readerRenderVisible()},120))}}()),window.addEventListener("resize",function(){let t=null;return()=>{readerState.mode!=="pdf"||readerState.zoomMode==="custom"||(clearTimeout(t),t=setTimeout(()=>readerApplyZoom(readerState.zoomMode),200))}}()),function(){["reader-dropzone","reader-doc"].map(id=>$(id)).filter(Boolean).forEach(el=>{el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.add("dragover"))}),el.addEventListener("dragleave",()=>el.classList.remove("dragover")),el.addEventListener("drop",e=>{var _a2;e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.remove("dragover"),readerImportFile((_a2=e.dataTransfer.files)==null?void 0:_a2[0]))})})}(),window.showReaderView=function(view){const main=$("reader-main-view"),cast=$("reader-audiobook-panel"),chars=$("reader-charsheets-panel"),lib=$("reader-library-card");main&&(main.hidden=view!=="main"),cast&&(cast.hidden=view!=="cast",view!=="cast"&&(cast.style.display="",cast.style.flexDirection="",cast.style.minHeight=""),view==="cast"&&!cast.innerHTML.trim()&&(cast.innerHTML=`

No active casting session

Open a document in the Reader tab and click "Cast as audiobook" to start extracting characters.


`,cast.className="ab-castpanel-inline card")),chars&&(chars.hidden=view!=="chars",view==="chars"&&!chars.innerHTML.trim()&&(chars.innerHTML=`
Character sheets

This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.

`,chars.className="")),lib&&(lib.hidden=view!=="library",view==="library"&&(lib.classList.remove("card"),lib.style.boxShadow="none",lib.style.border="none",lib.style.background="transparent",readerRenderLibrary())),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs(view==="cast"?"cast":view==="chars"?"chars":"source")},window.navReaderView=function(view){window.showReaderView(view)},window.readerJumpToPage=function(pageNum){const pg=parseInt(pageNum,10);if(!pg)return Promise.resolve(!1);typeof navTo=="function"&&navTo("s-reader"),window.showReaderView("main");let tries=0;const waitFrame=()=>new Promise(resolve=>requestAnimationFrame(resolve)),jump=async()=>{var _a2;await waitFrame();const pageState=(_a2=readerState.pages)==null?void 0:_a2[pg-1],pageDiv=pageState==null?void 0:pageState.pageDiv;return pageDiv&&!pageDiv.hidden?(await readerRenderPage(pg-1),pageDiv.scrollIntoView({behavior:"smooth",block:"start",inline:"nearest"}),readerRenderVisible(),!0):tries++<20?(await new Promise(resolve=>setTimeout(resolve,100)),jump()):(typeof toast=="function"&&toast("Source page is not loaded in Reader yet","info"),!1)};return jump()},window.readerOnShow=async function(){const sel=$("reader-backend-select");if(sel&&(!sel.value||sel.options.length<=1)){if(typeof availableTtsBackends=="function"&&!availableTtsBackends().length&&typeof refreshTtsBackendAvailability=="function")try{await refreshTtsBackendAvailability()}catch{}typeof availableTtsBackends=="function"&&availableTtsBackends().length&&(sel.innerHTML=ttsBackendOptions(sel.value),sel.disabled=!1)}readerUpdateBackendHint(),window.VoicePicker&&VoicePicker.upgrade("reader-voice-select"),readerRenderLibrary(),window._readerStartView&&(window.showReaderView(window._readerStartView),window._readerStartView=null),_readerEnsureImportVisible()};function _readerEnsureImportVisible(){if(readerState.sentences.length)return;const card=document.getElementById("reader-config-card"),body=card?card.querySelector(".card-col-body"):null;body&&body.hidden&&(body.hidden=!1,card.classList.remove("card-col-closed"))}const AUDIOBOOK_CHUNK_CHARS=3e3,AUDIOBOOK_WARMUP_TIMEOUT_MS=24e4,AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS=6e5,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS=3e5,AUDIOBOOK_RECAST_TIMEOUT_MS=24e4,AUDIOBOOK_RECAST_CONTEXT_CHARS=4200,AUDIOBOOK_RECAST_TARGETS_PER_CALL=6,AUDIOBOOK_DRAFT_AUTOSAVE_MS=5*60*1e3,_audiobook={running:!1,cancel:!1};function _abHideCastingControl(){document.querySelectorAll("#ab-cv-cancel").forEach(btn=>{btn.hidden=!0,btn.disabled=!1,btn.innerHTML=' Cancel'})}window._abDelegatedCancelHandler||(window._abDelegatedCancelHandler=!0,document.addEventListener("click",event=>{var _a2,_b2;const btn=(_b2=(_a2=event.target).closest)==null?void 0:_b2.call(_a2,"#ab-cv-cancel");!btn||btn.hidden||(_audiobook.running?(_audiobook.cancel=!0,btn.disabled=!0,btn.innerHTML=' Stopping...',typeof _audiobook.abort=="function"&&_audiobook.abort()):_abHideCastingControl())})),window._audiobook=_audiobook;let _abDraftAutosaveTimer=null,_abLastServerDraftErrorAt=0;function audiobookDialogueKey(text){return String(text||"").normalize("NFKC").replace(/[»«„“”"‘’'`]+/g,"").replace(/\s+/g," ").trim().toLowerCase()}function audiobookSegmentText(seg){return seg?seg.type==="narration"||!seg.speaker||/^Unknown|Unbekannt/i.test(seg.speaker)?seg.text||"":`"${seg.text||""}"`:""}function _abWordRangeAtPoint(x,y){let node=null,offset=0;if(document.caretRangeFromPoint){const r=document.caretRangeFromPoint(x,y);if(!r)return null;node=r.startContainer,offset=r.startOffset}else if(document.caretPositionFromPoint){const p=document.caretPositionFromPoint(x,y);if(!p)return null;node=p.offsetNode,offset=p.offset}else return null;if(!node||node.nodeType!==3)return null;const text=node.nodeValue||"",isW=ch=>ch!=null&&/[\p{L}\p{N}'’-]/u.test(ch);if(offset>=text.length&&(offset=text.length-1),!isW(text[offset]))if(offset>0&&isW(text[offset-1]))offset--;else return null;let a=offset,b=offset;for(;a>0&&isW(text[a-1]);)a--;for(;b+1{var _a3;return String(((_a3=segs[i])==null?void 0:_a3.text)||"").replace(/\s+/g," ").trim()},out=new Set;for(let i=0;i=1,excl=/[!?]\s*$/.test(t),imper=/^(?:(?:Nun|Jetzt|Dann|Also|Und|Aber|So|Doch|Hier|Da|Los)\s+)?(Bringt|Los|Mach|Macht|Kommt|Komm|Geht|Geh|Nehmt|Nimm|Haltet|Halt|Schafft|Schaff|T[öo]tet|Sieh|Seht|H[öo]rt|H[öo]r|Wartet|Warte|La[sß]|La[sß]t|Weist|Zeigt|Sagt|Sag|Gebt|Gib)\b/.test(t);p12&&(score+=1),p12n>=3&&(score+=2),excl&&(score+=1),imper&&(score+=3),p12&&excl&&(score+=1),t.length<160&&(p12||excl||imper)&&(score+=1),score>=minScore&&out.add(i)}return out}window._abSuspectNarration=_abSuspectNarration;function audiobookRecastGroups(unknownIdxs,segs){const groups=[];let cur=[];for(const idx of unknownIdxs){const prev=cur.length?cur[cur.length-1]:-1/0,shortGap=idx<=prev+2&&segs.slice(prev+1,idx).every(s=>!s||s.type==="narration"||/^Unknown|Unbekannt/i.test(s.speaker||""));!cur.length||shortGap&&cur.lengthsegs.slice(start,end).map(audiobookSegmentText).join(" ").trim();let text=render();for(;text.length>AUDIOBOOK_RECAST_CONTEXT_CHARS&&(starttargetEnd)&&(end>targetEnd&&end--,text=render(),!(text.length<=AUDIOBOOK_RECAST_CONTEXT_CHARS));)start=end?"after":"near"}: ${s.speaker}: ${(s.text||"").slice(0,100)}`)}return lines.slice(-12).join(` -`)}function audiobookFindReturnedSegment(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;let loose=null;for(let i=0;i=18&&(candKey.includes(targetKey)||targetKey.includes(candKey))&&(loose=loose||{idx:i,seg:cand})}return loose}function audiobookFindReturnedSegmentSequence(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;for(let i=0;ireturned[k])};if(key.length>targetKey.length*1.35+40)break}}return null}function _abStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):String(v)}const _AB_DRAFT_KEY="ttsvc_ab_draft";function _abBookId(){return window.readerState&&readerState.savedId||_audiobook.bookId||null}function _abDraftKey(bookId){return bookId?_AB_DRAFT_KEY+"_"+bookId:_AB_DRAFT_KEY}function _abTextId(text){const s=(text.slice(0,300)+text.slice(-300)).replace(/\s+/g,"");let h=5381;for(let i=0;i>>0;return h.toString(36)+"_"+text.length}function _abSafeFilename(name,fallback="cast"){return(String(name||fallback||"cast").replace(/[\\/:*?"<>|]+/g,"_").replace(/\s+/g,"_").replace(/^_+|_+$/g,"")||fallback||"cast").slice(0,120)}function _abCastMarkdown(data){const payload=data||{},segments=Array.isArray(payload.segments)?payload.segments:[],speakers=[...new Set(segments.filter(s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker).map(s=>String(s.speaker)))].sort(),lines=[`# ${payload.title||"Cast Script"}`,""],metaBits=[];payload.savedAt&&metaBits.push("exported "+new Date(payload.savedAt).toISOString().slice(0,16).replace("T"," ")),metaBits.push(`${speakers.length} character${speakers.length!==1?"s":""}`),metaBits.push(`${segments.length} segment${segments.length!==1?"s":""}`),lines.push(`*${metaBits.join(" \xB7 ")}*`,""),speakers.length&&lines.push(`**Characters:** ${speakers.join(", ")}`,"");let lastPage=null;for(const seg of segments){const text=String((seg==null?void 0:seg.text)||"").trim();if(text){if(seg.page!=null&&seg.page!==lastPage&&(lines.push("---","",`## Page ${seg.page}`,""),lastPage=seg.page),seg.type==="dialogue"){const speaker=String(seg.speaker||"Narrator"),tag=seg.emotion?` *(${seg.emotion})*`:"";lines.push(`**${speaker.toUpperCase()}**${tag}: "${text}"`)}else lines.push(text);lines.push("")}}return lines.join(` -`)}function _abStampSegmentPages(segs,pageMarks,text){if(!Array.isArray(segs)||!segs.length||segs.some(s=>s&&s.page!=null))return;const marks=(pageMarks||[]).slice().sort((a,b)=>(a.offset||0)-(b.offset||0)),src=text||"";if(!marks.length||!src)return;let markIdx=0,searchPos=0,cur=null;marks[0].offset<=2&&(cur=marks[0].page+1,markIdx=1);for(const s of segs){const probe=String(s.text||"").trim().slice(0,24),at=probe?src.indexOf(probe,searchPos):-1,pos=at>=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)cur=marks[markIdx].page+1,markIdx++;cur!=null&&(s.page=cur)}}let _abLastDraftSaveAt=0;const AB_DRAFT_SAVE_THROTTLE_MS=4e3;function _abSaveDraft(segs,roster,text,done,total){var _a2;const isFinal=total>0&&done>=total,now=Date.now();if(!isFinal&&now-_abLastDraftSaveAt{if(!r.ok)throw new Error(r.statusText||`HTTP ${r.status}`)}).catch(err=>{console.warn("[audiobook] server draft autosave failed:",err);const now2=Date.now();now2-_abLastServerDraftErrorAt>6e4&&(_abLastServerDraftErrorAt=now2,typeof toast=="function"&&toast("Server autosave failed. Browser draft is still saved.","error"))})}let _abExportBusy=!1;async function audiobookExportCastMd(){var _a2;if(_abExportBusy)return;_abExportBusy=!0,setTimeout(()=>{_abExportBusy=!1},2e3);const segs=_audiobook.segments||[];if(!segs.length){toast("No cast to export","error");return}const bookId=_abBookId();_audiobook.lastText&&_abSaveDraft(segs,_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0);const title=((_a2=window.readerState)==null?void 0:_a2.title)||_audiobook.title||"audiobook";if(bookId)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast/export`);if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const blob2=await r.blob(),match=(r.headers.get("Content-Disposition")||"").match(/filename="([^"]+)"/i),name=(match==null?void 0:match[1])||`${_abSafeFilename(title)}_cast.zip`;if(typeof readerDownload=="function")readerDownload(blob2,name);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob2),a.download=name,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}toast("Exported cast + character sheets (.zip)","success");return}catch{toast("Server export failed, downloading local cast copy","error")}const payload={bookId,title,textId:_audiobook.lastText?_abTextId(_audiobook.lastText):"",segments:segs,roster:_audiobook.roster||[],pageMarks:_audiobook.pageMarks||[],rehId:_audiobook.rehId||null,done:_audiobook.completedChunks||0,total:_audiobook.completedTotal||0,savedAt:Date.now()},blob=new Blob([_abCastMarkdown(payload)],{type:"text/markdown;charset=utf-8"});if(typeof readerDownload=="function")readerDownload(blob,`${_abSafeFilename(title)}_cast.md`);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=`${_abSafeFilename(title)}_cast.md`,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}}function _abStartDraftAutosave(saveNow){if(_abStopDraftAutosave(),typeof saveNow!="function")return;_abDraftAutosaveTimer=setInterval(saveNow,AUDIOBOOK_DRAFT_AUTOSAVE_MS);const saveOnLeave=()=>saveNow();_audiobook._draftSaveOnLeave=saveOnLeave,window.addEventListener("pagehide",saveOnLeave),window.addEventListener("beforeunload",saveOnLeave)}function _abStopDraftAutosave(){_abDraftAutosaveTimer&&clearInterval(_abDraftAutosaveTimer),_abDraftAutosaveTimer=null,_audiobook._draftSaveOnLeave&&(window.removeEventListener("pagehide",_audiobook._draftSaveOnLeave),window.removeEventListener("beforeunload",_audiobook._draftSaveOnLeave),_audiobook._draftSaveOnLeave=null)}async function _abEnsureLibraryBook(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2;const rs=window.readerState;if(!rs||rs.savedId||!((_a2=rs.sentences)!=null&&_a2.length))return!!(rs!=null&&rs.savedId);if(rs.mode==="pdf"&&!rs.fileBlob)return!1;const meta={title:rs.title||"Untitled",kind:rs.mode,idx:rs.idx,voice:((_b2=$("reader-voice-select"))==null?void 0:_b2.value)||"",backend:((_c2=$("reader-backend-select"))==null?void 0:_c2.value)||"",speed:rs.speed,instruct:((_d2=$("reader-instruct"))==null?void 0:_d2.value.trim())||"",chunkMode:rs.chunkMode,seed:((_e2=$("reader-seed"))==null?void 0:_e2.value.trim())||"",temperature:((_f2=$("reader-temp"))==null?void 0:_f2.value.trim())||"",tts_speed:parseFloat((_g2=$("reader-tts-speed"))==null?void 0:_g2.value)||1,normalize:rs.normalize,sentenceCount:rs.sentences.length,pageCount:((_h2=rs.pages)==null?void 0:_h2.length)||0,synthCount:0,updated:new Date().toISOString()};try{const r=await fetch("/api/reader/docs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(meta)});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);rs.savedId=(await r.json()).id,_audiobook.bookId=rs.savedId;const ext=rs.mode==="pdf"?"pdf":"txt",body=rs.mode==="pdf"?rs.fileBlob:new Blob([rs.docText||audiobookScopeText()||""],{type:"text/plain"});return(await fetch(`/api/reader/docs/${encodeURIComponent(rs.savedId)}/source?ext=${ext}`,{method:"PUT",body})).ok&&(rs.sourceUploaded=!0),typeof readerRenderLibrary=="function"&&readerRenderLibrary(),!0}catch(err){return console.warn("[audiobook] could not create reader-library autosave target:",err),!1}}async function _abLoadDraftServer(bookId){if(!bookId)return null;for(let attempt=0;attempt<2;attempt++)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`);if(!r.ok){if(attempt===0&&(r.status===404||r.status>=500)){await new Promise(resolve=>setTimeout(resolve,650));continue}return null}const d=await r.json();return d&&Array.isArray(d.segments)&&d.segments.length?d:null}catch{attempt===0&&await new Promise(resolve=>setTimeout(resolve,650))}return null}function _abLoadDraft(text){var _a2;const bookId=_abBookId(),textId=_abTextId(text),title=String(((_a2=window.readerState)==null?void 0:_a2.title)||"").trim().toLowerCase();try{if(bookId){const raw2=localStorage.getItem(_abDraftKey(bookId));if(raw2){const d=JSON.parse(raw2);if(d&&Array.isArray(d.segments)&&d.segments.length)return d}}const raw=localStorage.getItem(_AB_DRAFT_KEY);if(raw){const d=JSON.parse(raw);if(d&&Array.isArray(d.segments)&&d.segments.length&&d.textId===textId)return d}let best=null;for(let i=0;ibest.score)&&(best={score,draft:cand})}catch{}}if(best!=null&&best.draft)return best.draft}catch{return null}return null}function _abClearDraft(){const bookId=_abBookId();try{localStorage.removeItem(_abDraftKey(bookId))}catch{}bookId&&fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`,{method:"DELETE"}).catch(()=>{})}const AB_DIALOGUE_RE=/[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/;function audiobookHasDialogue(t){return AB_DIALOGUE_RE.test(t||"")}function audiobookDehyphenate(t){return(t||"").replace(/([a-zäöüß])-\s+(?=[a-zäöüßA-ZÄÖÜ])/g,"$1")}const AB_SPEECH_VERBS="(?:sagte|fragte|rief|antwortete|erwiderte|entgegnete|meinte|fl\xFCsterte|wisperte|raunte|murmelte|brummte|knurrte|br\xFCllte|schrie|stammelte|fauchte|zischte|seufzte|lachte|kicherte|befahl|wiederholte|fuhr\\s+fort|grunzte|versetzte|unterbrach|dr\xE4ngte|herrschte|keuchte|hauchte|\xE4chzte|st\xF6hnte|jammerte|spottete|h\xF6hnte|warf\\s+ein|setzte\\s+hinzu|erkl\xE4rte|verk\xFCndete|widersprach|beharrte|gestand|best\xE4tigte|erinnerte|dachte|\xFCberlegte|sinnierte|said|asked|replied|answered|whispered|murmured|muttered|shouted|cried|called|exclaimed|added|continued)",AB_NOTNAME=new Set(["Der","Die","Das","Den","Dem","Ein","Eine","Einen","Er","Sie","Es","Ich","Du","Wir","Ihr","Man","Und","Aber","Da","Dann","Doch","So","Nun","Jetzt","The","He","She","It","They","A","An","And","But","Then","Now","Sofort","Pl\xF6tzlich","Endlich","Schlie\xDFlich","Stille","Schweigen","Stimme","Stimmen","Frage","Antwort","Gel\xE4chter","Wieder","Gleich","Sogleich","Langsam","Leise","Laut","Kaum","Vielleicht","Nat\xFCrlich","Wirklich","Ja","Nein","Komm","Warte","Halt","Geh","Hier","Dort","Oben","Unten","Schon","Noch","Auch","Nur","Immer","Nie"]),_AB_NAME="([A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+)",AB_PERSON_NOUNS=new Set(["Mann","Frau","Junge","M\xE4dchen","Alte","Alter","Fremde","Fremder","Krieger","Kriegerin","W\xE4chter","Wache","Soldat","Hauptmann","Ork","Ritter","Magier","Magierin","Zwerg","Elf","Elfe","H\xE4ndler","Wirt","Wirtin","Bauer","Priester","Priesterin","Nachbar","Nachbarin","Sklave","Sklavin","Verweser","Inquisitor","General","K\xF6nig","K\xF6nigin","Prinz","Prinzessin","F\xFCrst","F\xFCrstin","Baron","Baronin","Bote","Diener","Dienerin","Knabe","Kind","Reiter","Reiterin","Bogensch\xFCtze","Schmied","Schmiedin","Heiler","Heilerin","Gelehrte","Gelehrter","Kapit\xE4n","Anf\xFChrer","Anf\xFChrerin"]);function audiobookResolveUnknowns(segs,prevTail,roster){var _a2,_b2;const isUnknown=s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)),isNamed=s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt|Narrator$/i.test(s.speaker),names=(roster||[]).filter(n=>n&&!/^(Narrator|Unknown|Unbekannt)/i.test(n)).sort((a,b)=>b.length-a.length),lastNameIn=text=>{let best=null,bestAt=-1;for(const n of names){const at=text.lastIndexOf(n);at>bestAt&&(bestAt=at,best=n)}if(best)return best;const clause=text.match(new RegExp(_AB_NAME+"[^.!?:]{0,80}\\b(?:und\\s+)?(?:"+AB_SPEECH_VERBS+")[^:]{0,60}:\\s*$"));if(clause&&!AB_NOTNAME.has(clause[1])&&!AB_PERSON_NOUNS.has(clause[1]))return clause[1];const m=[...text.matchAll(/\b(?:[Dd]er|[Dd]ie|[Dd]en|[Dd]em|[Ee]in|[Ee]ine)\s+([A-ZÄÖÜ][a-zäöüß]{2,})\b/g)].map(x=>x[1]).filter(n=>AB_PERSON_NOUNS.has(n));return m.length?m[m.length-1]:null},all=[...prevTail||[],...segs],offset=(prevTail||[]).length,resolved=[];for(let i=offset;i=0&&j>=i-6&&prevDialogues.length<2&&!(((_a2=all[j])==null?void 0:_a2.page)!=null&&s.page!=null&&all[j].page!==s.page);j--)if(((_b2=all[j])==null?void 0:_b2.type)==="dialogue"){if(!isNamed(all[j])){prevDialogues.length=0;break}prevDialogues.push(all[j].speaker)}prevDialogues.length===2&&prevDialogues[0]!==prevDialogues[1]&&(who=prevDialogues[1])}who&&_nextIsInquitText&&next&&next.type!=="narration"&&(next.type="narration",next.speaker="Narrator",next.emotion=""),who&&!/^(Narrator|Unknown|Unbekannt)$/i.test(who)&&(s.speaker=who,resolved.push(who))}return resolved}const _AB_TAG_NAME="[A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+",AB_SPEECH_TAG_ONLY_RE=new RegExp("^\\s*[,.;:!?\u2013-]*\\s*(?:(?:"+AB_SPEECH_VERBS+")\\b\\s+(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")\\s+(?:"+AB_SPEECH_VERBS+")\\b)(?:\\s+(?:mit|in|leise|laut|kalt|heiser|erstickt|ruhig|zornig|w\xFCtend|\xE4ngstlich|sp\xF6ttisch|ver\xE4chtlich|fragend|fl\xFCsternd|schrill|dumpf|slowly|coldly|quietly|softly|angrily|hoarsely)\\b[\\s\\S]*)?[.!?\u2026]*\\s*$","i");function audiobookIsSpeechTagOnly(text){const t=String(text||"").trim();return!t||t.length>180||/[»«„“”"‟‚‘’『「]/.test(t)?!1:AB_SPEECH_TAG_ONLY_RE.test(t)}function audiobookGuessSpeaker(after,before){let m;const ok=n=>n&&!AB_NOTNAME.has(n)?n:null;if(m=new RegExp("^[\\s,;\u2013-]*"+AB_SPEECH_VERBS+"\\s+(?:der|die|das|ein|eine)?\\s*"+_AB_NAME).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp("^[\\s,;\u2013-]*"+_AB_NAME+"\\s+"+AB_SPEECH_VERBS).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp(_AB_NAME+"\\s+"+AB_SPEECH_VERBS+"[\\s:,\u2013-]*$").exec(before||"")){const r=ok(m[1]);if(r)return r}return null}const AB_QUOTE_PAIRS={"\xBB":"\xAB","\xAB":"\xBB","\u201E":"\u201C","\u201C":"\u201D",'"':'"',"\u300C":"\u300D","\u300E":"\u300F","\u2018":"\u2019","\u201A":"\u2018"},_AB_UNCLOSED_TAG_RE=new RegExp("^\\s*(?:[,;:\u2013-]\\s*)?(?:"+AB_SPEECH_VERBS+"|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|I|we|you)\\s+"+AB_SPEECH_VERBS+")\\b","i");function _abUnclosedQuoteEnd(raw,closeIdx){const limit=closeIdx>=0?closeIdx:raw.length;for(let i=0;i40)return m[1].length}return closeIdx>=0?closeIdx:raw.length}function _abOrphanClosingQuoteSpan(text,closeIdx,minStart){var _a2;if(!"\xAB\u201D\u2019\u2039\u300D\u300F".includes(text[closeIdx]))return null;const before=text.slice(minStart,closeIdx),prev=((_a2=before.match(/\S(?=\s*$)/))==null?void 0:_a2[0])||"";if(!/[.!?]/.test(prev))return null;const trimmedLen=before.trimEnd().length;let localStart=0;const boundaryRe=/[\n\r]|[.!?:]\s+/g;let m;for(;m=boundaryRe.exec(before);){const next=m.index+m[0].length;nextch&&(AB_QUOTE_OPENERS+AB_QUOTE_CLOSERS).includes(ch);for(let i=0;is.text.trim())}function _audiobookMergeAdjacentSameSpeaker(segments){const out=[];for(const s of segments){const last=out[out.length-1];if(last&&last.type===s.type&&last.page===s.page){const lastSpeaker=(last.speaker||"Narrator").trim().toLowerCase(),curSpeaker=(s.speaker||"Narrator").trim().toLowerCase(),sameEmotion=s.type!=="dialogue"||(last.emotion||"").toLowerCase()===(s.emotion||"").toLowerCase(),unknownSpeaker=!lastSpeaker||lastSpeaker==="unknown"||lastSpeaker==="unbekannt";if(lastSpeaker===curSpeaker&&sameEmotion&&!(s.type==="dialogue"&&unknownSpeaker)){const lastText=last.text.trimEnd(),curText=s.text.trimStart();last.text=/[.!?…»«”"’']$/.test(lastText)?lastText+` - -`+curText:lastText+" "+curText;continue}}out.push(Object.assign({},s))}return out}const _AB_TITLE_PREFIX=/^(?:der|die|das|den|dem|ein|eine|einer|herr|frau|oberst|obrist|graf|gräfin|ritter|hauptmann|kommandant|meister|bruder|schwester|prinz|prinzessin|könig|königin|alter|alte|junger|junge|blonder|blonde)\s+/i;function _abIdentityTokens(name){let n=String(name||"").trim().toLowerCase(),prev=null;for(;prev!==n;)prev=n,n=n.replace(_AB_TITLE_PREFIX,"");return n.split(/[\s\-]+/).filter(Boolean)}const _AB_NOT_A_CHARACTER=new Set(["hinter","vor","neben","\xFCber","unter","zwischen","gegen\xFCber","keiner","keine","niemand","jemand","alle","beide","einige","viele","andere","anderen","jeder","jede","man","von","zu","bei","nach","aus","mit","ihm","ihr","ihnen","dort","hier","drau\xDFen","drinnen","oben","unten","pl\xF6tzlich","endlich","schlie\xDFlich","inneren","innere","herz","herzen","hand","h\xE4nde","haenden","kopf","stimme","stimmen","wind","feuer","flamme","flammen","dunkelheit","stille","schatten","licht","nacht","angst","schmerz","gedanke","gedanken","traum","tr\xE4ume","geliebten","geliebter","geliebte","blondschopf","antwort","echo","ferne","n\xE4he","naehe","tiefe","wasser","regen","donner","erde","himmel","sonne","mond","tod","leben","seele","geist der","wahrheit","zweifel","hoffnung","erinnerung","vergangenheit"]);function _abIsImplausibleSpeaker(name){const n=String(name||"").trim().toLowerCase().replace(/^(?:der|die|das|den|dem|ein|eine|einer|einem)\s+/,"");return n?_AB_NOT_A_CHARACTER.has(n):!1}function _audiobookDropPhantomSpeakers(segments){let dropped=0;const names=new Set;for(const s of segments)(s==null?void 0:s.type)==="dialogue"&&_abIsImplausibleSpeaker(s.speaker)&&(names.add(s.speaker),s.speaker="Unknown",dropped++);return{dropped,names:[...names]}}window._audiobookDropPhantomSpeakers=_audiobookDropPhantomSpeakers;function _audiobookConsolidateSpeakerAliases(segments){const count=new Map;for(const s of segments){if((s==null?void 0:s.type)!=="dialogue")continue;const sp=String(s.speaker||"").trim();!sp||/^(unknown|unbekannt|narrator)/i.test(sp)||count.set(sp,(count.get(sp)||0)+1)}const names=[...count.keys()],tokens=new Map(names.map(n=>[n,new Set(_abIdentityTokens(n))])),mapping=new Map;for(const a of names){const ta=tokens.get(a);if(!ta.size)continue;let best=null;for(const b of names){if(a===b)continue;const tb=tokens.get(b);if(!tb.size||tb.size<=ta.size)continue;let subset=!0;for(const t of ta)if(!tb.has(t)){subset=!1;break}subset&&(!best||count.get(b)>count.get(best)||count.get(b)===count.get(best)&&b.length>best.length)&&(best=b)}best&&mapping.set(a,best)}let moved=0;if(mapping.size)for(const s of segments){if((s==null?void 0:s.type)!=="dialogue")continue;const to=mapping.get(String(s.speaker||"").trim());to&&(s.speaker=to,moved++)}return{merged:mapping.size,moved,mapping}}window._audiobookConsolidateSpeakerAliases=_audiobookConsolidateSpeakerAliases;function _audiobookMergeSplitSentences(segments){const out=[];let merged=0;for(const s of segments){const prev=out[out.length-1],cur=String((s==null?void 0:s.text)||"").trim();if(prev&&prev.type==="narration"&&(s==null?void 0:s.type)==="narration"&&cur){const prevText=String(prev.text||"").trimEnd(),endsOpen=prevText&&!/[.!?:;…»"'\)\]]$/.test(prevText),startsLower=/^[a-zäöüß]/.test(cur),prevIsInquit=new RegExp(_AB_INQUIT_VERBS+"[^.!?]{0,40},\\s*$","i").test(prevText);if(endsOpen&&startsLower&&!prevIsInquit){prev.text=prevText+" "+cur,merged++;continue}}out.push(s)}return{segments:out,merged}}function _audiobookDedupNearbyDuplicates(segments){const QUOTE_CHARS='\xBB\xAB\u201E"\u2018\u2019\u203A\u2039',stripRe=new RegExp(`(^[${QUOTE_CHARS}\\s]+)|([${QUOTE_CHARS}\\s]+$)`,"g"),norm=t=>String(t||"").replace(stripRe,"").toLowerCase().replace(/\s+/g," ").trim(),WINDOW=30,MIN_LEN=20,out=[];let removed=0;for(const s of segments){const nt=norm(s==null?void 0:s.text);if(nt.length>=MIN_LEN){let isDup=!1;for(let k=out.length-1;k>=0&&out.length-k<=WINDOW;k--)if(out[k].type===s.type&&norm(out[k].text)===nt){isDup=!0;break}if(isDup){removed++;continue}}out.push(s)}return{segments:out,removed}}function _abPrefixDupCut(text,prefixText){const INSIG=/[«»„“”"‟‚‘’›‹\s]/,d=String(prefixText||"").replace(/[«»„“”"‟‚‘’›‹\s]/g,"").toLowerCase();if(d.length<12)return-1;const t=String(text||"");let j=0;for(let i=0;i{const r=String(role||"").toLowerCase(),n=String(name||"").toLowerCase();if(!r||!n||r===n||!labels.has(r)||!labels.has(n))return;votes.has(r)||votes.set(r,new Map);const m2=votes.get(r);m2.set(n,(m2.get(n)||0)+1)},W="([A-Z\xC4\xD6\xDC][A-Za-z\xC4\xD6\xDC\xE4\xF6\xFC\xDF-]{2,})",ART="(?:der|die|das|den|dem)",txt=String(sourceText||"");let m;const nameFirst=new RegExp("\\b"+W+",?\\s+"+ART+"\\s+"+W+"\\b","g");for(;m=nameFirst.exec(txt);)vote(m[2],m[1]);const roleFirst=new RegExp("\\b"+ART+"\\s+"+W+"\\s+"+W+"\\b","g");for(;m=roleFirst.exec(txt);)vote(m[1],m[2]);const MIN_VOTES=2,mapping=new Map;for(const[role,names]of votes){let best=null,bestN=0,runnerUp=0;for(const[n,c]of names)c>bestN?(runnerUp=bestN,best=n,bestN=c):c>runnerUp&&(runnerUp=c);best&&bestN>=MIN_VOTES&&bestN>runnerUp&&mapping.set(role,labels.get(best))}for(const[role,target]of mapping){let t=target,guard=0;for(;mapping.has(t.toLowerCase())&&mapping.get(t.toLowerCase())!==t&&guard++<5;)t=mapping.get(t.toLowerCase());mapping.set(role,t)}let moved=0;const pairs=[];for(const s of segments){if((s==null?void 0:s.type)!=="dialogue")continue;const to=mapping.get(String(s.speaker||"").trim().toLowerCase());to&&to!==s.speaker&&(pairs.push(`${s.speaker}\u2192${to}`),s.speaker=to,moved++)}return{merged:mapping.size,moved,pairs:[...new Set(pairs)]}}const _AB_GENERIC_ROLE_WORDS=new Set("mann frau junge m\xE4dchen menge m\xE4nner frauen krieger kriegerin wache wachen reiter schreiber adepten elfe zwerg zauberer magier magierin ork orks vampir inquisitor schmied oberst fremde fremder gestalt stimme sklave sklaven nachbar bote soldat soldaten ritter ritterin unsicher alter junger b\xE4ckerstochter weber kutscher druide amazone henker".split(" "));function _abIsGenericRoleLabel(name){const toks=String(name||"").trim().toLowerCase().replace(/^(der|die|das|den|dem|ein|eine|alter|alte|junger|junge)\s+/,"").match(/[\wäöüß]+/gu)||[];return toks.length>0&&toks.every(t=>_AB_GENERIC_ROLE_WORDS.has(t))}function _abSegmentConfidence(segs,i){const s=segs[i]||{},t=String(s.text||""),sp=String(s.speaker||"").trim(),why=[];let score;if(s.type==="narration"){score=88,why.push("narration"),/[»«]/.test(t)&&(score-=25,why.push("carries quote marks"));const p12=(t.match(/\b(ich|du|wir|ihr|mich|dich|uns|euch)\b/gi)||[]).length;return p12>=3?(score-=30,why.push("sustained 1st/2nd person \u2014 may be speech")):p12>=1&&(score-=8,why.push("1st/2nd person present")),/[!?]\s*$/.test(t)&&(score-=8,why.push("ends with ! or ?")),{score:Math.max(2,Math.min(99,score)),why}}if(!sp||/^(unknown|unbekannt)/i.test(sp))return{score:3,why:["speaker unknown"]};score=60,_abIsGenericRoleLabel(sp)&&(score-=28,why.push("speaker is a generic role label"));const nxt=i+10?String(segs[i-1].text||""):"",first=(sp.split(/\s+/)[0]||"").replace(/[^\wäöüß]/gu,""),V=_AB_INQUIT_VERBS;return first&&new RegExp(V+"\\W+"+first,"i").test(nxt)?(score+=32,why.push("speech tag names this speaker")):first&&new RegExp(first+"\\W+\\w*\\s*"+V,"i").test(nxt)?(score+=30,why.push("speaker + speech verb follows")):first&&new RegExp(first,"i").test(prv)?(score+=12,why.push("speaker named just before")):new RegExp("^\\s*[,\xBB\xAB]?\\s*"+V,"i").test(nxt)?(score+=6,why.push("unnamed speech tag follows")):(score-=12,why.push("no nearby speech tag")),/[»«]/.test(t)||(score-=14,why.push("no quote marks")),/\n\s*\n/.test(t.trim())&&(score-=22,why.push("fused: several turns in one segment")),t.trim().length<12&&(score-=10,why.push("very short line")),{score:Math.max(2,Math.min(99,score)),why}}const _AB_CONF_BANDS=[{max:19,pct:16,label:"Guess"},{max:34,pct:47,label:"Doubtful"},{max:49,pct:69,label:"Probably"},{max:64,pct:75,label:"Likely"},{max:79,pct:79,label:"Likely"},{max:100,pct:96,label:"Confident"}];function _abCalibrateConfidence(raw){for(const b of _AB_CONF_BANDS)if(raw<=b.max)return b;return _AB_CONF_BANDS[_AB_CONF_BANDS.length-1]}function _abCheckBtnHtml(s){if(typeof s.conf!="number")return"";const on=(s.confMark||(s.confChecked?"ok":null))==="ok";return``}function _abConfClass(conf){return typeof conf!="number"?"":conf>=80?"ab-conf-green":conf>=50?"ab-conf-orange":"ab-conf-red"}function _abConfBadgeHtml(s){if(typeof s.conf!="number")return"";const cls=_abConfClass(s.conf),label=s.conf>=80?"Confident":s.conf>=50?"Check this":"Likely wrong",why=s.confWhy?" \u2014 "+s.confWhy:"",mark=s.confMark||(s.confChecked?"ok":null);if(mark==="ok")return'\u2713 Correct';if(mark==="wrong")return'\u2717 Wrong';const word=s.confLabel||label;return`${word} ${s.conf}%`}function _abStampConfidence(segs){for(let i=0;i0&&!/[\s([{—–-]/.test(text[i-1]))continue;const raw=text.slice(i+1),closeIdx=raw.indexOf(close),endInRaw=_abUnclosedQuoteEnd(raw,closeIdx),quote=raw.slice(0,endInRaw).trim();if(!quote)continue;const consumedClose=closeIdx>=0&&endInRaw===closeIdx;spans.push({start:i,end:i+1+endInRaw+(consumedClose?1:0),quote}),i=spans[spans.length-1].end-1}if(!spans.length)return[{speaker:"Narrator",type:"narration",text,emotion:""}];const out=[];let last=0;for(let k=0;k{});const rehModel=$("reh-llm-model");return rehModel&&cleanModel&&[...rehModel.options].some(o=>o.value===cleanModel)&&(rehModel.value=cleanModel),{url:cleanUrl,model:cleanModel}}function audiobookScopeText(){var _a2,_b2,_c2;if(_audiobook.pageMarks=[],typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";const idxs=readerScopeIndices(),anchors=[];let lastPage=null;for(const i of idxs){const u=readerState.sentences[i],pg=(_c2=(_b2=u==null?void 0:u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;pg!=null&&pg!==lastPage&&(anchors.push({page:pg,anchor:(u.text||"").trim().slice(0,40)}),lastPage=pg)}let raw="";for(const i of idxs){const u=readerState.sentences[i];u!=null&&u.text&&(raw+=raw?(u.paraStart?` - -`:" ")+u.text:u.text)}raw=raw.replace(/[ \t]+/g," ").replace(/\n{3,}/g,` - -`).trim();const text=audiobookDehyphenate(raw);let from=0;for(const a of anchors){const probe=audiobookDehyphenate(a.anchor).slice(0,24);if(!probe)continue;const pos=text.indexOf(probe,from);pos>=0&&(_audiobook.pageMarks.push({offset:pos,page:a.page}),from=pos+probe.length)}return text}function audiobookProgress(total){let ov=document.getElementById("audiobook-overlay");ov||(ov=document.createElement("div"),ov.id="audiobook-overlay",ov.className="audiobook-overlay",ov.innerHTML=`
-
Casting audiobook
-
Analysing\u2026
-
-
-
`,document.body.appendChild(ov),ov.querySelector("#audiobook-cancel").addEventListener("click",()=>{_audiobook.cancel=!0})),ov.hidden=!1;const fill=ov.querySelector("#audiobook-fill"),msg=ov.querySelector("#audiobook-msg");return{update(done,label){fill&&(fill.style.width=done/total*100+"%"),msg&&label&&(msg.textContent=label)},done(){ov.hidden=!0}}}function _abNormalizeColor(color,fallbackName){return clNormalizeColor(color,fallbackName||"Narrator")}function _abDefaultCharacterColor(name){return/^Unknown|Unbekannt/i.test(name||"")?"#a83232":_abNormalizeColor(null,name)}function _abRecordColor(rec,name){var _a2;return _abNormalizeColor((rec==null?void 0:rec.color)||((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.color),name||(rec==null?void 0:rec.name))}let _abShowQuotes=(()=>{try{return localStorage.getItem("ab-show-quotes")!=="0"}catch{return!0}})();function _abDisplayText(s){const t=String((s==null?void 0:s.text)||"");return(s==null?void 0:s.type)!=="dialogue"||!t.trim()||/^\s*[»„"']/.test(t)?t:"\xBB"+t.trim()+"\xAB"}function _abMarkQuotes(html){return String(html||"").replace(/([»«„“”])/g,'$1')}function audiobookToggleQuotes(on){_abShowQuotes=on===void 0?!_abShowQuotes:!!on;try{localStorage.setItem("ab-show-quotes",_abShowQuotes?"1":"0")}catch{}return document.querySelectorAll(".ab-cv-quote-toggle").forEach(b=>{b.classList.toggle("is-on",_abShowQuotes),b.title=_abShowQuotes?"Hide quotation marks":"Show quotation marks"}),document.body.classList.toggle("ab-hide-quotes",!_abShowQuotes),_abShowQuotes}try{document.body.classList.toggle("ab-hide-quotes",!_abShowQuotes)}catch{}window.audiobookToggleQuotes=audiobookToggleQuotes;function highlightText(text,names){if(!text)return"";let html=escHtml(text);return(names||_audiobook&&_audiobook.roster||[]).filter(n=>n&&n.toLowerCase()!=="narrator"&&!/^Unknown|Unbekannt/i.test(n)).sort((a,b)=>b.length-a.length).forEach(name=>{if(name.length<2)return;const safe=name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),color=_abDefaultCharacterColor(name);html=html.replace(new RegExp(`\\b(${safe})\\b`,"gi"),m=>`${m}`)}),html}function _abShowBusyOverlay(message,withProgress){const ov=document.createElement("div");return ov.className="audiobook-overlay ab-busy-overlay",ov.innerHTML=`
- -
${escHtml(message)}
- ${withProgress?'
':""} -
`,document.body.appendChild(ov),ov.setProgress=(done,total)=>{const fill=ov.querySelector(".ab-busy-fill");fill&&total&&(fill.style.width=Math.round(done/total*100)+"%");const msgEl=ov.querySelector(".ab-busy-msg");msgEl&&(msgEl.textContent=`${message} (${done} / ${total})`)},ov}let _abFootMenuEl=null;function _abCloseFootMenu(){var _a2;_abFootMenuEl&&((_a2=document.getElementById(_abFootMenuEl.dataset.forId))==null||_a2.classList.remove("is-menu-open"),_abFootMenuEl.remove(),_abFootMenuEl=null,document.removeEventListener("click",_abFootMenuOutside,!0),document.removeEventListener("keydown",_abFootMenuKey,!0))}function _abFootMenuOutside(e){_abFootMenuEl&&!_abFootMenuEl.contains(e.target)&&!e.target.closest(".ab-foot-trigger")&&_abCloseFootMenu()}function _abFootMenuKey(e){e.key==="Escape"&&_abCloseFootMenu()}function _abToggleFootMenu(triggerBtn,items){const reopening=_abFootMenuEl&&_abFootMenuEl.dataset.forId===triggerBtn.id;if(_abCloseFootMenu(),reopening)return;const el=document.createElement("div");el.className="ab-foot-menu",el.dataset.forId=triggerBtn.id,el.innerHTML=items.map((it,i)=>it.divider?'
':``).join(""),document.body.appendChild(el);const rect=triggerBtn.getBoundingClientRect();el.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-el.offsetWidth-8))+"px";const menuH=el.offsetHeight,spaceBelow=window.innerHeight-rect.bottom,spaceAbove=rect.top;spaceBelow>=menuH+12||spaceBelow>=spaceAbove?(el.style.top=Math.min(rect.bottom+6,Math.max(8,window.innerHeight-menuH-8))+"px",el.style.bottom="auto"):(el.style.bottom=window.innerHeight-rect.top+6+"px",el.style.top="auto"),el.querySelectorAll(".ab-foot-menu-item").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const it=items[Number(btn.dataset.idx)];_abCloseFootMenu(),(_a2=it==null?void 0:it.onClick)==null||_a2.call(it)})}),_abFootMenuEl=el,triggerBtn.classList.add("is-menu-open"),setTimeout(()=>{document.addEventListener("click",_abFootMenuOutside,!0),document.addEventListener("keydown",_abFootMenuKey,!0)},0)}function _abOpenRecastSelectPopup(existingChars){const ov=document.createElement("div");ov.className="audiobook-overlay";const selected=new Set;let sort="lines",filter="";try{sort=localStorage.getItem("ttsvc_ab_recast_sort")||"lines"}catch{}const rowHtml=c=>{var _a2;const lineCount=((_a2=c==null?void 0:c.sheet)==null?void 0:_a2.line_count)||0,color=_abRecordColor(c,c.name),avatar=c!=null&&c.image?``:`${escHtml(((c==null?void 0:c.name)||"?")[0].toUpperCase())}`;return``},render=()=>{const q=filter.trim().toLowerCase(),items=existingChars.filter(c=>!q||(c.name||"").toLowerCase().includes(q)).sort(sort==="alpha"?(a,b)=>(a.name||"").localeCompare(b.name||""):(a,b)=>{var _a2,_b2;return(((_a2=b.sheet)==null?void 0:_a2.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)}),list=ov.querySelector(".ab-recast-select-list");list&&(list.innerHTML=items.length?items.map(rowHtml).join(""):'
No matches
',list.querySelectorAll(".ab-recast-cb").forEach(cb=>{cb.addEventListener("change",()=>{cb.checked?selected.add(cb.value):selected.delete(cb.value)})}))};ov.innerHTML=`
Select characters to cast or recast roles
Only re-reads the passages that mention the characters you pick below, including known aliases, plus a little surrounding context. Everyone else's sheet stays as-is.
`,document.body.appendChild(ov),render();const sortSel=ov.querySelector("#ab-recast-sort");sortSel.value=sort,sortSel.addEventListener("change",()=>{sort=sortSel.value;try{localStorage.setItem("ttsvc_ab_recast_sort",sort)}catch{}render()});let _searchT=null;ov.querySelector("#ab-recast-search").addEventListener("input",e=>{clearTimeout(_searchT);const v=e.target.value;_searchT=setTimeout(()=>{filter=v,render()},120)}),ov.querySelector("#ab-recast-cancel").addEventListener("click",()=>ov.remove()),ov.querySelector("#ab-recast-select-all").addEventListener("click",e=>{const allSelected=existingChars.length>0&&existingChars.every(c=>selected.has(c.name));allSelected?selected.clear():existingChars.forEach(c=>selected.add(c.name)),e.currentTarget.textContent=allSelected?"Select all":"Select none",render()}),ov.querySelector("#ab-recast-confirm").addEventListener("click",async()=>{const names=[...ov.querySelectorAll(".ab-recast-cb:checked")].map(cb=>cb.value).filter(Boolean);if(!names.length){toast("Select at least one character","error");return}ov.remove(),typeof window.audiobookRecastSelectedCharacters=="function"?await window.audiobookRecastSelectedCharacters(names):typeof window.csForReaderSelective=="function"&&await window.csForReaderSelective(names)})}async function audiobookRecastSelectedCharacters(selectedNames){return typeof window.csForReaderSelective=="function"?window.csForReaderSelective(selectedNames):(toast("Character-sheet recast is unavailable right now","error"),null)}window.audiobookRecastSelectedCharacters=audiobookRecastSelectedCharacters;function _abScrollIntoView(el,opts){el&&(el.scrollIntoView(Object.assign({behavior:"auto"},opts)),requestAnimationFrame(()=>requestAnimationFrame(()=>{el.scrollIntoView(Object.assign({behavior:"smooth"},opts))})))}function audiobookCastView(total,llmUrl,defaultModel,isIdle=!1){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2;const panel=document.getElementById("reader-audiobook-panel");if(!panel)return;if(typeof window.navReaderView=="function")window.navReaderView("cast");else if(typeof window.showReaderView=="function")window.showReaderView("cast");else{const mainView=document.getElementById("reader-main-view");mainView&&(mainView.hidden=!0),panel.hidden=!1}panel.className="ab-castpanel-inline card",panel.style.display="",panel.style.flexDirection="",panel.style.minHeight="",panel.innerHTML=` -
- - Casting audiobook - passage 0 / ${total} - - - - - - - - - - -
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
- Characters found - -
-
- - -
-
-
- ${[38,62,45,28,54,35].map(w=>`
`).join("")} -
-
-
-
-
- - - - - - -
`;const AB_DEFAULT_PROMPT=`Du bist ein erfahrener Drehbuchautor und H\xF6rbuch-Regisseur. Deine Aufgabe ist es, einen Auszug aus einem deutschen Roman zu analysieren und ihn perfekt in einzelne Segmente f\xFCr Erz\xE4hler und Dialoge (w\xF6rtliche Rede) zu unterteilen. - -WICHTIGE DEFINITION VON DIALOG: -Text ist NUR dann 'dialogue', wenn er explizit in Anf\xFChrungszeichen steht (z.B. \xBB...\xAB, \u201E...\u201C, "...", \xAB...\xBB, <<...>>) oder mit einem Gedankenstrich (\u2014) beginnt. ALLES ANDERE, einschlie\xDFlich Handlungsbeschreibungen (Inquit-Formeln), inneren Gedanken und Beschreibungen, MUSS als 'narration' (Erz\xE4hler) deklariert werden. -Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog). - -ABSATZ- UND KAPITELSTRUKTUR: -Eine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/Szenenanfang). Eine sehr kurze, alleinstehende Zeile direkt vor einer Leerzeile (z.B. "1. Kapitel", "Prolog", ein Zahlwort) ist eine Kapitel\xFCberschrift \u2014 immer 'narration'/'Narrator', niemals Dialog. Behalte Leerzeilen als eigenst\xE4ndige narration-Segmente oder als Teil des umgebenden Erz\xE4hler-Segments bei; erfinde daraus keinen Dialog und l\xF6sche sie nicht aus dem rekonstruierten Text. - -GRAMMATIK-REGELN F\xDCR NARRATION: -- Inquit-Formeln / Sprecher-Tags sind IMMER narration: finite Sprechverben wie sagte, fragte, rief, entgegnete, murmelte, fl\xFCsterte, schrie, antwortete + Subjekt/Pronomen/Name (z.B. "murmelte er mit erstickter Stimme.", ", entgegnete Marcian kalt."). -- Action Beats sind IMMER narration: Ein Charakter handelt, blickt, geht, lacht, schweigt, hebt die Hand, dreht sich um, usw. \u2014 auch wenn direkt davor/danach Dialog steht. -- Grammatische Probe: Wenn der Text eine Erz\xE4hler-Aussage \xDCBER das Sprechen ist (Verb + Sprecher + Art und Weise), ist es narration. Nur die tats\xE4chlich ge\xE4u\xDFerten W\xF6rter innerhalb der Anf\xFChrungszeichen sind dialogue. -- Satzfragmente mit f\xFChrendem Komma/Punkt wie ", sagte sie leise." oder ". fragte Uriens." sind niemals eigenst\xE4ndige Dialoge; sie geh\xF6ren als narration zum Erz\xE4hlertext. - -ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv \u2014 arbeite wie ein Lektor): -1. Direkte Zuordnung: "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) IMMER zum tats\xE4chlichen Namen auf: Das Pronomen meint die zuletzt genannte Person passenden Geschlechts. -2. Doppelpunkt-Regel: Endet ein Erz\xE4hlersatz mit ":", spricht das Subjekt dieses Satzes das folgende Zitat (z.B. "Dann richtete er sich auf und rief in die Runde:" \u2192 der zuvor genannte Charakter spricht das n\xE4chste Zitat; "Ein anderer fragte verschlafen:" \u2192 dieser andere spricht). -3. Nachgestellte Zuordnung: Der Erz\xE4hlersatz NACH einem Zitat verr\xE4t oft den Sprecher \u2014 auch bei unpers\xF6nlicher Formel: "\xBBWas machst du denn da?\xAB ert\xF6nte es \xFCber ihm. Karyla hatte ihren Hammer weggelegt und war her\xFCbergekommen." \u2192 Karyla sprach das Zitat. -4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat AKTIV HANDELT (tritt vor, dreht sich, hebt die Hand), spricht meist \u2014 ABER NUR bei Handlungsverben. Verben der WAHRNEHMUNG oder REAKTION (sp\xFCrte, h\xF6rte, f\xFChlte, sah, blickte, lauschte, bemerkte, nickte) kennzeichnen den ZUH\xD6RER, NICHT den Sprecher (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau). -5. Adressaten-Regel: "X wandte sich an Y" / "X sah Y an" \u2192 X spricht das n\xE4chste Zitat, und Y ist der wahrscheinlichste Antwortende. -6. Das Ping-Pong-Prinzip: Sprechen zwei Personen miteinander, wechseln sie sich strikt ab \u2014 auch \xFCber viele Zitate OHNE Inquit-Formeln hinweg. Verfolge die Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung und f\xFChre sie bis zum Szenenende fort. In einer Zwei-Personen-Szene ist 'Unknown' fast immer falsch. -7. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt sie zum Wissen oder Tonfall eines bestimmten Charakters? -8. Namenlose Sprecher: Hat eine sprechende Person im bisherigen Text KEINEN Eigennamen, ist eine Rollenbezeichnung ein g\xFCltiger Sprecher (z.B. 'Ork', 'Nachbar', 'W\xE4chter'). Ist die Person dagegen namentlich bekannt, benutze IMMER den Eigennamen und NIE die Rollenbezeichnung \u2014 eine Rollenbezeichnung ist kein Ersatz f\xFCr einen Namen und auch kein Mittel, 'Unknown' zu vermeiden. -9. Wiederverwendung: Nutze EXAKT die Namen aus der Liste der bekannten Charaktere, FALLS der Name dort aufgef\xFChrt ist. Wenn ein neuer Charakter spricht, extrahiere seinen Namen direkt aus dem Text (z.B. 'Karyla', 'Uriens'). -10. 'Unknown' NUR, wenn eine Zuordnung trotz aller obigen Regeln unm\xF6glich ist. Ist der EINZIGE Anhaltspunkt f\xFCr einen Namen, dass die Figur in der N\xE4he steht oder in der Szene wichtig ist, gib 'Unknown' aus statt zu raten. Ist die Person im Text namentlich bekannt, benutze den Eigennamen statt einer Rollenbezeichnung. -11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende \u2014 die Rede geht danach unver\xE4ndert weiter, bis das tats\xE4chliche schlie\xDFende Anf\xFChrungszeichen erscheint. -12. Stimm-Ank\xFCndigung: Erw\xE4hnt ein Erz\xE4hlersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), geh\xF6rt diese Zeile dieser Person \u2014 nicht 'Unknown'. -13. ZUH\xD6RER-AUSSCHLUSS (negative Evidenz \u2014 sehr wichtig): Ein Name im Erz\xE4hlsatz neben einem Zitat ist H\xC4UFIG der Zuh\xF6rer, nicht der Sprecher. Schlie\xDFe eine Person als Sprecher AUS, wenn sie (a) im Nachbarsatz wahrnimmt oder reagiert (Marcian sp\xFCrte ihre Tr\xE4nen auf seiner Brust; Marcian h\xF6rte dar\xFCber hinweg \u2192 Marcian H\xD6RT ZU, jemand anderes spricht), oder (b) im Zitat selbst ANGEREDET wird (Vokativ nach Komma: M\xF6gen die G\xF6tter dich sch\xFCtzen, Marcian. \u2192 Marcian ist der ANGESPROCHENE). Wer angeredet wird, spricht nicht. Wenn durch diesen Ausschluss kein Kandidat mehr \xFCbrig bleibt, suche WEITER ZUR\xDCCK (letzte eindeutige Nennung, Ping-Pong-Kette, wer in der Szene anwesend ist) \u2014 der Ausschluss grenzt die Kandidaten nur ein und rechtfertigt f\xFCr sich allein NIEMALS 'Unknown'. -16. FORTSETZUNGS-REGEL (wichtiger als Ping-Pong): Beschreibt der Erzaehlsatz ZWISCHEN zwei Zitaten dieselbe Person, die gerade gesprochen hat (per Pronomen oder Name, z.B. "Leise schluchzte sie.", "Er atmete tief durch."), dann spricht diese Person WEITER \u2014 das folgende Zitat gehoert IHR, nicht dem Gespraechspartner. Das Ping-Pong-Prinzip (Regel 6) gilt NUR, wenn der Erzaehlsatz auf die ANDERE Person zeigt oder gar keinen Hinweis enthaelt. Beispiel: Cindira spricht, dann "Leise schluchzte sie." \u2192 das naechste Zitat ist wieder CINDIRA; ein folgendes "Marcian spuerte ihre Traenen" bestaetigt nur, dass Marcian zuhoert (Regel 13). -17. NEU AUFTRETENDE, UNBENANNTE HANDELNDE: Fuehrt der Erzaehlsatz unmittelbar vor einem Zitat eine unbenannte Person handelnd ein ("Eine Frau durchbrach die Kette der Krieger und warf sich...", "Ein Soldat trat vor"), dann spricht GENAU DIESE Person. Verwende die Rollenbezeichnung aus dem Text als Sprechernamen ("Eine Frau", "Ein Soldat") \u2014 niemals 'Unknown'. Spricht dieselbe Person spaeter erneut (erkennbar am Pronomen im Erzaehlsatz), nutze exakt DENSELBEN Rollennamen weiter, damit sie eine durchgehende Stimme behaelt. -18. EPITHETA / BESTIMMTE BESCHREIBUNGEN als Sprecher: Bezeichnungen wie "die Elfe", "der Zauberer", "der Zwerg", "der Hauptmann" meinen fast immer eine BEREITS BEKANNTE Figur. Loese sie auf den kanonischen Namen auf, sobald der Text die Verbindung herstellt (Nyrilla ist die Elfe \u2192 "Laut hallte die Stimme der Elfe durch den Gang." bedeutet: NYRILLA hat gesprochen). Nutze immer den kanonischen Namen als Sprecher, niemals das Epitheton und niemals 'Unknown', damit die Figur durchgehend dieselbe Stimme behaelt. -19. GEMISCHTE SEGMENTE TRENNEN: Enthaelt ein Segment sowohl Erzaehltext als auch ein Zitat ("Nyrilla richtete sich auf. Alle anderen wichen zurueck. \xBBIch werde jetzt ...\xAB"), dann zerlege es: der Erzaehlteil wird ein eigenes Segment mit speaker 'Narrator' und type 'narration', das Zitat ein eigenes Segment mit type 'dialogue' und der handelnden Person als Sprecher (hier NYRILLA, denn sie richtet sich unmittelbar davor auf \u2014 siehe Regel 4). Ein gemischtes Segment darf niemals als Ganzes 'Unknown' bleiben. ZERLEGE EBENSO Segmente, die REDEBEITRAEGE MEHRERER SPRECHER enthalten: Ein Befehl, der jemanden beim Namen anspricht, und die darauffolgende Antwort sind ZWEI Sprecher, nicht einer (z.B. "Bringt Fackeln ... Lysandra, du nimmst dir einige Krieger ..." = MARCIAN befiehlt; "Mit Vergn\xFCgen, ich hab doch gleich gesagt ..." = LYSANDRA antwortet). Merkmal: Wer im ersten Teil per Vokativ ANGESPROCHEN wird (Regel 13b), ist typischerweise der Sprecher des NAECHSTEN Redebeitrags. Lege fuer jeden Redebeitrag ein eigenes Segment mit dem jeweiligen Sprecher an, statt den ganzen Block als 'Unknown' zu lassen. -14. GETEILTE ZITATE / Pronomen-Inquit: Beginnt ein Segment mit Komma oder Kleinbuchstaben plus Sprechverb und Pronomen (, antwortete er knapp. / , murmelte sie leise.), ist es die abgetrennte Inquit-Formel des DAVORSTEHENDEN Zitats. Ordne jenes Zitat der Person zu, auf die das Pronomen zeigt: die zuletzt genannte Person passenden Geschlechts VOR dem Zitat (er \u2192 letzter Mann, sie \u2192 letzte Frau). Solche Zeilen d\xFCrfen niemals 'Unknown' bleiben. -15. MEHRERE KANDIDATEN in der N\xE4he: Stehen mehrere bekannte Namen im Umfeld, entscheide in dieser Reihenfolge: (a) streiche alle nach Regel 13 ausgeschlossenen (Zuh\xF6rer/Angeredete); (b) bevorzuge, wer grammatisches SUBJEKT eines Sprech- oder Handlungsverbs ist, gegen\xFCber jemandem in Objekt- oder Pr\xE4positionalstellung; (c) pr\xFCfe, wessen Wissensstand, Rolle und Sprechweise zum Inhalt passt; (d) nutze das Ping-Pong-Prinzip aus Regel 6. Erst wenn danach zwei gleichwertige Kandidaten bleiben, ist 'Unknown' zul\xE4ssig. - -F\xDCR JEDES SEGMENT GIBST DU FOLGENDES AUS: -- speaker: 'Narrator' f\xFCr Narration/Erz\xE4hlertext, oder den EXAKTEN Namen des Charakters f\xFCr gesprochene Dialoge. -- type: 'narration' oder 'dialogue' -- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' BEH\xC4LTST du die umschlie\xDFenden Anf\xFChrungszeichen als Teil des Texts (z.B. \xBBHallo!\xAB bleibt \xBBHallo!\xAB) \u2014 entferne sie NICHT und lasse niemals nur eines der beiden \xFCbrig. -- emotion: Bei Dialogen 1-2 deutsche W\xF6rter, die den Tonfall beschreiben (z.B. w\xFCtend, fl\xFCsternd, \xE4ngstlich). Bei Narration leer lassen (''). - -STRIKTE FORMAT- UND TEXTREGELN: -- Mische NIEMALS Narration und Dialog im selben Segment! Trenne sie strikt. Wenn ein Zitat durch eine Handlungsanweisung unterbrochen wird (\xBBNein\xAB, sagte sie, \xBBhalt.\xAB), erstelle 3 Segmente: dialogue ("\xBBNein\xAB"), narration (", sagte sie, "), dialogue ("\xBBhalt.\xAB"). -- \xBBText?\xAB und \xBBText!\xAB sind vollst\xE4ndige Dialoge \u2014 das ?\xAB bzw. !\xAB schlie\xDFt das Zitat ab, auch wenn es ungewohnt aussieht. -- PDF-/OCR-SCHUTZ: Wenn ein \xBB oder \xAB offensichtlich fehlt, darf dieser eine Fehler NICHT den Rest der Passage als Dialog verschlucken. Schlie\xDFe ein offenes \xBB-Zitat am ersten plausiblen Satzende (? ! .), besonders wenn danach eine Inquit-Formel folgt ("fl\xFCsterte er", "sagte sie", "rief Uriens") oder normale Erz\xE4hlerhandlung weitergeht. -- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und h\xE4nge das schlie\xDFende \xAB an dessen Ende an, statt es als eigenes Segment stehen zu lassen \u2014 ein Anf\xFChrungszeichen darf NIE ein eigenes Segment f\xFCr sich bilden. -- Nur wenn ein offenes \xBB wirklich am Ende des Auszugs steht und danach KEINE Erz\xE4hlerhandlung/Inquit-Formel mehr folgt, behandle den Text ab \xBB bis Textende als 'dialogue'. -- Anf\xFChrungszeichen geh\xF6ren IMMER zum Dialog-Segment, NIEMALS zum Narration-Segment davor oder danach: Das schlie\xDFende \xAB am Ende einer Figurenrede geh\xF6rt ans ENDE des dialogue-Segments, nicht an den Anfang des folgenden narration-Segments. Das \xF6ffnende \xBB am Anfang einer Figurenrede geh\xF6rt an den ANFANG des dialogue-Segments, nicht ans Ende des vorherigen narration-Segments. Ein narration-Segment darf NIE mit einem einzelnen \xBB, \u201E, \u201A oder \u203A beginnen oder enden, und NIE mit einem einzelnen \xAB, ", ' oder \u2039 enden oder beginnen \u2014 verschiebe das Zeichen ins richtige Nachbar-Segment. -- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!`,normalizeCastingPrompt=prompt=>{let p=prompt||AB_DEFAULT_PROMPT;if(p=p.replace("- Wenn ein Auszug mit einem offenen \xBB-Zitat endet (kein schlie\xDFendes \xAB), behandle den Text ab \xBB bis Textende als 'dialogue'.",`- PDF-/OCR-SCHUTZ: Wenn ein \xBB oder \xAB offensichtlich fehlt, darf dieser eine Fehler NICHT den Rest der Passage als Dialog verschlucken. Schlie\xDFe ein offenes \xBB-Zitat am ersten plausiblen Satzende (? ! .), besonders wenn danach eine Inquit-Formel folgt ("fl\xFCsterte er", "sagte sie", "rief Uriens") oder normale Erz\xE4hlerhandlung weitergeht. -- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und entferne das einzelne \xAB aus dem ausgegebenen Text. -- Nur wenn ein offenes \xBB wirklich am Ende des Auszugs steht und danach KEINE Erz\xE4hlerhandlung/Inquit-Formel mehr folgt, behandle den Text ab \xBB bis Textende als 'dialogue'.`),p=p.replace("- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' ENTFERNST du die umschlie\xDFenden Anf\xFChrungszeichen.","- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' ENTFERNST du die umschlie\xDFenden Anf\xFChrungszeichen vollst\xE4ndig (nie nur ein einzelnes \xBB oder \xAB stehen lassen)."),p=p.replace("- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren!","- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren \u2014 abgesehen von entfernten \xE4u\xDFeren Dialog-Anf\xFChrungszeichen!"),p=p.replace("- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' ENTFERNST du die umschlie\xDFenden Anf\xFChrungszeichen vollst\xE4ndig (nie nur ein einzelnes \xBB oder \xAB stehen lassen).","- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' BEH\xC4LTST du die umschlie\xDFenden Anf\xFChrungszeichen als Teil des Texts (z.B. \xBBHallo!\xAB bleibt \xBBHallo!\xAB) \u2014 entferne sie NICHT und lasse niemals nur eines der beiden \xFCbrig."),p=p.replace('- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und entferne das einzelne \xAB aus dem ausgegebenen Text.','- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und h\xE4nge das schlie\xDFende \xAB an dessen Ende an, statt es als eigenes Segment stehen zu lassen \u2014 ein Anf\xFChrungszeichen darf NIE ein eigenes Segment f\xFCr sich bilden.'),p=p.replace("- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren \u2014 abgesehen von entfernten \xE4u\xDFeren Dialog-Anf\xFChrungszeichen!","- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!"),/Doppelpunkt-Regel/.test(p)||(p=p.replace(`ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv): -1. Direkte Zuordnung: Achte auf W\xF6rter wie "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) zum tats\xE4chlichen Namen auf. -2. Handlungs-Hinweise (Action Beats): Wenn ein Charakter eine Handlung ausf\xFChrt und direkt davor/danach w\xF6rtliche Rede steht, spricht meist dieser Charakter (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"). -3. Das Ping-Pong-Prinzip: Wenn zwei Personen sprechen, wechseln sie sich ab. Verfolge diese Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung. -4. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt die Aussage zum Wissen oder Tonfall eines bestimmten Charakters? -5. Wiederverwendung: Nutze EXAKT die Namen aus der Liste der bekannten Charaktere, FALLS der Name dort aufgef\xFChrt ist. Wenn ein neuer Charakter spricht, extrahiere seinen Namen direkt aus dem Text (z.B. 'Karyla', 'Uriens'). -6. Unbekannte Sprecher: Nur wenn eine Zuordnung durch Kontext absolut nicht m\xF6glich ist, verwende 'Unknown'. Rate nicht blind, aber bevorzuge immer einen namentlich genannten Charakter gegen\xFCber 'Unknown'.`,`ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv \u2014 arbeite wie ein Lektor): -1. Direkte Zuordnung: "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) IMMER zum tats\xE4chlichen Namen auf: Das Pronomen meint die zuletzt genannte Person passenden Geschlechts. -2. Doppelpunkt-Regel: Endet ein Erz\xE4hlersatz mit ":", spricht das Subjekt dieses Satzes das folgende Zitat (z.B. "Dann richtete er sich auf und rief in die Runde:" \u2192 der zuvor genannte Charakter spricht das n\xE4chste Zitat; "Ein anderer fragte verschlafen:" \u2192 dieser andere spricht). -3. Nachgestellte Zuordnung: Der Erz\xE4hlersatz NACH einem Zitat verr\xE4t oft den Sprecher \u2014 auch bei unpers\xF6nlicher Formel: "\xBBWas machst du denn da?\xAB ert\xF6nte es \xFCber ihm. Karyla hatte ihren Hammer weggelegt und war her\xFCbergekommen." \u2192 Karyla sprach das Zitat. -4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat AKTIV HANDELT (tritt vor, dreht sich, hebt die Hand), spricht meist \u2014 ABER NUR bei Handlungsverben. Verben der WAHRNEHMUNG oder REAKTION (sp\xFCrte, h\xF6rte, f\xFChlte, sah, blickte, lauschte, bemerkte, nickte) kennzeichnen den ZUH\xD6RER, NICHT den Sprecher (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau). -5. Adressaten-Regel: "X wandte sich an Y" / "X sah Y an" \u2192 X spricht das n\xE4chste Zitat, und Y ist der wahrscheinlichste Antwortende. -6. Das Ping-Pong-Prinzip: Sprechen zwei Personen miteinander, wechseln sie sich strikt ab \u2014 auch \xFCber viele Zitate OHNE Inquit-Formeln hinweg. Verfolge die Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung und f\xFChre sie bis zum Szenenende fort. In einer Zwei-Personen-Szene ist 'Unknown' fast immer falsch. -7. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt sie zum Wissen oder Tonfall eines bestimmten Charakters? -8. Namenlose Sprecher: Hat eine sprechende Person im bisherigen Text KEINEN Eigennamen, ist eine Rollenbezeichnung ein g\xFCltiger Sprecher (z.B. 'Ork', 'Nachbar', 'W\xE4chter'). Ist die Person dagegen namentlich bekannt, benutze IMMER den Eigennamen und NIE die Rollenbezeichnung \u2014 eine Rollenbezeichnung ist kein Ersatz f\xFCr einen Namen und auch kein Mittel, 'Unknown' zu vermeiden. -9. Wiederverwendung: Nutze EXAKT die Namen aus der Liste der bekannten Charaktere, FALLS der Name dort aufgef\xFChrt ist. Wenn ein neuer Charakter spricht, extrahiere seinen Namen direkt aus dem Text (z.B. 'Karyla', 'Uriens'). -10. 'Unknown' NUR, wenn eine Zuordnung trotz aller obigen Regeln unm\xF6glich ist. Ist der EINZIGE Anhaltspunkt f\xFCr einen Namen, dass die Figur in der N\xE4he steht oder in der Szene wichtig ist, gib 'Unknown' aus statt zu raten. Ist die Person im Text namentlich bekannt, benutze den Eigennamen statt einer Rollenbezeichnung.`)),/GRAMMATIK-REGELN FÜR NARRATION/.test(p)||(p=p.replace('Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).',`Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog). - -GRAMMATIK-REGELN F\xDCR NARRATION: -- Inquit-Formeln / Sprecher-Tags sind IMMER narration: finite Sprechverben wie sagte, fragte, rief, entgegnete, murmelte, fl\xFCsterte, schrie, antwortete + Subjekt/Pronomen/Name (z.B. "murmelte er mit erstickter Stimme.", ", entgegnete Marcian kalt."). -- Action Beats sind IMMER narration: Ein Charakter handelt, blickt, geht, lacht, schweigt, hebt die Hand, dreht sich um, usw. \u2014 auch wenn direkt davor/danach Dialog steht. -- Grammatische Probe: Wenn der Text eine Erz\xE4hler-Aussage \xDCBER das Sprechen ist (Verb + Sprecher + Art und Weise), ist es narration. Nur die tats\xE4chlich ge\xE4u\xDFerten W\xF6rter innerhalb der Anf\xFChrungszeichen sind dialogue. -- Satzfragmente mit f\xFChrendem Komma/Punkt wie ", sagte sie leise." oder ". fragte Uriens." sind niemals eigenst\xE4ndige Dialoge; sie geh\xF6ren als narration zum Erz\xE4hlertext.`)),/ABSATZ- UND KAPITELSTRUKTUR/.test(p)||(p=p.replace('Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).',`Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog). - -ABSATZ- UND KAPITELSTRUKTUR: -Eine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/Szenenanfang). Eine sehr kurze, alleinstehende Zeile direkt vor einer Leerzeile (z.B. "1. Kapitel", "Prolog", ein Zahlwort) ist eine Kapitel\xFCberschrift \u2014 immer 'narration'/'Narrator', niemals Dialog. Behalte Leerzeilen als eigenst\xE4ndige narration-Segmente oder als Teil des umgebenden Erz\xE4hler-Segments bei; erfinde daraus keinen Dialog und l\xF6sche sie nicht aus dem rekonstruierten Text.`)),!/Gedankenstrich-Pause-Regel/.test(p)||!/ZUHÖRER-AUSSCHLUSS/.test(p)||!/FORTSETZUNGS-REGEL/.test(p)||!/EPITHETA/.test(p)){const newRules=`11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende \u2014 die Rede geht danach unver\xE4ndert weiter, bis das tats\xE4chliche schlie\xDFende Anf\xFChrungszeichen erscheint. -12. Stimm-Ank\xFCndigung: Erw\xE4hnt ein Erz\xE4hlersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), geh\xF6rt diese Zeile dieser Person \u2014 nicht 'Unknown'. -13. ZUH\xD6RER-AUSSCHLUSS (negative Evidenz \u2014 sehr wichtig): Ein Name im Erz\xE4hlsatz neben einem Zitat ist H\xC4UFIG der Zuh\xF6rer, nicht der Sprecher. Schlie\xDFe eine Person als Sprecher AUS, wenn sie (a) im Nachbarsatz wahrnimmt oder reagiert (Marcian sp\xFCrte ihre Tr\xE4nen auf seiner Brust; Marcian h\xF6rte dar\xFCber hinweg \u2192 Marcian H\xD6RT ZU, jemand anderes spricht), oder (b) im Zitat selbst ANGEREDET wird (Vokativ nach Komma: M\xF6gen die G\xF6tter dich sch\xFCtzen, Marcian. \u2192 Marcian ist der ANGESPROCHENE). Wer angeredet wird, spricht nicht. Wenn durch diesen Ausschluss kein Kandidat mehr \xFCbrig bleibt, suche WEITER ZUR\xDCCK (letzte eindeutige Nennung, Ping-Pong-Kette, wer in der Szene anwesend ist) \u2014 der Ausschluss grenzt die Kandidaten nur ein und rechtfertigt f\xFCr sich allein NIEMALS 'Unknown'. -16. FORTSETZUNGS-REGEL (wichtiger als Ping-Pong): Beschreibt der Erzaehlsatz ZWISCHEN zwei Zitaten dieselbe Person, die gerade gesprochen hat (per Pronomen oder Name, z.B. "Leise schluchzte sie.", "Er atmete tief durch."), dann spricht diese Person WEITER \u2014 das folgende Zitat gehoert IHR, nicht dem Gespraechspartner. Das Ping-Pong-Prinzip (Regel 6) gilt NUR, wenn der Erzaehlsatz auf die ANDERE Person zeigt oder gar keinen Hinweis enthaelt. Beispiel: Cindira spricht, dann "Leise schluchzte sie." \u2192 das naechste Zitat ist wieder CINDIRA; ein folgendes "Marcian spuerte ihre Traenen" bestaetigt nur, dass Marcian zuhoert (Regel 13). -17. NEU AUFTRETENDE, UNBENANNTE HANDELNDE: Fuehrt der Erzaehlsatz unmittelbar vor einem Zitat eine unbenannte Person handelnd ein ("Eine Frau durchbrach die Kette der Krieger und warf sich...", "Ein Soldat trat vor"), dann spricht GENAU DIESE Person. Verwende die Rollenbezeichnung aus dem Text als Sprechernamen ("Eine Frau", "Ein Soldat") \u2014 niemals 'Unknown'. Spricht dieselbe Person spaeter erneut (erkennbar am Pronomen im Erzaehlsatz), nutze exakt DENSELBEN Rollennamen weiter, damit sie eine durchgehende Stimme behaelt. -18. EPITHETA / BESTIMMTE BESCHREIBUNGEN als Sprecher: Bezeichnungen wie "die Elfe", "der Zauberer", "der Zwerg", "der Hauptmann" meinen fast immer eine BEREITS BEKANNTE Figur. Loese sie auf den kanonischen Namen auf, sobald der Text die Verbindung herstellt (Nyrilla ist die Elfe \u2192 "Laut hallte die Stimme der Elfe durch den Gang." bedeutet: NYRILLA hat gesprochen). Nutze immer den kanonischen Namen als Sprecher, niemals das Epitheton und niemals 'Unknown', damit die Figur durchgehend dieselbe Stimme behaelt. -19. GEMISCHTE SEGMENTE TRENNEN: Enthaelt ein Segment sowohl Erzaehltext als auch ein Zitat ("Nyrilla richtete sich auf. Alle anderen wichen zurueck. \xBBIch werde jetzt ...\xAB"), dann zerlege es: der Erzaehlteil wird ein eigenes Segment mit speaker 'Narrator' und type 'narration', das Zitat ein eigenes Segment mit type 'dialogue' und der handelnden Person als Sprecher (hier NYRILLA, denn sie richtet sich unmittelbar davor auf \u2014 siehe Regel 4). Ein gemischtes Segment darf niemals als Ganzes 'Unknown' bleiben. ZERLEGE EBENSO Segmente, die REDEBEITRAEGE MEHRERER SPRECHER enthalten: Ein Befehl, der jemanden beim Namen anspricht, und die darauffolgende Antwort sind ZWEI Sprecher, nicht einer (z.B. "Bringt Fackeln ... Lysandra, du nimmst dir einige Krieger ..." = MARCIAN befiehlt; "Mit Vergn\xFCgen, ich hab doch gleich gesagt ..." = LYSANDRA antwortet). Merkmal: Wer im ersten Teil per Vokativ ANGESPROCHEN wird (Regel 13b), ist typischerweise der Sprecher des NAECHSTEN Redebeitrags. Lege fuer jeden Redebeitrag ein eigenes Segment mit dem jeweiligen Sprecher an, statt den ganzen Block als 'Unknown' zu lassen. -14. GETEILTE ZITATE / Pronomen-Inquit: Beginnt ein Segment mit Komma oder Kleinbuchstaben plus Sprechverb und Pronomen (, antwortete er knapp. / , murmelte sie leise.), ist es die abgetrennte Inquit-Formel des DAVORSTEHENDEN Zitats. Ordne jenes Zitat der Person zu, auf die das Pronomen zeigt: die zuletzt genannte Person passenden Geschlechts VOR dem Zitat (er \u2192 letzter Mann, sie \u2192 letzte Frau). Solche Zeilen d\xFCrfen niemals 'Unknown' bleiben. -15. MEHRERE KANDIDATEN in der N\xE4he: Stehen mehrere bekannte Namen im Umfeld, entscheide in dieser Reihenfolge: (a) streiche alle nach Regel 13 ausgeschlossenen (Zuh\xF6rer/Angeredete); (b) bevorzuge, wer grammatisches SUBJEKT eines Sprech- oder Handlungsverbs ist, gegen\xFCber jemandem in Objekt- oder Pr\xE4positionalstellung; (c) pr\xFCfe, wessen Wissensstand, Rolle und Sprechweise zum Inhalt passt; (d) nutze das Ping-Pong-Prinzip aus Regel 6. Erst wenn danach zwei gleichwertige Kandidaten bleiben, ist 'Unknown' zul\xE4ssig.`,unknownRuleRe=/(\n\d+\. 'Unknown' NUR,[^\n]*'Unknown'\.)/;unknownRuleRe.test(p)?p=p.replace(unknownRuleRe,`$1 -${newRules}`):p+=` -${newRules}`}return/gehören IMMER zum Dialog-Segment/.test(p)||(p=p.replace("- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!",`- Anf\xFChrungszeichen geh\xF6ren IMMER zum Dialog-Segment, NIEMALS zum Narration-Segment davor oder danach: Das schlie\xDFende \xAB am Ende einer Figurenrede geh\xF6rt ans ENDE des dialogue-Segments, nicht an den Anfang des folgenden narration-Segments. Das \xF6ffnende \xBB am Anfang einer Figurenrede geh\xF6rt an den ANFANG des dialogue-Segments, nicht ans Ende des vorherigen narration-Segments. Ein narration-Segment darf NIE mit einem einzelnen \xBB, \u201E, \u201A oder \u203A beginnen oder enden, und NIE mit einem einzelnen \xAB, ", ' oder \u2039 enden oder beginnen \u2014 verschiebe das Zeichen ins richtige Nachbar-Segment. -- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!`)),p},rawPrompt=typeof _appSettings!="undefined"&&_appSettings.audiobook_prompt?_appSettings.audiobook_prompt:AB_DEFAULT_PROMPT,globalPrompt=normalizeCastingPrompt(rawPrompt);globalPrompt!==rawPrompt&&typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=globalPrompt,fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:globalPrompt})}).catch(()=>{})),panel.querySelector("#ab-cv-prompt-text").value=globalPrompt;const closePanel=()=>{if(panel.hidden=!0,panel.innerHTML="",typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1),typeof window.navReaderView=="function")window.navReaderView("main");else if(typeof window.showReaderView=="function")window.showReaderView("main");else{const mv=document.getElementById("reader-main-view");mv&&(mv.hidden=!1)}},setStoppingState=()=>{const cancelBtn=panel.querySelector("#ab-cv-cancel");cancelBtn&&(cancelBtn.disabled=!0,cancelBtn.innerHTML=' Stopping...',cancelBtn.title="Stopping the current casting request");const status2=panel.querySelector("#ab-cv-status-msg");status2&&(status2.style.display="inline-block",status2.textContent="Stopping... completed passages will stay saved.")};panel.querySelector("#ab-cv-cancel").addEventListener("click",()=>{if(_audiobook.running){_audiobook.cancel=!0,setStoppingState(),typeof _audiobook.abort=="function"&&_audiobook.abort();return}closePanel()}),panel.querySelector("#ab-cv-prompt-btn").addEventListener("click",()=>{const p=panel.querySelector("#ab-cv-prompt-panel");p.hidden=!p.hidden;const chevron=panel.querySelector("#ab-cv-prompt-chevron");chevron&&(chevron.className=p.hidden?"mdi mdi-chevron-down":"mdi mdi-chevron-up")}),function(){const tBtn=panel.querySelector("#ab-cv-settings-toggle"),sBox=panel.querySelector("#ab-cv-settings"),chev=panel.querySelector("#ab-cv-settings-chevron");function apply(collapsed2){if(sBox&&(sBox.style.display=collapsed2?"none":"flex"),collapsed2){const p=panel.querySelector("#ab-cv-prompt-panel");p&&(p.hidden=!0)}chev&&(chev.className="mdi "+(collapsed2?"mdi-chevron-down":"mdi-chevron-up"))}let collapsed=!1;try{collapsed=localStorage.getItem("ttsvc_ab_settings_collapsed")==="1"}catch{}apply(collapsed),tBtn&&tBtn.addEventListener("click",()=>{collapsed=!collapsed;try{localStorage.setItem("ttsvc_ab_settings_collapsed",collapsed?"1":"0")}catch{}apply(collapsed)})}(),function(){const side=panel.querySelector("#ab-cv-side"),body=panel.querySelector(".ab-cv-body"),cBtn=panel.querySelector("#ab-cv-side-collapse");function apply(collapsed2){side&&side.classList.toggle("is-collapsed",collapsed2),body&&body.classList.toggle("side-collapsed",collapsed2),cBtn&&(cBtn.querySelector(".mdi").className="mdi "+(collapsed2?"mdi-chevron-left":"mdi-chevron-right")),cBtn&&(cBtn.title=collapsed2?"Expand character list":"Collapse to avatars")}let collapsed=!1;try{collapsed=localStorage.getItem("ttsvc_ab_side_collapsed")==="1"}catch{}apply(collapsed),cBtn&&cBtn.addEventListener("click",()=>{collapsed=!collapsed;try{localStorage.setItem("ttsvc_ab_side_collapsed",collapsed?"1":"0")}catch{}apply(collapsed)})}();let savedPrompts=[];try{savedPrompts=JSON.parse(localStorage.getItem("ttsvc_ab_prompts")||"[]")}catch{savedPrompts=[]}const libSelect=panel.querySelector("#ab-cv-prompt-lib"),delBtn=panel.querySelector("#ab-cv-prompt-del"),promptText=panel.querySelector("#ab-cv-prompt-text"),renderPromptLib=(selectedIdx=-1)=>{libSelect.innerHTML=''+savedPrompts.map((p,i)=>``).join(""),selectedIdx>=0?(libSelect.value=selectedIdx,delBtn.style.display="block"):(libSelect.value="",delBtn.style.display="none")};renderPromptLib(),libSelect.addEventListener("change",()=>{const idx=parseInt(libSelect.value),nameInput=panel.querySelector("#ab-cv-prompt-name");!isNaN(idx)&&savedPrompts[idx]?(promptText.value=savedPrompts[idx].prompt,nameInput&&(nameInput.value=savedPrompts[idx].name),delBtn.style.display="block"):(nameInput&&(nameInput.value=""),delBtn.style.display="none")}),delBtn.addEventListener("click",()=>{const idx=parseInt(libSelect.value);isNaN(idx)||confirm("Delete this saved prompt preset?")&&(savedPrompts.splice(idx,1),localStorage.setItem("ttsvc_ab_prompts",JSON.stringify(savedPrompts)),renderPromptLib(),toast("Prompt deleted","success"))}),panel.querySelector("#ab-cv-prompt-save").addEventListener("click",async()=>{const val=promptText.value.trim();if(!val){toast("Prompt is empty","error");return}const name=panel.querySelector("#ab-cv-prompt-name").value.trim()||"Custom Prompt "+(savedPrompts.length+1);let targetIdx=parseInt(libSelect.value);!isNaN(targetIdx)&&savedPrompts[targetIdx]&&savedPrompts[targetIdx].name===name?savedPrompts[targetIdx].prompt=val:(savedPrompts.push({name,prompt:val}),targetIdx=savedPrompts.length-1),localStorage.setItem("ttsvc_ab_prompts",JSON.stringify(savedPrompts)),renderPromptLib(targetIdx),toast("Prompt preset saved","success"),typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=val);try{await fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:val})})}catch{}}),(()=>{const localSel=panel.querySelector("#ab-cv-llm-url"),globalSel=document.getElementById("llm-active-url");if(globalSel&&localSel){const currentVal=localSel.value;localSel.innerHTML=globalSel.innerHTML,localSel.value=currentVal||globalSel.value||""}})();const loadModels=()=>{const urlInput2=panel.querySelector("#ab-cv-llm-url"),sel=panel.querySelector("#ab-cv-llm-select");if(!sel||!urlInput2)return;const currentUrl=urlInput2.value.trim(),oldVal=sel.value;sel.innerHTML='',fetch("/api/conversation/llm-models"+(currentUrl?"?url="+encodeURIComponent(currentUrl):"")).then(r=>r.json()).then(d=>{if(d.models&&d.models.length){const preferred=audiobookSafeLlmModel(oldVal||defaultModel);sel.innerHTML=d.models.map(m=>``).join(""),preferred&&d.models.includes(preferred)?sel.value=preferred:oldVal&&d.models.includes(oldVal)&&!audiobookIsRouterModel(oldVal)?sel.value=oldVal:d.models.includes(defaultModel)&&(sel.value=defaultModel)}else sel.innerHTML=``}).catch(()=>{sel.innerHTML=``})};loadModels(),panel.querySelector("#ab-cv-llm-refresh").addEventListener("click",loadModels);const urlInput=panel.querySelector("#ab-cv-llm-url");urlInput&&urlInput.addEventListener("change",loadModels);const applyPromptAndRun=callback=>{const newPrompt=panel.querySelector("#ab-cv-prompt-text").value,choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt})}).finally(()=>{callback&&callback(savedChoice.url,savedChoice.model)})};if(isIdle){const hasSegs=_audiobook.segments&&_audiobook.segments.length>0;if(panel.querySelector("#ab-cv-status-msg").style.display="inline-block",hasSegs){const recastUnkBtn=panel.querySelector("#ab-cv-start-recast-unk"),recastBtn=panel.querySelector("#ab-cv-start-recast");recastUnkBtn.style.display="inline-block",recastBtn.style.display="inline-block",recastUnkBtn.addEventListener("click",()=>applyPromptAndRun(audiobookRecastUnknown)),recastBtn.addEventListener("click",async()=>{await confirmDialog("Start from scratch? This will discard the current cast for this book and rebuild the character definitions from the text.",{title:"Discard current cast?",okLabel:"Start from scratch",danger:!0})&&applyPromptAndRun(audiobookCast)}),panel.querySelector("#ab-cv-status-msg").textContent="Ready to identify characters."}else{const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn.style.display="inline-block",castBtn.addEventListener("click",()=>applyPromptAndRun(audiobookCast))}}else panel.querySelector("#ab-cv-cancel").innerHTML=' Stop Casting';const fill=panel.querySelector("#ab-cv-fill"),count=panel.querySelector("#ab-cv-count"),feed=panel.querySelector("#ab-cv-feed"),chars=panel.querySelector("#ab-cv-chars"),roster=new Map,characterRecords=new Map,identityNames=recOrSheet=>{const s=(recOrSheet==null?void 0:recOrSheet.sheet)||recOrSheet||{},out=new Set,add=(v,opts={})=>clSplitIdentityTokens(v,opts).forEach(x=>out.add(x));return add((recOrSheet==null?void 0:recOrSheet.name)||s.name),["aliases","first_name","last_name","full_name","title"].forEach(k=>add(s[k],{aliases:k==="aliases"})),[...out]};let _hlVer=0,_hlCache={ver:-1,rosterSize:-1,regex:null,byLower:new Map};const registerCharacterRecord=rec=>{if(rec!=null&&rec.name){_hlVer++;for(const n of identityNames(rec))characterRecords.set(n.toLowerCase(),rec)}},recordForName=name=>characterRecords.get(String(name||"").toLowerCase())||null,colorFor=(name,rec)=>{const key=String(name||"").toLowerCase(),stored=rec||characterRecords.get(key),color=stored?_abRecordColor(stored,name):_abDefaultCharacterColor(name);return roster.has(name)?roster.get(name).color=color:roster.set(name,{count:0,color}),roster.get(name).color},_abAvatarHtml=(name,color)=>{var _a3;const img=(_a3=recordForName(name))==null?void 0:_a3.image,c=color||colorFor(name);return img?``:`${escHtml((name||"?")[0].toUpperCase())}`},setCharacterColor=(name,color)=>{const safe=_abNormalizeColor(color,name),info=roster.get(name);info&&(info.color=safe);const key=String(name||"").toLowerCase(),rec=characterRecords.get(key);return rec&&(rec.color=safe,rec.sheet&&(rec.sheet.color=safe)),safe},highlightText2=text=>{if(!text)return"";let html=escHtml(text);if(_hlCache.ver!==_hlVer||_hlCache.rosterSize!==roster.size){const extraNames=[];new Set([...characterRecords.values()]).forEach(rec=>extraNames.push(...identityNames(rec)));const seen=new Set,names=[];for(const n of[...roster.keys(),...extraNames]){const k=n.toLowerCase();n.length<2||k==="narrator"||/^unknown|unbekannt/i.test(k)||seen.has(k)||(seen.add(k),names.push(n))}names.sort((a,b)=>b.length-a.length);const pattern=names.map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|");_hlCache={ver:_hlVer,rosterSize:roster.size,regex:pattern?new RegExp(`\\b(${pattern})\\b`,"gi"):null,byLower:new Map(names.map(n=>[n.toLowerCase(),n]))}}return _hlCache.regex&&(html=html.replace(_hlCache.regex,match=>{const canon=_hlCache.byLower.get(match.toLowerCase())||match;return`${match}`})),html},feedWrap=panel.querySelector(".ab-cv-feed-wrap"),jumpBtn=(_a2=panel.querySelector("#ab-cv-quotes"))==null?void 0:_a2.addEventListener("click",()=>audiobookToggleQuotes());{const _qb2=panel.querySelector("#ab-cv-quotes");_qb2&&(_qb2.classList.toggle("is-on",_abShowQuotes),_qb2.title=_abShowQuotes?"Hide quotation marks":"Show quotation marks")}panel.querySelector("#ab-cv-jump-btn");const _abTopbar=document.createElement("div");_abTopbar.className="ab-cv-topbar",feedWrap.insertBefore(_abTopbar,feedWrap.firstChild);const _abBar=document.createElement("div");_abBar.className="ab-char-bar",_abBar.hidden=!0,_abTopbar.appendChild(_abBar);const _abEditToolbar=document.createElement("div");_abEditToolbar.className="ab-edit-toolbar",_abEditToolbar.innerHTML=` - - `,_abTopbar.appendChild(_abEditToolbar);const _abPageNav=document.createElement("div");_abPageNav.className="ab-page-nav",_abPageNav.hidden=!0,_abPageNav.innerHTML=` - - - - `,_abTopbar.appendChild(_abPageNav);const _abFontCtl=document.createElement("div");_abFontCtl.className="ab-font-ctl",_abFontCtl.innerHTML=` - - - `,_abTopbar.appendChild(_abFontCtl);const _abReviewCtl=document.createElement("div");_abReviewCtl.className="ab-review-ctl",_abReviewCtl.id="ab-cv-review",_abReviewCtl.hidden=!0,_abReviewCtl.innerHTML=` - - review - - `,_abTopbar.insertBefore(_abReviewCtl,_abTopbar.firstChild);{const revBox=panel.querySelector("#ab-cv-review"),revCount=panel.querySelector("#ab-cv-review-count"),revLevel=panel.querySelector("#ab-cv-review-level"),uncertainRows=()=>{const limit=parseInt((revLevel==null?void 0:revLevel.value)||"96",10)||96;return[...panel.querySelectorAll(".ab-cv-row")].filter(r=>{var _a3;if((revLevel==null?void 0:revLevel.value)==="wrong")return r.dataset.mark==="wrong";if(r.dataset.mark)return!1;const c=parseInt((_a3=r.dataset.conf)!=null?_a3:"",10);return!isNaN(c)&&c{if(!revBox)return;const rows=uncertainRows(),total2=panel.querySelectorAll(".ab-cv-row[data-conf]").length;revBox.hidden=total2===0;const ok=panel.querySelectorAll('.ab-cv-row[data-mark="ok"]').length,bad=panel.querySelectorAll('.ab-cv-row[data-mark="wrong"]').length,tail=ok||bad?` \xB7 ${ok}\u2713 ${bad}\u2717`:` of ${total2}`;revCount&&(revCount.textContent=rows.length?`${rows.length} to check${tail}`:ok||bad?`all clear${tail}`:`all ${total2} look confident`)};let cursor=-1;const goTo=dir=>{var _a3;const rows=uncertainRows();if(!rows.length){toast("No lines below the review threshold","success");return}const mid=(_a3=panel.querySelector(".ab-cv-feed"))==null?void 0:_a3.getBoundingClientRect();if(mid){const firstBelow=rows.findIndex(r=>r.getBoundingClientRect().top>mid.top+4);dir>0?cursor=firstBelow===-1?rows.length-1:firstBelow-1:cursor=firstBelow===-1?rows.length:firstBelow}cursor=dir>0?Math.min(cursor+1,rows.length-1):Math.max(cursor-1,0);const row=rows[cursor];row&&(_abScrollIntoView(row,{block:"center"}),panel.querySelectorAll(".ab-cv-row.ab-review-focus").forEach(r=>r.classList.remove("ab-review-focus")),row.classList.add("ab-review-focus"),revCount&&(revCount.textContent=`${cursor+1} / ${rows.length} to check`))};(_b2=panel.querySelector("#ab-cv-review-next"))==null||_b2.addEventListener("click",()=>goTo(1)),(_c2=panel.querySelector("#ab-cv-review-prev"))==null||_c2.addEventListener("click",()=>goTo(-1)),revLevel==null||revLevel.addEventListener("change",()=>{cursor=-1,refreshReview()});const _abTopSpeakers=()=>{const c=new Map;for(const sg of _audiobook.segments||[]){if((sg==null?void 0:sg.type)!=="dialogue")continue;const n=String(sg.speaker||"").trim();!n||/^(unknown|unbekannt|narrator)$/i.test(n)||c.set(n,(c.get(n)||0)+1)}return[...c.entries()].sort((a,b)=>b[1]-a[1]).map(x=>x[0])},markChecked=(row,mark="ok")=>{const sg=row&&row.__seg;if(!sg)return;sg.confMark=mark||null,sg.confChecked=mark==="ok",mark?row.dataset.mark=mark:delete row.dataset.mark;const b=row.querySelector(".ab-cv-conf");b&&(b.outerHTML=_abConfBadgeHtml(sg));const cb=row.querySelector(".ab-cv-check");cb&&(cb.outerHTML=_abCheckBtnHtml(sg))},assignSpeaker=(row,name)=>{const sg=row&&row.__seg;if(!sg)return;sg.speaker=name,sg.type=/^narrator$/i.test(name)?"narration":"dialogue",sg.type==="narration"&&(sg.emotion="");const idx=(_audiobook.segments||[]).indexOf(sg);if(idx>=0){const{score,why}=_abSegmentConfidence(_audiobook.segments,idx),band=_abCalibrateConfidence(score);sg.conf=band.pct,sg.confLabel=band.label,sg.confRaw=score,sg.confWhy=why.join("; "),row.dataset.conf=String(sg.conf)}const spkEl=row.querySelector(".ab-cv-spk");if(spkEl){const{speakerName}=_abRowSpeaker(sg);spkEl.textContent=speakerName,spkEl.style.color=colorFor(speakerName)}row.classList.toggle("is-narr",sg.type==="narration")},persist=()=>{_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0),refreshReview()};document.addEventListener("keydown",e=>{if(!panel.isConnected||panel.offsetParent===null)return;const t=e.target;if(t&&(t.tagName==="INPUT"||t.tagName==="TEXTAREA"||t.tagName==="SELECT"||t.isContentEditable)||e.ctrlKey||e.metaKey||e.altKey)return;const focused=panel.querySelector(".ab-cv-row.ab-review-focus");if(!(e.key.startsWith("Arrow")&&!focused&&e.key!=="ArrowRight")){if(e.key==="ArrowRight"||e.key==="F2"){e.preventDefault(),goTo(e.shiftKey?-1:1);return}if(e.key==="ArrowLeft"){e.preventDefault(),goTo(-1);return}if(focused){if(e.key==="ArrowUp"||e.key==="ArrowDown"){e.preventDefault(),markChecked(focused,e.key==="ArrowUp"?"ok":"wrong"),persist(),goTo(1);return}if(/^[0-9n]$/i.test(e.key)){e.preventDefault();const name=/n/i.test(e.key)||e.key==="0"?"Narrator":_abTopSpeakers()[parseInt(e.key,10)-1];if(!name){toast(`No character #${e.key} in the cast`,"error");return}assignSpeaker(focused,name),markChecked(focused,"ok"),persist(),toast(`\u2192 ${name}`,"success"),goTo(1);return}/^u$/i.test(e.key)&&(e.preventDefault(),markChecked(focused,null),persist())}}}),panel._abRefreshReview=refreshReview,setTimeout(refreshReview,0)}{const _qb2=_abFontCtl.querySelector("#ab-cv-quotes");_qb2&&(_qb2.addEventListener("click",()=>audiobookToggleQuotes()),_qb2.classList.toggle("is-on",_abShowQuotes),_qb2.title=_abShowQuotes?"Hide quotation marks":"Show quotation marks")}const _abApplyFont=()=>{let v=1;try{v=parseFloat(localStorage.getItem("ttsvc_reh_stage_scale"))||1}catch{}feed.style.setProperty("--reh-stage-scale",String(v))},_abFontStep=d=>{let v=1;try{v=parseFloat(localStorage.getItem("ttsvc_reh_stage_scale"))||1}catch{}v=Math.round(Math.min(2,Math.max(.7,v+d))*100)/100;try{localStorage.setItem("ttsvc_reh_stage_scale",String(v))}catch{}_abApplyFont(),typeof rehApplyStageFont=="function"&&rehApplyStageFont()};_abFontCtl.querySelector(".ab-font-dec").addEventListener("click",()=>_abFontStep(-.1)),_abFontCtl.querySelector(".ab-font-inc").addEventListener("click",()=>_abFontStep(.1)),_abApplyFont();let _abCharRows=[],_abNavIdx=0,_abDetailEl=null,_abCurrentPageNum=1;const _abClearHL=()=>{feed.querySelectorAll(".ab-cv-row.ab-char-hl, .ab-cv-row.ab-char-focus").forEach(r=>r.classList.remove("ab-char-hl","ab-char-focus"))},_abCloseProfile=()=>{_abDetailEl&&(_abDetailEl.remove(),_abDetailEl=null),feed.style.display="",jumpBtn&&typeof _userScrolled!="undefined"&&(jumpBtn.hidden=!_userScrolled),_abCharRows.forEach(r=>r.classList.add("ab-char-hl"));const btn=_abBar.querySelector(".ab-char-bar-profile");btn&&(btn.innerHTML=' Profil')},_abCloseBar=()=>{_abBar.hidden=!0,_abBar.innerHTML="",_abCloseProfile(),_abClearHL(),_abCharRows=[],chars.querySelectorAll(".ab-char-item").forEach(el=>el.classList.remove("is-active")),typeof _abSyncTopbar=="function"&&_abSyncTopbar()},_abNav=(dir,pool)=>{const rows=pool||_abCharRows;if(!rows.length)return;_abNavIdx=(_abNavIdx+dir+rows.length)%rows.length,feed.querySelectorAll(".ab-cv-row.ab-char-focus").forEach(r=>r.classList.remove("ab-char-focus")),rows[_abNavIdx].classList.add("ab-char-focus"),_abScrollIntoView(rows[_abNavIdx],{block:"center"});const pos=_abBar.querySelector(".ab-char-bar-pos");pos&&(pos.textContent=_abNavIdx+1+" / "+rows.length)},_abRefreshCharacterColors=name=>{const names=name?[name]:[...roster.keys()];for(const n of names)colorFor(n);feed.querySelectorAll(".ab-cv-row").forEach(row=>{const seg=row.__seg;if(!seg)return;const speakerName=seg.type!=="dialogue"||!seg.speaker||seg.speaker.toLowerCase()==="narrator"?"Narrator":seg.speaker,c=colorFor(speakerName),spk=row.querySelector(".ab-cv-spk"),txt=row.querySelector(".ab-cv-txt");spk&&(!name||speakerName.toLowerCase()===name.toLowerCase())&&(spk.style.color=c),txt&&(txt.innerHTML=_abMarkQuotes(highlightText2(_abDisplayText(seg))))}),renderRoster();const selected=_abBar.hidden?null:_abBar.dataset.charName;if(selected){const dot=_abBar.querySelector(".ab-char-bar-dot");dot&&(dot.style.background=colorFor(selected))}},_abShowProfile=async name=>{var _a3,_b3,_c3,_d3,_e3,_f3,_g3;_abCloseProfile(),feed.style.display="none",jumpBtn&&(jumpBtn.hidden=!0);const profileBtn=_abBar.querySelector(".ab-char-bar-profile");profileBtn&&(profileBtn.innerHTML=' Skript');const title=((_a3=window.readerState)==null?void 0:_a3.title)||"";let rec=null;try{const all=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(title):typeof clGetAll=="function"?await clGetAll():[];for(const r of all||[])registerCharacterRecord(r);rec=recordForName(name)}catch{}const detail=document.createElement("div");if(detail.className="ab-char-detail-panel",_abDetailEl=detail,!rec){detail.innerHTML='
Kein Charakterblatt \u2013 zuerst \u201ECast all character roles\u201C ausf\xFChren.
',feedWrap.appendChild(detail),detail.querySelector(".ab-char-detail-back").addEventListener("click",_abCloseProfile);return}registerCharacterRecord(rec);const sh=rec.sheet||{},charColor=setCharacterColor(rec.name,rec.color||sh.color||colorFor(rec.name,rec)),accent2=clHslToHex((clNameHue(rec.name)+40)%360,58,30),tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":"Nebenfigur",voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",lineCount=_abStr(sh.line_count),pct=((_b3=rec.sheet)==null?void 0:_b3.moral_alignment_score)!=null?Math.max(0,Math.min(100,rec.sheet.moral_alignment_score)):null,arcMap={"good-to-bad":"\u2198 Entwicklung zum B\xF6sen","bad-to-good":"\u2197 Wandel zum Guten",complex:"\u2195 Komplex","stable-good":"\u2192 Stabil gut","stable-bad":"\u2192 Stabil b\xF6se",neutral:"\u2192 Neutral"},avatarHtml=rec.image?`
${escHtml(rec.name)}
`:`
${escHtml((rec.name||"?")[0].toUpperCase())}
`,field=(lbl,val,sk)=>{const v=_abStr(val);return!v&&!sk?"":`
${lbl}
${escHtml(v)}
`},srcItems=Array.isArray(rec.sources)?rec.sources:[],sourcesHtml=srcItems.length?`
- -
${srcItems.map(s=>`Seite ${s.page||"?"}`).join("")}
-
`:"";detail.innerHTML=` -
- ${avatarHtml} -
-
${escHtml(rec.name)}
- ${_abStr(sh.title)?`
${escHtml(_abStr(sh.title))}
`:""} - ${_abStr(sh.archetype)?`
${escHtml(_abStr(sh.archetype))}
`:""} -
${tierLabel}${lineCount?`${escHtml(lineCount)} lines`:""}
-
-
- - -
-
-
-
- - ${voiceId?escHtml(voiceId):'Noch keine Stimme'} - - -
- ${pct!=null?`
B\xF6se
Gut
${arcMap[sh.arc_direction||"neutral"]||"\u2192"} \xB7 ${pct>=70?"Rechtschaffen":pct<=30?"B\xF6se":"Ambivalent"} (${pct}/100)
`:""} -
-
${field("Vorname",sh.first_name,"first_name")}${field("Nachname",sh.last_name,"last_name")}${field("Geschlecht",sh.gender,"gender")}${field("Titel",sh.title,"title")}${field("Beruf / Rolle",sh.profession,"profession")}${field("Auch bekannt als",sh.aliases,"aliases")}
-
${field("K\xF6rperlich",sh.physical,"physical")}${field("Kleidung",sh.clothing,"clothing")}
-
${field("Eigenheiten",sh.mannerisms,"mannerisms")}${field("Stimme & Sprache",sh.voice_pattern,"voice_pattern")}
-
${field("Hintergrund",sh.backstory,"backstory")}${field("Motivation",sh.motivation,"motivation")}
-
${field("Fertigkeiten",sh.skills,"skills")}${field("Besondere F\xE4higkeiten",sh.capabilities,"capabilities")}
-
${field("",sh.relationships,"relationships")}
- ${sourcesHtml} -
-
`,feedWrap.appendChild(detail),detail.querySelector(".ab-char-detail-back").addEventListener("click",_abCloseProfile),(_c3=detail.querySelector(".ab-cd-color input"))==null||_c3.addEventListener("input",async e=>{var _a4;const color=setCharacterColor(rec.name,e.target.value);rec.color=color,rec.sheet||(rec.sheet={}),rec.sheet.color=color,(_a4=detail.querySelector(".lcd-header"))==null||_a4.style.setProperty("--lc1",color);const av=detail.querySelector(".lcd-avatar");av&&!av.querySelector("img")&&(av.style.background=color),_abRefreshCharacterColors(rec.name),clearTimeout(_audiobook._colorSaveTimer),_audiobook._colorSaveTimer=setTimeout(async()=>{try{typeof clPut=="function"&&await clPut(rec)}catch{}},400)}),(_d3=detail.querySelector(".ab-cd-pick"))==null||_d3.addEventListener("click",()=>{typeof _openVoicePicker=="function"&&_openVoicePicker(detail.querySelector(".lcd-voice-top"),rec,async()=>{const up=(typeof clGetAll=="function"?await clGetAll():[]).find(r=>r.id===rec.id);up&&(_abCloseProfile(),_abShowProfile(up.name))})}),(_e3=detail.querySelector(".ab-cd-auto"))==null||_e3.addEventListener("click",async()=>{if(typeof _autoAssignVoice=="function"){await _autoAssignVoice(rec);const up=(typeof clGetAll=="function"?await clGetAll():[]).find(r=>r.id===rec.id);up&&(_abCloseProfile(),_abShowProfile(up.name))}}),(_f3=detail.querySelector(".ab-cd-online"))==null||_f3.addEventListener("click",()=>{typeof _charSearchOnline=="function"&&_charSearchOnline(rec)}),(_g3=detail.querySelector(".ab-cd-refine-btn"))==null||_g3.addEventListener("click",async()=>{const name2=String(rec.name||"").trim();if(name2){if(typeof window.audiobookRecastSelectedCharacters=="function")return window.audiobookRecastSelectedCharacters([name2]);if(typeof window.csForReaderSelective=="function")return window.csForReaderSelective([name2]);toast("Character refinement is unavailable right now","error")}});const _saveTimers=new Map,editBtn=detail.querySelector(".ab-cd-edit-btn");editBtn==null||editBtn.addEventListener("click",function(){var _a4;const editing=detail.classList.toggle("ab-cd-editing");this.innerHTML=editing?'':'',this.title=editing?"Fertig":"Charakterblatt bearbeiten",detail.querySelectorAll(".ab-editable").forEach(el=>{el.contentEditable=editing?"true":"false",editing&&el.addEventListener("input",function(){const sk=el.dataset.sk;clearTimeout(_saveTimers.get(sk)),_saveTimers.set(sk,setTimeout(async()=>{rec.sheet||(rec.sheet={}),rec.sheet[sk]=el.textContent.trim(),["aliases","first_name","last_name","full_name","title"].includes(sk)&®isterCharacterRecord(rec),typeof clPut=="function"&&await clPut(rec)},900))})}),editing&&((_a4=detail.querySelector(".ab-editable"))==null||_a4.focus())}),detail.querySelectorAll(".ab-cd-src").forEach(el=>{el.addEventListener("click",()=>{const pg=parseInt(el.dataset.page,10);pg&&(typeof navTo=="function"&&navTo("s-reader"),setTimeout(()=>{var _a4,_b4;const pages=(_a4=window.readerState)==null?void 0:_a4.pages;pages&&pages.length>=pg&&((_b4=pages[pg-1])!=null&&_b4.pageDiv)?_abScrollIntoView(pages[pg-1].pageDiv,{block:"start"}):typeof toast=="function"&&toast('Buch im \u201EVorlesen"-Bereich \xF6ffnen, dann nochmal klicken',"info")},300))})})},_abSelectChar=name=>{if(!_abBar.hidden&&_abBar.dataset.charName===name){_abCloseBar();return}_abCloseBar(),chars.querySelectorAll(".ab-char-item").forEach(el=>el.classList.toggle("is-active",el.dataset.name===name)),_abCharRows=Array.from(feed.querySelectorAll(".ab-cv-row")).filter(r=>{var _a3;return((_a3=r.__seg)==null?void 0:_a3.speaker)&&r.__seg.speaker.toLowerCase()===name.toLowerCase()}),_abNavIdx=0;const color=colorFor(name);_abBar.dataset.charName=name,_abBar.hidden=!1,_abSyncTopbar(),_abBar.innerHTML=`${(name||"?")[0].toUpperCase()} -
- ${escHtml(name)} - ${_abCharRows.length} Zeilen -
-
- - 1 / ${_abCharRows.length} - -
- - - `,_abCharRows.forEach(r=>r.classList.add("ab-char-hl")),_abCharRows.length&&(_abCharRows[0].classList.add("ab-char-focus"),_abScrollIntoView(_abCharRows[0],{block:"center"})),_abBar.querySelector(".ab-char-bar-prev").addEventListener("click",()=>_abNav(-1)),_abBar.querySelector(".ab-char-bar-next").addEventListener("click",()=>_abNav(1)),_abBar.querySelector(".ab-char-bar-profile").addEventListener("click",()=>{_abDetailEl?_abCloseProfile():_abShowProfile(name)}),_abBar.querySelector(".ab-char-bar-close").addEventListener("click",_abCloseBar);const inp=_abBar.querySelector(".ab-char-bar-search");let _st=null;const _doSearch=jump=>{const q=inp.value.trim().toLowerCase(),pool=q?_abCharRows.filter(r=>{var _a3,_b3;return(((_a3=r.__seg)==null?void 0:_a3.text)||((_b3=r.querySelector(".ab-cv-txt"))==null?void 0:_b3.textContent)||"").toLowerCase().includes(q)}):_abCharRows;feed.querySelectorAll(".ab-cv-row.ab-char-focus").forEach(r=>r.classList.remove("ab-char-focus")),pool.length?(jump?_abNavIdx=(_abNavIdx+1)%pool.length:_abNavIdx=0,pool[_abNavIdx].classList.add("ab-char-focus"),_abScrollIntoView(pool[_abNavIdx],{block:"center"}),_abBar.querySelector(".ab-char-bar-pos").textContent=_abNavIdx+1+" / "+pool.length):_abBar.querySelector(".ab-char-bar-pos").textContent="0 Treffer"};inp.addEventListener("input",()=>{clearTimeout(_st),_st=setTimeout(()=>_doSearch(!1),200)}),inp.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),_doSearch(!0))})};let _abRosterFilter="",_abRosterSort="count";const renderRoster=()=>{const q=_abRosterFilter.trim().toLowerCase();let items=[...roster.entries()].filter(([n,info])=>info.count>0&&(!q||n.toLowerCase().includes(q)));if(items.sort(_abRosterSort==="alpha"?(a,b)=>a[0].localeCompare(b[0]):(a,b)=>b[1].count-a[1].count),!items.length){chars.innerHTML=q?'No matches':`
${[38,62,45,28,54,35].map(w=>`
`).join("")}
`;return}const prev=_abBar.hidden?null:_abBar.dataset.charName;chars.innerHTML=items.map(([n,info])=>{var _a3;const color=info.color||colorFor(n);return`
- ${_abAvatarHtml(n,color)} - ${escHtml(n)} - ${((_a3=roster.get(n))==null?void 0:_a3.count)||0} - -
`}).join(""),chars.querySelectorAll(".ab-char-item").forEach(item=>{item.addEventListener("click",()=>_abSelectChar(item.dataset.name))}),chars.querySelectorAll(".ab-char-alias-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),_abOpenAliasPopup(btn.dataset.name,btn)})})};(()=>{const searchInp=panel.querySelector("#ab-cv-side-search"),sortSel=panel.querySelector("#ab-cv-side-sort");try{_abRosterSort=localStorage.getItem("ttsvc_ab_side_sort")||"count"}catch{}sortSel&&(sortSel.value=_abRosterSort);let _t2=null;searchInp==null||searchInp.addEventListener("input",()=>{clearTimeout(_t2),_t2=setTimeout(()=>{_abRosterFilter=searchInp.value,renderRoster()},120)}),sortSel==null||sortSel.addEventListener("change",()=>{_abRosterSort=sortSel.value;try{localStorage.setItem("ttsvc_ab_side_sort",_abRosterSort)}catch{}renderRoster()})})();let _abAliasPopup=null;function _abCloseAliasPopup(){_abAliasPopup&&(_abAliasPopup.remove(),_abAliasPopup=null)}function _abOpenAliasPopup(name,anchorEl){_abCloseAliasPopup();const otherNames=[...roster.entries()].filter(([n,info])=>info.count>0&&n.toLowerCase()!==name.toLowerCase()&&!/^Narrator$/i.test(n)&&!/^Unknown|Unbekannt/i.test(n)).sort((a,b)=>b[1].count-a[1].count),listId="ab-alias-roster-list",el=document.createElement("div");el.className="ab-alias-popup",el.innerHTML=` -
Also known as \u2014 ${escHtml(name)}
- - - ${otherNames.map(([n,info])=>``).join("")} - - ${otherNames.length?`
Or merge with an already-found character:
-
- ${otherNames.slice(0,8).map(([n,info])=>``).join("")} -
`:""} -
- - -
`,document.body.appendChild(el);const rect=anchorEl.getBoundingClientRect();el.style.left=Math.min(rect.left,window.innerWidth-280)+"px",el.style.top=Math.min(rect.bottom+4,window.innerHeight-340)+"px";const inp=el.querySelector(".ab-alias-popup-inp");setTimeout(()=>inp.focus(),30);const otherByLower=new Map(otherNames.map(([n])=>[n.toLowerCase(),n])),doSave=async mergeName=>{var _a3,_b3;const alias=mergeName||inp.value.trim();if(!alias){_abCloseAliasPopup();return}const mergeTarget=otherByLower.get(alias.toLowerCase());if(_abCloseAliasPopup(),mergeTarget){const busy=_abShowBusyOverlay(`Merging "${mergeTarget}" into ${name}\u2026`,!0);await new Promise(r=>requestAnimationFrame(r));try{const book=((_a3=window.readerState)==null?void 0:_a3.title)||"",rec=await clUpsert(book,{name,aliases:alias});rec&&(registerCharacterRecord(rec),_hlCache&&(_hlCache.ver=-1));const{changed:n,segs}=_abMergeCharacters(mergeTarget,name);n&&(await _abPatchScatteredRows(segs,(done,total2)=>busy.setProgress(done,total2))||await _abRedrawSegmentsChunked(_abActiveSegments().arr,(done,total2)=>busy.setProgress(done,total2)),_abPersistManualEdit()),toast(n?`Merged "${mergeTarget}" into ${name} (${n} line${n!==1?"s":""})`:`"${alias}" added as an alias for ${name}`,"success")}catch(err){toast("Could not merge: "+(err.message||err),"error")}finally{busy.remove()}}else try{const book=((_b3=window.readerState)==null?void 0:_b3.title)||"",rec=await clUpsert(book,{name,aliases:alias});rec&&(registerCharacterRecord(rec),_hlCache&&(_hlCache.ver=-1)),renderRoster(),toast(`"${alias}" added as an alias for ${name}`,"success")}catch(err){toast("Could not save alias: "+(err.message||err),"error")}};el.querySelector(".ab-alias-save").addEventListener("click",()=>doSave()),el.querySelector(".ab-alias-cancel").addEventListener("click",()=>_abCloseAliasPopup()),el.querySelectorAll(".ab-alias-merge-opt").forEach(btn=>{btn.addEventListener("click",()=>doSave(btn.dataset.name))}),inp.addEventListener("keydown",e=>{e.key==="Enter"?(e.preventDefault(),doSave()):e.key==="Escape"&&(e.preventDefault(),_abCloseAliasPopup())}),setTimeout(()=>{document.addEventListener("click",function onDoc(e){!el.contains(e.target)&&e.target!==anchorEl&&(_abCloseAliasPopup(),document.removeEventListener("click",onDoc))})},0),_abAliasPopup=el}function _abMergeCharacters(fromName,intoName){const active=_abActiveSegments();if(!active.arr.length)return{changed:0,arr:active.arr,segs:[]};_abPushEditState(active.key,active.arr);let changed=0;const segs=[];for(const s of active.arr)s.type==="dialogue"&&s.speaker&&s.speaker.toLowerCase()===fromName.toLowerCase()&&(s.speaker=intoName,changed++,segs.push(s));return{changed,arr:active.arr,segs}}(async()=>{var _a3;const title=((_a3=window.readerState)==null?void 0:_a3.title)||"";try{const records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(title):typeof clGetAll=="function"?await clGetAll():[];for(const rec of records||[])if(rec!=null&&rec.name){registerCharacterRecord(rec);for(const alias of identityNames(rec)){const rosterName=[...roster.keys()].find(n=>n.toLowerCase()===alias.toLowerCase());rosterName&&setCharacterColor(rosterName,_abRecordColor(rec,rec.name))}}_abRefreshCharacterColors()}catch{}})();const MAXROWS=Number.POSITIVE_INFINITY;let _userScrolled=!1;feed.addEventListener("scroll",()=>{var _a3;const atBottom=feed.scrollTop+feed.clientHeight>=feed.scrollHeight-80;_userScrolled=!atBottom,jumpBtn&&(jumpBtn.hidden=atBottom);const labels=Array.from(feed.querySelectorAll(".ab-cv-page-label[data-page]"));let current=_abCurrentPageNum;for(const label of labels){const r=(_a3=label.closest(".ab-cv-page"))==null?void 0:_a3.getBoundingClientRect(),fr=feed.getBoundingClientRect();if(r&&r.top<=fr.top+80)current=Number(label.dataset.page)||current;else break}current&&_abSetCurrentPage(current)},{passive:!0});const _scrollToBottom=()=>{feed.scrollTop=feed.scrollHeight};jumpBtn&&jumpBtn.addEventListener("click",()=>{_userScrolled=!1,jumpBtn.hidden=!0,_scrollToBottom()});const trim=()=>{if(!_userScrolled){if(Number.isFinite(MAXROWS)){let rows=feed.querySelectorAll(".ab-cv-row, .ab-cv-note, .ab-cv-divider");for(;rows.length>MAXROWS;){const first=rows[0],page=first.closest(".ab-cv-page");first.remove(),page&&!page.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider")&&page.remove(),rows=feed.querySelectorAll(".ab-cv-row, .ab-cv-note, .ab-cv-divider")}}_scrollToBottom()}};let _abCurPage=null;const _abJumpToReaderPage=async pageNum=>{const pg=parseInt(pageNum,10);if(pg){if(typeof window.readerJumpToPage=="function"){await window.readerJumpToPage(pg);return}typeof navTo=="function"&&navTo("s-reader"),typeof window.navReaderView=="function"?window.navReaderView("main"):typeof window.showReaderView=="function"&&window.showReaderView("main"),setTimeout(async()=>{var _a3,_b3,_c3;const pageDiv=(_c3=(_b3=(_a3=window.readerState)==null?void 0:_a3.pages)==null?void 0:_b3[pg-1])==null?void 0:_c3.pageDiv;pageDiv?(typeof window.readerRenderPage=="function"&&await window.readerRenderPage(pg-1),_abScrollIntoView(pageDiv,{block:"start",inline:"nearest"})):typeof toast=="function"&&toast("Source page is not loaded in Reader yet","info")},250)}},_abNewPage=(label,pageNum)=>{_abCurPage&&!_abCurPage.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider")&&_abCurPage.remove();const p=document.createElement("div");if(p.className="ab-cv-page",label){const pageAttr=pageNum?` data-page="${escHtml(String(pageNum))}"`:"";p.innerHTML=``}return feed.appendChild(p),_abCurPage=p,pageNum&&(_abSetCurrentPage(pageNum),_abUpdatePageNav()),p},_abPage=()=>_abCurPage||_abNewPage(),_abPageNumbers=()=>{var _a3,_b3,_c3;const set=new Set,readerPages=((_a3=window.readerState)==null?void 0:_a3.mode)==="pdf"&&((_c3=(_b3=window.readerState)==null?void 0:_b3.pages)==null?void 0:_c3.length)||0;if(readerPages>0)for(let i=1;i<=readerPages;i++)set.add(i);else(_audiobook.pageMarks||[]).forEach(m=>set.add((m.page||0)+1));return feed.querySelectorAll(".ab-cv-page-label[data-page]").forEach(el=>{const n=parseInt(el.dataset.page,10);n&&set.add(n)}),[...set].sort((a,b)=>a-b)},_abSetCurrentPage=pageNum=>{const pg=parseInt(pageNum,10);if(!pg)return;_abCurrentPageNum=pg;const sel=_abPageNav.querySelector(".ab-page-select");sel&&[...sel.options].some(o=>Number(o.value)===pg)&&(sel.value=String(pg))},_abUpdatePageNav=()=>{const pages=_abPageNumbers(),sel=_abPageNav.querySelector(".ab-page-select");if(!sel||!pages.length){_abPageNav.hidden=!0,typeof _abSyncTopbar=="function"&&_abSyncTopbar();return}const old=Number(sel.value)||_abCurrentPageNum||pages[0];sel.innerHTML=pages.map(n=>``).join(""),_abPageNav.hidden=pages.length<=1,_abSetCurrentPage(pages.includes(old)?old:pages[0]),typeof _abSyncTopbar=="function"&&_abSyncTopbar()},_abScrollToCastPage=pageNum=>{const pg=parseInt(pageNum,10);if(!pg)return;let label=feed.querySelector(`.ab-cv-page-label[data-page="${CSS.escape(String(pg))}"]`);if(!label){const active=_abActiveSegments();active.arr.length&&_abRedrawSegments(active.arr),label=feed.querySelector(`.ab-cv-page-label[data-page="${CSS.escape(String(pg))}"]`)}if(pg===1){_userScrolled=!0,jumpBtn&&(jumpBtn.hidden=!1);const firstPage=(label==null?void 0:label.closest(".ab-cv-page"))||feed.firstElementChild;firstPage?_abScrollIntoView(firstPage,{block:"start"}):feed.scrollTo({top:0,behavior:"smooth"}),_abSetCurrentPage(pg);return}if(!label){toast("This cast page is not available yet. Use the book button to jump to the source page.","info"),_abSetCurrentPage(pg);return}_userScrolled=!0,jumpBtn&&(jumpBtn.hidden=!1),_abScrollIntoView(label.closest(".ab-cv-page"),{block:"start"}),_abSetCurrentPage(pg)},_abStepPage=dir=>{var _a3;const pages=_abPageNumbers();if(!pages.length)return;const current=Number((_a3=_abPageNav.querySelector(".ab-page-select"))==null?void 0:_a3.value)||_abCurrentPageNum||pages[0],idx=Math.max(0,pages.indexOf(current)),next=pages[Math.max(0,Math.min(pages.length-1,idx+dir))];_abScrollToCastPage(next)};(_d2=_abPageNav.querySelector(".ab-page-select"))==null||_d2.addEventListener("change",e=>_abScrollToCastPage(e.target.value)),(_e2=_abPageNav.querySelector(".ab-page-prev"))==null||_e2.addEventListener("click",()=>_abStepPage(-1)),(_f2=_abPageNav.querySelector(".ab-page-next"))==null||_f2.addEventListener("click",()=>_abStepPage(1)),(_g2=_abPageNav.querySelector(".ab-page-source"))==null||_g2.addEventListener("click",()=>{var _a3;const pg=Number((_a3=_abPageNav.querySelector(".ab-page-select"))==null?void 0:_a3.value)||_abCurrentPageNum;_abJumpToReaderPage(pg)});const _abUndoStack=[],_abRedoStack=[],_abHistoryLimit=40,_abSyncTopbar=()=>{const hasSelection=!_abBar.hidden,hasHistory=_abUndoStack.length>0||_abRedoStack.length>0;_abEditToolbar.hidden=!hasHistory,_abTopbar.hidden=!1},_abActiveSegments=()=>_audiobook.running&&Array.isArray(_audiobook.liveSegments)?{key:"liveSegments",arr:_audiobook.liveSegments}:Array.isArray(_audiobook.segments)?{key:"segments",arr:_audiobook.segments}:Array.isArray(_audiobook.liveSegments)?{key:"liveSegments",arr:_audiobook.liveSegments}:{key:"segments",arr:[]},_abCloneSegments=segs=>(segs||[]).map(s=>({...s})),_abSyncRosterList=segs=>{const names=[];for(const s of segs||[]){const sp=(s==null?void 0:s.type)==="dialogue"&&s.speaker?s.speaker:"";!sp||/^Narrator$/i.test(sp)||/^Unknown|Unbekannt/i.test(sp)||names.some(n=>n.toLowerCase()===sp.toLowerCase())||names.push(sp)}_audiobook.roster=names},_abPersistManualEdit=()=>{const active=_abActiveSegments();if(_audiobook.lastText){const done=_audiobook.completedChunks||active.arr.length,total2=_audiobook.completedTotal||done;setTimeout(()=>_abSaveDraft(active.arr||[],_audiobook.roster||[],_audiobook.lastText,done,total2),50)}_audiobookDebouncedSave()},_abUpdateEditButtons=()=>{const undo=_abEditToolbar.querySelector(".ab-edit-undo"),redo=_abEditToolbar.querySelector(".ab-edit-redo");undo&&(undo.disabled=!_abUndoStack.length),redo&&(redo.disabled=!_abRedoStack.length),_abSyncTopbar()},_abPushEditState=(keyOverride,arrOverride)=>{const active=arrOverride?{key:keyOverride||"segments",arr:arrOverride}:_abActiveSegments();if(!active.arr.length)return!1;for(_abUndoStack.push({key:active.key,segments:_abCloneSegments(active.arr)});_abUndoStack.length>_abHistoryLimit;)_abUndoStack.shift();return _abRedoStack.length=0,_abUpdateEditButtons(),!0},_abRowSpeaker=s=>{const isNarrator=s.type!=="dialogue"||!s.speaker||String(s.speaker).toLowerCase()==="narrator";return{isNarrator,speakerName:isNarrator?"Narrator":s.speaker}},_abIsHeadingSeg=s=>{if((s==null?void 0:s.type)==="dialogue")return!1;const t=String((s==null?void 0:s.text)||"").trim();return!t||t.length>60?!1:/^(prolog(ue)?|epilog(ue)?|kapitel|chapter|teil|buch|part|book|akt|szene|scene)\b/i.test(t)||/^\d+[\.\)]?\s*(kapitel|chapter)?$/i.test(t)?!0:t.split(/\s+/).length<=7&&!/[.!?;:,«»"]/.test(t)},_abRowFromSegment=(s,extraClass="")=>{const{isNarrator,speakerName}=_abRowSpeaker(s),c=colorFor(speakerName),row=document.createElement("div");return row.className=`ab-cv-row${extraClass?" "+extraClass:""}${isNarrator?" is-narr":""}${_abIsHeadingSeg(s)?" is-heading":""}`,row.__seg=s,row.innerHTML=` - - - - - ${escHtml(speakerName)}${s.emotion?' ('+escHtml(s.emotion)+")":""} - ${_abConfBadgeHtml(s)}${_abCheckBtnHtml(s)} - ${_abMarkQuotes(highlightText2(_abDisplayText(s)))}`,typeof s.conf=="number"&&(row.dataset.conf=String(s.conf)),s.confChecked&&(row.dataset.checked="1"),row},_abPatchScatteredRows=(segs,onProgress)=>new Promise(resolve=>{if(!segs||!segs.length){resolve(!0);return}const bySeg=new Set(segs),targets=[...feed.querySelectorAll(".ab-cv-row")].filter(r=>r.__seg&&bySeg.has(r.__seg));if(!targets.length){resolve(!1);return}let i=0;const step=()=>{const end=Math.min(i+150,targets.length);for(;i{const rows=(oldRows||[]).filter(Boolean);if(!rows.length)return!1;const parent=rows[0].parentNode;if(!parent||rows.some(r=>r.parentNode!==parent))return!1;const beforeCounts=_abRosterCountsFromSegs(rows.map(r=>r.__seg).filter(Boolean)),afterCounts=_abRosterCountsFromSegs(newSegs||[]),frag=document.createDocumentFragment();for(const seg of newSegs||[])frag.appendChild(_abRowFromSegment(seg));parent.insertBefore(frag,rows[0]);for(const row of rows)row.remove();return _abApplyRosterDelta(beforeCounts,afterCounts),_abQueueRosterRender(),_abClearHL(),_abUpdatePageNav(),!0},_abAdjacentRow=(row,dir)=>{let cur=dir<0?row==null?void 0:row.previousElementSibling:row==null?void 0:row.nextElementSibling;for(;cur&&!(cur.classList&&cur.classList.contains("ab-cv-row"));)cur=dir<0?cur.previousElementSibling:cur.nextElementSibling;return cur||null},_abStartRowEdit=row=>{if(!row||!row.__seg||row.querySelector(".ab-cv-edit-ta"))return;const s=row.__seg,txtEl=row.querySelector(".ab-cv-txt");if(!txtEl)return;const ta=document.createElement("textarea");ta.className="ab-cv-edit-ta",ta.value=s.text||"",txtEl.replaceWith(ta),ta.focus(),ta.setSelectionRange(ta.value.length,ta.value.length);const commit=save=>{if(save){const val=ta.value;val!==s.text&&(_abPushEditState(),s.text=val,_abPersistManualEdit())}ta.replaceWith(txtEl),txtEl.innerHTML=_abMarkQuotes(highlightText2(_abDisplayText(s)))};ta.addEventListener("keydown",e=>{e.key==="Enter"&&(e.ctrlKey||e.metaKey)?(e.preventDefault(),commit(!0)):e.key==="Escape"&&(e.preventDefault(),commit(!1))}),ta.addEventListener("blur",()=>commit(!0))},_abRosterCountsFromSegs=segs=>{const counts=new Map;for(const s of segs||[]){const{speakerName}=_abRowSpeaker(s||{});counts.set(speakerName,(counts.get(speakerName)||0)+1)}return counts},_abApplyRosterCounts=counts=>{for(const info of roster.values())info.count=0;for(const[name,count2]of counts||[]){const info=roster.get(name)||{count:0,color:colorFor(name)};info.count=count2,roster.set(name,info)}_audiobook.roster=[...roster.entries()].filter(([n,info])=>info.count>0&&!/^Narrator$/i.test(n)&&!/^Unknown|Unbekannt/i.test(n)).map(([n])=>n)},_abApplyRosterDelta=(beforeCounts,afterCounts)=>{const names=new Set([...(beforeCounts||new Map).keys(),...(afterCounts||new Map).keys()]);for(const name of names){const before=(beforeCounts==null?void 0:beforeCounts.get(name))||0,after=(afterCounts==null?void 0:afterCounts.get(name))||0;if(before===after)continue;const info=roster.get(name)||{count:0,color:colorFor(name)};info.count=Math.max(0,(info.count||0)+(after-before)),roster.set(name,info)}_audiobook.roster=[...roster.entries()].filter(([n,info])=>info.count>0&&!/^Narrator$/i.test(n)&&!/^Unknown|Unbekannt/i.test(n)).map(([n])=>n)};let _abRosterRenderRaf=0;const _abQueueRosterRender=()=>{_abRosterRenderRaf||(_abRosterRenderRaf=requestAnimationFrame(()=>{_abRosterRenderRaf=0,renderRoster()}))},_abRecountRoster=segs=>{_abApplyRosterCounts(_abRosterCountsFromSegs(segs||[])),_abQueueRosterRender()},_abRedrawSegments=segs=>{const selectedChar=!_abBar.hidden&&_abBar.dataset.charName?_abBar.dataset.charName:"";feed.innerHTML="",_abCurPage=null;let lastPage=null;for(const s of segs||[])s.page!=null&&s.page!==lastPage&&(_abNewPage("Page "+s.page,s.page),lastPage=s.page),_abPage().appendChild(_abRowFromSegment(s));_abRecountRoster(segs||[]),_abClearHL(),_abUpdatePageNav(),selectedChar&&(_abBar.hidden=!0,_abSelectChar(selectedChar))};let _abRedrawGen=0;const _abRedrawSegmentsChunked=(segs,onProgress,batchSize=150)=>new Promise(resolve=>{const selectedChar=!_abBar.hidden&&_abBar.dataset.charName?_abBar.dataset.charName:"",myGen=++_abRedrawGen;feed.innerHTML="",_abCurPage=null;const list=segs||[];let i=0,lastPage=null;const step=()=>{if(myGen!==_abRedrawGen){resolve();return}const end=Math.min(i+batchSize,list.length);for(;i{if(!snap)return;const restored=_abCloneSegments(snap.segments);snap.key==="liveSegments"?_audiobook.liveSegments=restored:_audiobook.segments=restored,_abRedrawSegments(restored),_abPersistManualEdit()},_abUndoEdit=()=>{const active=_abActiveSegments();!_abUndoStack.length||!active.arr.length||(_abRedoStack.push({key:active.key,segments:_abCloneSegments(active.arr)}),_abRestoreEditState(_abUndoStack.pop()),_abUpdateEditButtons())},_abRedoEdit=()=>{const active=_abActiveSegments();!_abRedoStack.length||!active.arr.length||(_abUndoStack.push({key:active.key,segments:_abCloneSegments(active.arr)}),_abRestoreEditState(_abRedoStack.pop()),_abUpdateEditButtons())};(_h2=_abEditToolbar.querySelector(".ab-edit-undo"))==null||_h2.addEventListener("click",_abUndoEdit),(_i2=_abEditToolbar.querySelector(".ab-edit-redo"))==null||_i2.addEventListener("click",_abRedoEdit),_abSyncTopbar();let assignModeSeg=null,assignModeRow=null,assignPopup=document.getElementById("ab-cv-assign-popup");if(!assignPopup){assignPopup=document.createElement("div"),assignPopup.id="ab-cv-assign-popup",assignPopup.style.cssText="position:fixed; z-index:2001; display:none; background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:0 10px 25px rgba(0,0,0,0.4); padding:10px; width:220px; max-height:320px; flex-direction:column; gap:8px;";const header=document.createElement("div");header.style.cssText="display:flex; justify-content:space-between; align-items:center; margin-bottom:-4px; margin-top:-4px;",header.innerHTML='Assign to',assignPopup.appendChild(header);const inp=document.createElement("input");inp.type="text",inp.placeholder="Search or add character...",inp.style.cssText="width:100%; padding:6px; font-size:13px; border:1px solid var(--border); border-radius:4px; background:var(--bg); color:var(--text);",assignPopup.appendChild(inp);const list=document.createElement("div");list.className="ab-cv-popup-list",list.style.cssText="display:flex; flex-direction:column; gap:2px; overflow-y:auto; max-height:220px; margin-right:-4px; padding-right:4px;",assignPopup.appendChild(list),document.body.appendChild(assignPopup),header.querySelector(".mdi-close").addEventListener("click",()=>{var _a3;return(_a3=assignPopup._close)==null?void 0:_a3.call(assignPopup)}),document.addEventListener("mousedown",e=>{var _a3;assignPopup.style.display!=="none"&&!assignPopup.contains(e.target)&&!e.target.closest(".ab-cv-spk")&&!e.target.closest(".ab-cv-txt")&&((_a3=assignPopup._close)==null||_a3.call(assignPopup))}),document.addEventListener("keydown",e=>{var _a3;e.key==="Escape"&&assignPopup.style.display!=="none"&&((_a3=assignPopup._close)==null||_a3.call(assignPopup))}),inp.addEventListener("keydown",e=>{var _a3,_b3;if(e.key==="Enter"){const addBtn=assignPopup.querySelector(".ab-cv-popup-add-btn"),visibleBtns=Array.from(assignPopup.querySelectorAll(".ab-cv-popup-list > div[data-name]")).filter(b=>b.style.display!=="none");if(addBtn&&addBtn.style.display!=="none")addBtn.click();else if(visibleBtns.length===1)visibleBtns[0].click();else{const val=inp.value.trim();val&&((_a3=assignPopup._assignName)==null||_a3.call(assignPopup,val))}}else e.key==="Escape"&&((_b3=assignPopup._close)==null||_b3.call(assignPopup))}),inp.addEventListener("input",()=>{const val=inp.value.trim().toLowerCase(),listItems=assignPopup.querySelectorAll(".ab-cv-popup-list > div[data-name]");let exactMatch=!1;listItems.forEach(item=>{const name=item.dataset.name.toLowerCase();name===val&&(exactMatch=!0),name.includes(val)?item.style.display="flex":item.style.display="none"});let addBtn=assignPopup.querySelector(".ab-cv-popup-add-btn");val&&!exactMatch&&val!=="narrator"?(addBtn||(addBtn=document.createElement("div"),addBtn.className="ab-cv-popup-add-btn",addBtn.style.cssText="padding:6px 8px; font-size:12px; cursor:pointer; border-radius:4px; display:flex; align-items:center; gap:6px; transition:background 0.1s; font-weight:600; color:var(--primary); border-top:1px solid var(--border); margin-top:4px; padding-top:8px;",addBtn.onmouseover=()=>addBtn.style.background="var(--panel)",addBtn.onmouseout=()=>addBtn.style.background="transparent"),addBtn.innerHTML=`+ Add "${escHtml(inp.value.trim())}"`,addBtn.onclick=()=>{var _a3;return(_a3=assignPopup._assignName)==null?void 0:_a3.call(assignPopup,inp.value.trim())},addBtn.style.display="flex",assignPopup.querySelector(".ab-cv-popup-list").appendChild(addBtn)):addBtn&&(addBtn.style.display="none")})}function closeAssignPopup(){assignPopup.style.display="none",assignModeRow&&assignModeRow.classList.remove("is-assigning"),assignModeSeg=null,assignModeRow=null}function assignName(name){if(!assignModeSeg)return;const active=_abActiveSegments();_abPushEditState(active.key,active.arr);const isNarrator=name.toLowerCase()==="narrator",_oldSpk=assignModeSeg.speaker;if(_oldSpk&&roster.has(_oldSpk)){const _old=roster.get(_oldSpk);_old.count>0&&_old.count--}assignModeSeg.speaker=isNarrator?"Narrator":name,assignModeSeg.type=isNarrator?"narration":"dialogue",isNarrator&&(assignModeSeg.emotion=""),assignModeRow.classList.remove("is-assigning");const speakerName=isNarrator?"Narrator":name,c=colorFor(speakerName);roster.has(speakerName)||roster.set(speakerName,{count:0,color:c}),roster.get(speakerName).count++,assignModeRow.className="ab-cv-row"+(isNarrator?" is-narr":""),assignModeRow.querySelector(".ab-cv-spk").innerHTML=`${escHtml(speakerName)}${assignModeSeg.emotion?' ('+escHtml(assignModeSeg.emotion)+")":""}`,assignModeRow.querySelector(".ab-cv-spk").style.color=c,assignModeRow.querySelector(".ab-cv-spk").title="Click to assign character",_abRecountRoster(_abActiveSegments().arr),toast(`Assigned to ${isNarrator?"Narrator":name}`,"success"),closeAssignPopup(),_abPersistManualEdit()}function _abOpenAssignPopup(row,anchorEl,prefill){var _a3;if(!row||!row.__seg||row.classList.contains("is-processing"))return;assignModeRow&&assignModeRow.classList.remove("is-assigning"),assignModeSeg=row.__seg,assignModeRow=row,row.classList.add("is-assigning"),assignPopup._assignName=assignName,assignPopup._close=closeAssignPopup,window.getSelection().removeAllRanges(),assignPopup.style.display="flex";const rect=anchorEl.getBoundingClientRect(),top=Math.min(rect.bottom+4,window.innerHeight-300);assignPopup.style.top=top+"px",assignPopup.style.left=rect.left+"px";const list=assignPopup.querySelector(".ab-cv-popup-list");list.innerHTML="";const narrBtn=document.createElement("div");narrBtn.dataset.name="Narrator",narrBtn.style.cssText="padding:6px 8px; font-size:12px; cursor:pointer; border-radius:4px; display:flex; align-items:center; gap:6px; transition:background 0.1s; font-weight:600; color:var(--subtext); border-bottom:1px solid var(--border); margin-bottom:4px; padding-bottom:8px;",narrBtn.innerHTML='\u{1F4D6} Narrator',narrBtn.onmouseover=()=>narrBtn.style.background="var(--panel)",narrBtn.onmouseout=()=>narrBtn.style.background="transparent",narrBtn.onclick=()=>assignName("Narrator"),list.appendChild(narrBtn);const items=[...roster.entries()].sort((a,b)=>b[1].count-a[1].count);for(const[n,info]of items){const btn=document.createElement("div");btn.dataset.name=n,btn.style.cssText="padding:6px 8px; font-size:12px; cursor:pointer; border-radius:4px; display:flex; align-items:center; gap:6px; transition:background 0.1s; font-weight:600; color:var(--text);";const img=(_a3=recordForName(n))==null?void 0:_a3.image;btn.innerHTML=(img?``:``)+` ${escHtml(n)}`,btn.onmouseover=()=>btn.style.background="var(--panel)",btn.onmouseout=()=>btn.style.background="transparent",btn.onclick=()=>assignName(n),list.appendChild(btn)}const inp=assignPopup.querySelector("input");inp.value=prefill||"",setTimeout(()=>{inp.focus(),prefill&&inp.dispatchEvent(new Event("input"))},50)}const _abIsUnknownSeg=s=>!(s!=null&&s.speaker)||/^Unknown|Unbekannt/i.test(s.speaker),_abIsNarrSeg=s=>(s==null?void 0:s.type)!=="dialogue"||!s.speaker||/^Narrator$/i.test(s.speaker),_abMergedSegment=(a,b)=>{var _a3;let base=a;(_abIsUnknownSeg(a)&&!_abIsUnknownSeg(b)||_abIsNarrSeg(a)&&!_abIsNarrSeg(b))&&(base=b);const type=_abIsNarrSeg(base)?"narration":"dialogue";return{speaker:type==="narration"?"Narrator":base.speaker||"Unknown",type,emotion:type==="dialogue"&&(base.emotion||a.emotion||b.emotion)||"",text:audiobookJoinSegmentText(a.text||"",b.text||""),page:(_a3=a.page)!=null?_a3:b.page}},_abMergeByRow=(row,dir)=>{const active=_abActiveSegments(),idx=active.arr.indexOf(row==null?void 0:row.__seg);if(idx<0)return;const leftIdx=dir<0?idx-1:idx,rightIdx=dir<0?idx:idx+1;if(leftIdx<0||rightIdx>=active.arr.length){toast("No adjacent segment to merge","info");return}const leftRow=dir<0?_abAdjacentRow(row,-1):row,rightRow=dir<0?row:_abAdjacentRow(row,1);_abPushEditState(active.key,active.arr);const merged=_abMergedSegment(active.arr[leftIdx],active.arr[rightIdx]);active.arr.splice(leftIdx,2,merged),active.key==="segments"?_audiobook.segments=active.arr:_audiobook.liveSegments=active.arr,!leftRow||!rightRow||leftRow.parentNode!==rightRow.parentNode?_abRedrawSegmentsChunked(active.arr):_abPatchRowRange([leftRow,rightRow],[merged],active.arr)||_abRedrawSegmentsChunked(active.arr),_abPersistManualEdit(),toast("Segments merged","success")};feed.addEventListener("click",e=>{var _a3;const pageLabel=e.target.closest(".ab-cv-page-label[data-page]");if(pageLabel){e.preventDefault(),e.stopPropagation(),_abJumpToReaderPage(pageLabel.dataset.page);return}const editBtn=e.target.closest(".ab-cv-edit-text");if(editBtn){e.preventDefault(),e.stopPropagation(),_abStartRowEdit(editBtn.closest(".ab-cv-row"));return}const chkBtn=e.target.closest(".ab-cv-check");if(chkBtn){e.preventDefault(),e.stopPropagation();const row=chkBtn.closest(".ab-cv-row"),seg=row&&row.__seg;if(seg){seg.confChecked=!seg.confChecked,seg.confChecked?row.dataset.checked="1":delete row.dataset.checked;const badge=row.querySelector(".ab-cv-conf");badge&&(badge.outerHTML=_abConfBadgeHtml(seg)),chkBtn.outerHTML=_abCheckBtnHtml(seg);const root=document.querySelector(".ab-castpanel-inline");root&&typeof root._abRefreshReview=="function"&&root._abRefreshReview(),_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0)}return}const mergeBtn=e.target.closest(".ab-cv-row-tool");if(mergeBtn){e.preventDefault(),e.stopPropagation(),_abMergeByRow(mergeBtn.closest(".ab-cv-row"),mergeBtn.classList.contains("ab-cv-merge-prev")?-1:1);return}const spk=e.target.closest(".ab-cv-spk");if(spk){_abOpenAssignPopup(spk.closest(".ab-cv-row"),spk,"");return}if(_abJustHandledSelection){_abJustHandledSelection=!1;return}const txtEl=e.target.closest(".ab-cv-txt");if(txtEl&&window.getSelection().isCollapsed){if(!assignModeRow)return;const row=txtEl.closest(".ab-cv-row"),nameHit=e.target.closest(".ab-name-hit"),clickedWord=nameHit?nameHit.dataset.name||nameHit.textContent.trim():(_a3=_abWordRangeAtPoint(e.clientX,e.clientY))==null?void 0:_a3.word;if(assignModeRow!==row){if(clickedWord){const inp=assignPopup.querySelector("input");inp.value=clickedWord,inp.dispatchEvent(new Event("input")),inp.focus()}return}if(nameHit)_abOpenAssignPopup(row,nameHit,clickedWord);else{const hit=_abWordRangeAtPoint(e.clientX,e.clientY);if(hit){const rect=hit.range.getBoundingClientRect();_abOpenAssignPopup(row,{getBoundingClientRect:()=>rect},hit.word)}}}});let _abJustHandledSelection=!1,_abHoverHL=document.getElementById("ab-word-hover");_abHoverHL||(_abHoverHL=document.createElement("div"),_abHoverHL.id="ab-word-hover",_abHoverHL.className="ab-word-hover",_abHoverHL.hidden=!0,document.body.appendChild(_abHoverHL));let _abHoverRaf=0,_abHoverPt=null;const _abHideHoverHL=()=>{_abHoverHL.hidden=!0};feed.addEventListener("mousemove",e=>{_abHoverPt={x:e.clientX,y:e.clientY,target:e.target},!_abHoverRaf&&(_abHoverRaf=requestAnimationFrame(()=>{var _a3;_abHoverRaf=0;const pt=_abHoverPt;if(!pt||!((_a3=pt.target)!=null&&_a3.closest))return;if(!pt.target.closest(".ab-cv-txt")||pt.target.closest(".ab-name-hit")||feed.querySelector(".ab-cv-edit-ta")){_abHideHoverHL();return}const hit=_abWordRangeAtPoint(pt.x,pt.y);if(!hit){_abHideHoverHL();return}const r=hit.range.getBoundingClientRect();if(!r.width){_abHideHoverHL();return}_abHoverHL.style.left=r.left-2+"px",_abHoverHL.style.top=r.top-1+"px",_abHoverHL.style.width=r.width+4+"px",_abHoverHL.style.height=r.height+2+"px",_abHoverHL.hidden=!1}))}),feed.addEventListener("mouseleave",_abHideHoverHL),feed.addEventListener("scroll",_abHideHoverHL,{passive:!0}),feed.addEventListener("dblclick",e=>{const hit=e.target.closest(".ab-name-hit");if(!hit)return;const row=hit.closest(".ab-cv-row"),name=hit.dataset.name;row&&name&&(_abOpenAssignPopup(row,hit,""),assignName(name))});let splitBtn=document.getElementById("ab-cv-split-btn");splitBtn||(splitBtn=document.createElement("button"),splitBtn.id="ab-cv-split-btn",splitBtn.className="btn-secondary btn-sm",splitBtn.innerHTML=' Split text to Unknown Speaker',splitBtn.style.cssText="position:fixed; z-index:2000; display:none; background:var(--accent); color:#fff; border:none; box-shadow:0 4px 12px rgba(0,0,0,0.3);",document.body.appendChild(splitBtn));let currentSplitState=null;const _abOffsetInEl=(el,node,offset)=>{const r=document.createRange();return r.selectNodeContents(el),r.setEnd(node,offset),r.toString().length};return document.addEventListener("selectionchange",()=>{if(!feed)return;const sel=window.getSelection();if(sel.isCollapsed||!feed.contains(sel.anchorNode)){splitBtn&&(splitBtn.style.display="none"),currentSplitState=null;return}const txtSpan=sel.anchorNode.nodeType===3?sel.anchorNode.parentNode.closest(".ab-cv-txt"):sel.anchorNode.closest(".ab-cv-txt");if(!txtSpan){splitBtn.style.display="none";return}const row=txtSpan.closest(".ab-cv-row");if(!row||!row.__seg)return;const rawText=sel.toString(),text=rawText.trim();if(text.length>0){if(assignModeSeg&&text.length<40&&!text.includes(` -`)){splitBtn.style.display="none";return}const range=sel.getRangeAt(0),rect=range.getBoundingClientRect();splitBtn.style.display="block",splitBtn.style.top=rect.bottom+8+"px",splitBtn.style.left=Math.max(10,rect.left+rect.width/2-100)+"px";const startOffset=_abOffsetInEl(txtSpan,range.startContainer,range.startOffset),endOffset=_abOffsetInEl(txtSpan,range.endContainer,range.endOffset);currentSplitState={row,text,rawText,seg:row.__seg,startOffset,endOffset}}else splitBtn.style.display="none",currentSplitState=null}),splitBtn.addEventListener("click",()=>{if(!currentSplitState)return;const{row,seg,startOffset,endOffset}=currentSplitState,fullText=seg.text||"",before=fullText.slice(0,startOffset),selectedText=fullText.slice(startOffset,endOffset),after=fullText.slice(endOffset);if(!selectedText)return;let arr=_audiobook.segments||[],globalIdx=arr.indexOf(seg);globalIdx===-1&&Array.isArray(_audiobook.liveSegments)&&(arr=_audiobook.liveSegments,globalIdx=arr.indexOf(seg)),globalIdx!==-1&&_abPushEditState(arr===_audiobook.liveSegments?"liveSegments":"segments",arr);const newSegs=[];if(before.length&&newSegs.push({speaker:seg.speaker,type:seg.type,emotion:seg.emotion,text:before,page:seg.page}),newSegs.push({speaker:"Unknown",type:"dialogue",emotion:"",text:selectedText,page:seg.page}),after.length&&newSegs.push({speaker:seg.speaker,type:seg.type,emotion:seg.emotion,text:after,page:seg.page}),globalIdx!==-1)arr.splice(globalIdx,1,...newSegs),_abPatchRowRange([row],newSegs,arr)||_abRedrawSegments(arr),_abPersistManualEdit();else{console.warn("[audiobook] split during casting: DOM-only, segment not yet in array");const frag=document.createDocumentFragment();for(const s of newSegs)frag.appendChild(_abRowFromSegment(s));row.parentNode.insertBefore(frag,row),row.remove()}splitBtn.style.display="none",window.getSelection().removeAllRanges(),toast("Segment split! You can now assign a character to the Unknown block.","success")}),feed.addEventListener("mouseup",()=>{const sel=window.getSelection();if(sel.isCollapsed||!feed.contains(sel.anchorNode))return;const text=sel.toString().trim();!text||text.length>=40||text.includes(` -`)||/^[»„“"'‘]/.test(text)||assignModeSeg&&(_abJustHandledSelection=!0,assignName(text),sel.removeAllRanges())}),chars.addEventListener("click",e=>{const chip=e.target.closest(".ab-chip");if(chip){let name="";for(const n of chip.childNodes)n.nodeType===3&&(name+=n.nodeValue);if(name=name.trim(),assignModeSeg)name&&assignName(name);else if(name){const rows=Array.from(feed.querySelectorAll(".ab-cv-row")).filter(r=>r.__seg&&r.__seg.speaker===name);if(!rows.length)return;const currentY=feed.scrollTop;let target=rows.find(r=>r.offsetTop-feed.offsetTop>currentY+10);target||(target=rows[0]),_abScrollIntoView(target,{block:"center"}),target.style.transition="background 0.3s",target.style.background="var(--accent-hover, rgba(100, 150, 255, 0.2))",setTimeout(()=>{target.parentNode&&(target.style.background="")},1e3)}}}),{recountRoster(allSegs){_abRecountRoster(allSegs||[])},rebuild(segs){var _a3,_b3;this.clearProcessing(),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),_abRedrawSegmentsChunked(segs||[])},update(done){const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!1);const pct=Math.round(done/total*100);fill&&(fill.style.width=pct+"%"),count&&(count.hidden=!1,count.textContent=`passage ${done} / ${total}`);const fillText=panel.querySelector("#ab-cv-fill-text");fillText&&(fillText.textContent=`${pct}% (Passage ${done} of ${total})`)},processing(text){this._procRow&&this._procRow.remove(),this._thinkRaw="",this._thinkDone=!1,this._procRow=document.createElement("div"),this._procRow.className="ab-cv-row is-processing ab-cv-llm-row";const isLong=text.length>160,preview=escHtml(text.slice(0,160))+(isLong?"\u2026":"");this._procRow.innerHTML=` -
- LLM Reading\u2026 - -
-
${preview}
- - `,this._procRow.querySelector(".ab-cv-expand-btn").addEventListener("click",function(){const split=this.closest(".ab-cv-llm-row").querySelector(".ab-cv-llm-split"),open=this.getAttribute("aria-expanded")==="true";this.setAttribute("aria-expanded",String(!open)),split.hidden=open;const icon=this.querySelector("span");icon&&(icon.className=open?"mdi mdi-chevron-right":"mdi mdi-chevron-down")}),_abPage().appendChild(this._procRow),trim()},thinking(delta){if(!this._procRow||!delta||this._thinkDone)return;this._thinkRaw=(this._thinkRaw||"")+delta;const think=this._procRow.querySelector(".ab-cv-think"),label=this._procRow.querySelector(".ab-cv-think-label");if(!think)return;const openAt=this._thinkRaw.indexOf("");let shown,isReal=!1,closeAt=-1;if(openAt>=0){const afterOpen=this._thinkRaw.slice(openAt+7);closeAt=afterOpen.indexOf(""),shown=closeAt>=0?afterOpen.slice(0,closeAt):afterOpen,isReal=!0}else shown=this._thinkRaw;label&&(label.innerHTML=isReal?' LLM Thinking\u2026':' Live Output (raw \u2014 no reasoning exposed)'),think.textContent=shown.trim().slice(-2e4),think.scrollTop=think.scrollHeight,closeAt>=0&&(this._thinkDone=!0)},clearProcessing(){this._procRow&&(this._procRow.remove(),this._procRow=null)},addSegments(segs){var _a3,_b3;this.clearProcessing(),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const makeRow=s=>{const{speakerName}=_abRowSpeaker(s);return colorFor(speakerName),roster.get(speakerName).count++,_abRowFromSegment(s)},CHUNK=80,first=segs.slice(0,CHUNK),frag0=document.createDocumentFragment();if(first.forEach(s=>frag0.appendChild(makeRow(s))),_abPage().appendChild(frag0),renderRoster(),segs.length>CHUNK){let i=CHUNK;const next=()=>{if(i>=segs.length){trim(),renderRoster();return}const batch=document.createDocumentFragment(),end=Math.min(i+CHUNK,segs.length);for(;i=0?prevIdx+1:0),segsAll=_audiobook.segments||[],gapTo=Math.min(segsAll.length-1,nextIdx>=0?nextIdx-1:segsAll.length-1),gapCount=Math.max(0,gapTo-gapFrom+1);r.innerHTML=`\u22EF ${gapCount>0?gapCount+" line"+(gapCount!==1?"s":"")+" hidden \u2014 ":""}click to expand`,r.title="Click to reveal the text between these passages",r.addEventListener("click",()=>{const gap=segsAll.slice(gapFrom,gapTo+1);if(!gap.length){r.remove();return}const PEEK=20,show=gap.slice(0,PEEK),rest=gap.slice(PEEK),frag=document.createDocumentFragment();for(const s of show){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`${escHtml(spk)}${s.emotion?' ('+escHtml(s.emotion)+")":""}${_abMarkQuotes(highlightText2(_abDisplayText(s)))}`,frag.appendChild(row)}if(rest.length){const next=document.createElement("div");next.className="ab-cv-divider ab-cv-divider-expand",next.innerHTML=`\u22EF ${rest.length} line${rest.length!==1?"s":""} hidden \u2014 click to expand`,next.title="Click to reveal more";const restSegs=rest;next.addEventListener("click",function(){const f2=document.createDocumentFragment();for(const s of restSegs){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`${escHtml(spk)}${s.emotion?' ('+escHtml(s.emotion)+")":""}${_abMarkQuotes(highlightText2(_abDisplayText(s)))}`,f2.appendChild(row)}next.replaceWith(f2)}),frag.appendChild(next)}r.replaceWith(frag)}),_abPage().appendChild(r),trim()},pagemark(pageNum){this.clearProcessing(),_abNewPage("Page "+pageNum,pageNum),trim()},complete(summary,onOpen,onRecast,onRecastUnknown){var _a3,_b3;_abHideCastingControl(),count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0);const completedSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length?_audiobook.segments:typeof allSegments!="undefined"&&allSegments.length?allSegments:null;completedSegments&&_abRedrawSegmentsChunked(completedSegments);const cancelBtn=panel.querySelector("#ab-cv-cancel");cancelBtn&&(cancelBtn.hidden=!0);const foot=panel.querySelector("#ab-cv-foot");foot.hidden=!1;const sideEl=panel.querySelector(".ab-cv-side");if(sideEl){let doneEl=sideEl.querySelector(".ab-cv-side-done");doneEl||(doneEl=document.createElement("div"),doneEl.className="ab-cv-side-done",sideEl.appendChild(doneEl)),doneEl.innerHTML=` ${escHtml(summary)}`}const continueBtnHtml=_audiobook.completedChunks>0&&_audiobook.completedTotal>0&&_audiobook.completedChunks<_audiobook.completedTotal?``:"";foot.innerHTML=`${continueBtnHtml}`;const verificationPrompt=`Du bist ein Qualit\xE4tspr\xFCfer f\xFCr die Sprecherzuordnung eines bereits analysierten deutschen H\xF6rbuch-Textes. Eine erste KI hat jedem Segment bereits einen Sprecher zugewiesen ('Narrator' oder einen Charakternamen, ggf. 'Unknown'). Du bekommst diese Zuweisung NICHT \u2014 du siehst nur den Text und musst selbst neu urteilen, ob die Zeile zu Narration oder zu gesprochener Rede eines Charakters geh\xF6rt, und falls Dialog: zu welchem. Das ist eine Gegenprobe, keine Neuklassifizierung von Grund auf: dein Job ist nicht "wer k\xF6nnte das gesagt haben", sondern "ist diese Zeile wirklich Narration, oder wurde hier gesprochene Rede in den Erz\xE4hltext hineingezogen?". - -WORAUF DU PR\xDCFST \u2014 IN DIESER REIHENFOLGE (h\xF6chste Priorit\xE4t zuerst): -1. ZUERST alle 'Unknown'-Zeilen: L\xF6se den Sprecher \xFCber Kontext auf (Inquit-Formeln, Ping-Pong-Wechsel in Zwiegespr\xE4chen, Adressaten-Bezug, zuletzt genannte Person). 'Unknown' bleibt nur, wenn wirklich keine Ableitung m\xF6glich ist. Das ist die dringendste Kategorie \u2014 eine Zeile ohne jeden Sprecher ist schlimmer als eine falsch zugeordnete. -2. DANACH Zeilenketten mit demselben Sprecher in Folge (2 oder mehr aufeinanderfolgende Zeilen \u2014 egal ob 'Narrator' oder ein Charakter): Das ist die zweith\xE4ufigste Fehlerquelle. Pr\xFCfe jede Zeile in einer solchen Kette einzeln: Ist das wirklich durchgehend derselbe Sprecher, oder wurde eine Sprecherwechsel-Zeile (z.B. eine Antwort einer anderen Figur, oder gesprochene Rede ohne erhaltene Anf\xFChrungszeichen) f\xE4lschlich in die Kette hineingezogen? Pr\xFCfe besonders bei 'Narrator'-Ketten: Ist wirklich jede Zeile neutrale Erz\xE4hlerbeschreibung, oder ist tats\xE4chlich eine Aussage dabei, die ein Charakter so gesagt haben k\xF6nnte \u2014 nur ohne erhaltene Anf\xFChrungszeichen (typisch bei PDF/OCR-Extraktion)? Pr\xFCfe Tonfall, Wortwahl und Perspektive: Klingt eine der Zeilen wie eine direkte Aussage/Reaktion einer Person in der Szene (Ich-Form, Anrede, Ausruf, Frage), nicht wie neutrale Erz\xE4hlerbeschreibung? Dann ist SIE Dialog, nicht Narration \u2014 auch wenn keine \xBB \xAB vorhanden sind, und auch wenn die Zeilen davor/danach in derselben Kette echt narrativ sind. -3. ZULETZT ein allgemeiner Plausibilit\xE4ts-Check bei allen \xFCbrigen, bereits vermuteten Sprechern: Passt Tonfall/Wortwahl der Zeile zu dem, was \xFCber diese Figur im bisherigen Text bekannt ist (Sprechweise, Haltung, Beziehung zu anderen Figuren)? Wenn eine Zeile im Kontext eindeutig zu einer ANDEREN, im Text erkennbaren Person passt, korrigiere sie dorthin. -4. Echte Narration NICHT anfassen: Reine Beschreibung, Handlung, \xDCberg\xE4nge, Kapitelanf\xE4nge bleiben 'Narrator'. Nur weil eine Figur erw\xE4hnt wird, wird der Satz nicht zu ihrem Dialog. - -DEDUKTIONS-WERKZEUGE f\xFCr die Sprecherzuordnung: -- Doppelpunkt-Regel: Endet der Satz davor mit ":", spricht dessen Subjekt das Folgende. -- Nachgestellte Zuordnung: Folgt einer als 'Narrator' markierten Zeile direkt eine kurze Inquit-Formel (Sprechverb + Name/Pronomen, z.B. "entgegnete Oberst von Blautann.", ", meldete sich Lysandra zu Wort.", "murmelte er."), dann war die VORHERIGE Zeile in Wahrheit das Zitat dieser Person \u2014 auch ohne Anf\xFChrungszeichen. Das ist der h\xE4ufigste Fehler der ersten Zuordnung: gesprochene Rede wird f\xE4lschlich als Narration markiert, weil die Anf\xFChrungszeichen bei der Extraktion verloren gingen. -- Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einer fraglichen Zeile AKTIV HANDELT (aufblickt, sich umdreht, den Kopf sch\xFCttelt), ist der wahrscheinlichste Sprecher dieser Zeile. -- ZUH\xD6RER-AUSSCHLUSS (negative Evidenz): Wahrnehmungs- und Reaktionsverben kennzeichnen den ZUH\xD6RER, nicht den Sprecher ("Marcian sp\xFCrte ihre Tr\xE4nen", "Marcian h\xF6rte dar\xFCber hinweg" \u2192 Marcian h\xF6rt zu, jemand anderes spricht). Ebenso ist eine Person, die im Zitat per Vokativ ANGEREDET wird ("..., Marcian."), nicht der Sprecher. Pr\xFCfe diese Ausschl\xFCsse ZUERST, bevor du einen Namen aus dem Nachbarsatz \xFCbernimmst \u2014 das ist die h\xE4ufigste Fehlerquelle bei nicht zugeordneten Zeilen. -- Pronomen-Inquit bei geteilten Zitaten: Eine Zeile wie ", antwortete er knapp." geh\xF6rt zum vorherigen Zitat; l\xF6se das Pronomen auf die zuletzt genannte Person passenden Geschlechts VOR dem Zitat auf (er \u2192 letzter Mann, sie \u2192 letzte Frau). -- Ping-Pong-Prinzip: Zwei Personen im Gespr\xE4ch wechseln sich strikt ab, auch ohne Tags \u2014 verfolge die Kette zur letzten eindeutigen Nennung zur\xFCck. -- Pronomen-Aufl\xF6sung: er/sie/es meint die zuletzt genannte Person passenden Geschlechts. -- Eine Rollenbezeichnung ('Ork', 'Der Fremde', 'W\xE4chter') ist nur dann ein g\xFCltiger Sprecher, wenn die Person im Text keinen Eigennamen hat; sonst immer den Eigennamen verwenden. - -F\xDCR JEDES SEGMENT AUSGABE: -- speaker: 'Narrator' f\xFCr Narration, oder EXAKT der Name des Charakters (nutze den in der bekannten Charakterliste gef\xFChrten Namen, nicht einen Spitznamen/Alias, falls der Kontext eindeutig zuordnet). -- type: 'narration' oder 'dialogue' -- text: EXAKT der WORTW\xD6RTLICHE Originaltext \u2014 KEINE \xC4nderungen, KEINE Auslassungen, KEINE Erg\xE4nzungen. -- emotion: Bei Dialogen 1-2 deutsche W\xF6rter f\xFCr den Tonfall. Bei Narration leer (''). - -ABSOLUTE REGELN: -- Alle Segmente zusammen M\xDCSSEN den Originaltext exakt, l\xFCckenlos und wortgetreu rekonstruieren. -- Erfinde NIEMALS Text. Lasse NIEMALS W\xF6rter weg. F\xFCge NIEMALS etwas hinzu. -- Mische NIEMALS Narration und Dialog in einem Segment. -- Sei konservativ: \xE4ndere eine Zeile nur, wenn du nach dieser Pr\xFCfung wirklich zu einem ANDEREN Ergebnis kommst als naheliegend w\xE4re \u2014 nicht jede Zeile muss sich \xE4ndern.`;window._abVerificationPrompt=verificationPrompt;const runVerificationPass=()=>{const choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);closePanel(),audiobookRecastUnknown(savedChoice.url,savedChoice.model,{prompt:verificationPrompt,repairNarration:!0})},runConsistencyPass=async()=>{const active=_abActiveSegments(),segs=active.arr;if(!segs.length){toast("Nothing cast yet to check","error");return}const byName=new Map;segs.forEach((s,idx)=>{s.type!=="dialogue"||!s.speaker||/^Narrator$/i.test(s.speaker)||/^Unknown|Unbekannt/i.test(s.speaker)||(byName.has(s.speaker)||byName.set(s.speaker,[]),byName.get(s.speaker).push(idx))});const MIN_LINES=4,MAX_LINES_PER_CALL=60,candidates=[...byName.entries()].filter(([,idxs])=>idxs.length>=MIN_LINES);if(!candidates.length){toast("No characters with enough lines yet to check for consistency","error");return}const choice2=audiobookCurrentCastLlm(panel),savedChoice2=audiobookSaveLlmChoice(choice2.url,choice2.model);closePanel(),_abPushEditState(active.key,segs);const busy=_abShowBusyOverlay(`Checking voice consistency for ${candidates.length} character${candidates.length!==1?"s":""}\u2026`,!0);let checked=0,flaggedTotal=0,failedNames=[];const touchedSegs=[],knownNames=[...roster.keys()];try{for(const[name,idxs]of candidates){busy.setProgress(checked,candidates.length);let sampleIdxs=idxs;if(idxs.length>MAX_LINES_PER_CALL){const step=idxs.length/MAX_LINES_PER_CALL;sampleIdxs=Array.from({length:MAX_LINES_PER_CALL},(_,i)=>idxs[Math.floor(i*step)])}const lines=sampleIdxs.map(i=>({index:i,text:segs[i].text}));try{const r=await fetch("/api/audiobook-consistency-check",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({character:name,lines,known_characters:knownNames.filter(n=>n.toLowerCase()!==name.toLowerCase()),llm_url:savedChoice2.url,model:savedChoice2.model})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const data=await r.json(),outliers=Array.isArray(data==null?void 0:data.outliers)?data.outliers:[];for(const o of outliers){let idx=o.index;const quote=String(o.quote||"").trim().toLowerCase(),matchesQuote=i=>{var _a4;return!quote||(((_a4=segs[i])==null?void 0:_a4.text)||"").toLowerCase().includes(quote.slice(0,40))};if((typeof idx!="number"||!segs[idx]||segs[idx].speaker!==name||!matchesQuote(idx))&&(idx=quote?sampleIdxs.find(i=>segs[i].speaker===name&&matchesQuote(i)):void 0),typeof idx!="number"||!segs[idx]||segs[idx].speaker!==name)continue;const suggested=String(o.suggested_speaker||"").trim();let newSpeaker=null;!suggested||/^unknown|unbekannt$/i.test(suggested)?newSpeaker="Unknown":newSpeaker=knownNames.find(n=>n.toLowerCase()===suggested.toLowerCase())||null,!(!newSpeaker||newSpeaker===name)&&(segs[idx].speaker=newSpeaker,newSpeaker==="Unknown"&&(segs[idx].type="dialogue"),touchedSegs.push(segs[idx]),flaggedTotal++)}}catch(err){failedNames.push(name),console.error("[consistency check]",name,err)}checked++,busy.setProgress(checked,candidates.length)}}finally{busy.remove()}flaggedTotal&&(await _abPatchScatteredRows(touchedSegs,()=>{})||await _abRedrawSegmentsChunked(active.arr),_abPersistManualEdit());const failSuffix=failedNames.length?` (${failedNames.length} character${failedNames.length!==1?"s":""} failed to check: ${failedNames.slice(0,5).join(", ")})`:"";toast((flaggedTotal?`Consistency check: ${flaggedTotal} line${flaggedTotal!==1?"s":""} reassigned across ${checked} character${checked!==1?"s":""}`:`Consistency check: no mismatches found across ${checked} character${checked!==1?"s":""}`)+failSuffix,failedNames.length&&!flaggedTotal?"error":"success")};foot.querySelector("#ab-cv-open-reh").addEventListener("click",async()=>{closePanel(),await audiobookOpenCurrentInRehearser()});const applyPromptAndRun2=callback=>{const newPrompt=panel.querySelector("#ab-cv-prompt-text").value,choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt})}).finally(()=>{closePanel(),callback&&callback(savedChoice.url,savedChoice.model)})},runIdentifyAll=async()=>{var _a4;const bookTitle2=((_a4=window.readerState)==null?void 0:_a4.title)||"";bookTitle2&&typeof window.openBookContextEditor=="function"&&!(typeof _getBookProfile=="function"?await _getBookProfile(bookTitle2):{}).confirmed&&!await window.openBookContextEditor(bookTitle2)||!await confirmDialog("Start from scratch? This will discard the current cast for this book and rebuild the character definitions from the text.",{title:"Discard current cast?",okLabel:"Start from scratch",danger:!0})||applyPromptAndRun2(audiobookCast)},runIdentifyUnknown=()=>applyPromptAndRun2(audiobookRecastUnknown),runRepairNarration=()=>{const segs=_audiobook.segments||[],n=typeof _abSuspectNarration=="function"?_abSuspectNarration(segs).size:0;if(!n){toast("No narration lines look like mislabeled dialogue","success");return}toast(`Re-checking ${n} narration line${n!==1?"s":""} that look like spoken dialogue\u2026`,"info"),audiobookRecastUnknown(null,null,{repairNarration:!0,prompt:verificationPrompt})},runCastAll=()=>{typeof window.csForReader=="function"?window.csForReader():typeof csForReader=="function"?csForReader():toast("Character sheets not loaded yet","error")},runCastFresh=async()=>{await confirmDialog("Discard every generated character sheet for this book and rebuild every profile from a blank slate? This cannot be undone.",{title:"Discard all character sheets?",okLabel:"Discard & rebuild",danger:!0})&&(typeof window.csForReader=="function"?window.csForReader({fresh:!0}):typeof csForReader=="function"?csForReader({fresh:!0}):toast("Character sheets not loaded yet","error"))},runCastSelected=()=>{if(!existing.length){toast("No character sheets found yet for this book","error");return}_abOpenRecastSelectPopup(existing)},runCastContinueUncasted=async()=>{if(typeof clGetAllByTagOrBook!="function"||typeof csForReaderSelective!="function"||typeof CS_DETAIL_FIELDS=="undefined"){toast("Character sheets not loaded yet","error");return}let recs=[];try{recs=await clGetAllByTagOrBook(bookTitle)}catch{}const recByName=new Map;recs.forEach(r=>{var _a4;const name=String(r.name||"").trim().toLowerCase();if(name&&recByName.set(name,r),typeof clSplitIdentityTokens=="function")for(const a of clSplitIdentityTokens((_a4=r==null?void 0:r.sheet)==null?void 0:_a4.aliases,{aliases:!0})){const key=a.toLowerCase();recByName.has(key)||recByName.set(key,r)}});const roster2=(_audiobook.roster||[]).filter(n=>n&&!/^unknown$|^unbekannt$/i.test(n.trim())),MIN_LINES_TO_TRY=10,lineCounts=new Map;(_audiobook.segments||[]).forEach(s=>{if((s==null?void 0:s.type)!=="dialogue"||!s.speaker)return;const k=String(s.speaker).trim().toLowerCase();lineCounts.set(k,(lineCounts.get(k)||0)+1)});const tooSparse=[],incomplete=roster2.filter(n=>{const rec=recByName.get(String(n).trim().toLowerCase());return rec&&CS_DETAIL_FIELDS.some(f=>String((rec.sheet||{})[f]||"").trim())?!1:(lineCounts.get(String(n).trim().toLowerCase())||0){var _a4;if((_a4=document.getElementById("s-caststudio"))!=null&&_a4.classList.contains("is-active")&&typeof window.showStudioPhase=="function"){window.showStudioPhase(3);return}try{sessionStorage.setItem("ttsvc_cast_return","reader")}catch{}typeof navTo=="function"&&navTo("s-library"),typeof navLibraryView=="function"&&navLibraryView("characters")},runLibraryCleanup=async()=>{if(typeof clGetAllByTagOrBook!="function"||typeof clDelete!="function"){toast("Character library is not available right now","error");return}const roster2=(_audiobook.roster||[]).filter(n=>n&&!/^unknown$|^unbekannt$/i.test(n.trim()));if(!roster2.length){toast("No current roster to clean up against","error");return}const rosterSet=new Set(roster2.map(n=>n.trim().toLowerCase()));let recs=[];try{recs=await clGetAllByTagOrBook(bookTitle)}catch{}const toDelete=recs.filter(r=>!rosterSet.has(String(r.name||"").trim().toLowerCase()));if(!toDelete.length){toast(`Library already matches the current ${roster2.length}-name roster \u2014 nothing to remove`,"success");return}const preview=toDelete.slice(0,12).map(r=>r.name).join(", ")+(toDelete.length>12?`, +${toDelete.length-12} more`:"");if(!await confirmDialog(`Remove ${toDelete.length} character record${toDelete.length!==1?"s":""} that aren't in the current ${roster2.length}-name roster? This cannot be undone. - -${preview}`,{title:"Clean up character library?",okLabel:`Remove ${toDelete.length}`,danger:!0}))return;let removed=0;for(const r of toDelete)try{await clDelete(r.id),removed++}catch{}toast(`Removed ${removed} character record${removed!==1?"s":""} not in the current roster`,"success")},bookTitle=((_a3=window.readerState)==null?void 0:_a3.title)||"";let existing=[];(async()=>{if(!(!bookTitle||typeof clGetAllByTagOrBook!="function"))try{existing=await clGetAllByTagOrBook(bookTitle)}catch{}})();const identifyMenu=foot.querySelector("#ab-cv-menu-identify"),castMenu=foot.querySelector("#ab-cv-menu-cast"),viewCastMenu=foot.querySelector("#ab-cv-menu-viewcast");identifyMenu==null||identifyMenu.addEventListener("click",()=>_abToggleFootMenu(identifyMenu,[{icon:"mdi-refresh",label:"1. Identify all characters",title:"Scan the text and build the cast list from scratch",onClick:runIdentifyAll,danger:!0},{icon:"mdi-auto-fix",label:"2. Auto-repair cast (2.1\u20132.5)",title:"Runs the repair sequence automatically: fix mislabeled dialogue, resolve unknown speakers, repeat while it helps, then a full verification only if still needed",onClick:()=>audiobookAutoRepairCast()},{icon:"mdi-comment-alert-outline",label:"2.1 Repair mislabeled dialogue",title:"Finds spoken lines that were typed as narration (usually because the \xBB \xAB quote marks were lost in extraction) and re-checks only those \u2014 far cheaper than verifying every narration line",onClick:runRepairNarration},{icon:"mdi-account-question-outline",label:"2.2 Identify unknown characters",title:"Re-scan only the unknown segments with the current prompt",onClick:runIdentifyUnknown},{icon:"mdi-repeat",label:"2.3 Run until < N unknown\u2026",title:"Repeats recast-unknown + narrator-verify passes automatically until the Unknown-speaker count drops below a target, or progress stalls",onClick:()=>{const input=window.prompt("Stop once fewer than this many Unknown speakers remain:","10");if(input==null)return;const threshold=parseInt(input,10);if(!Number.isFinite(threshold)||threshold<0){toast("Enter a whole number of 0 or more","error");return}audiobookRecastUntilThreshold(threshold)}},{icon:"mdi-shield-check-outline",label:"2.4 Verify all characters",title:"Second-pass plausibility check that keeps the existing cast and only corrects uncertain matches",onClick:runVerificationPass},{icon:"mdi-account-search-outline",label:"2.5 Check voice consistency",title:"Third-pass check: gathers every line already credited to each character across the whole book and flags any that don\u2019t match their established voice",onClick:runConsistencyPass},{icon:"mdi-traffic-light-outline",label:"2.6 Score attribution confidence",title:"Colour-code every line by how certain its speaker is (green/orange/red), then use F2 to jump between the uncertain ones. Instant \u2014 no LLM calls.",onClick:()=>{try{const segs=_audiobook.segments||[];if(!segs.length){toast("Nothing cast yet to score \u2014 open or cast a book first","error");return}_abStampConfidence(segs);const red=segs.filter(s=>typeof s.conf=="number"&&s.conf<50).length,amber=segs.filter(s=>typeof s.conf=="number"&&s.conf>=50&&s.conf<96).length,root=document.querySelector(".ab-castpanel-inline")||document;root.querySelectorAll(".ab-cv-row").forEach(row=>{const s=row.__seg;if(!s||typeof s.conf!="number")return;row.dataset.conf=String(s.conf);const existing2=row.querySelector(".ab-cv-conf");if(existing2)existing2.outerHTML=_abConfBadgeHtml(s);else{const spk=row.querySelector(".ab-cv-spk");spk?spk.insertAdjacentHTML("afterend",_abConfBadgeHtml(s)):row.insertAdjacentHTML("afterbegin",_abConfBadgeHtml(s))}}),typeof root._abRefreshReview=="function"&&root._abRefreshReview(),_audiobook.lastText&&_abSaveDraft(segs,_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0),toast(`Scored ${segs.length} lines \u2014 ${red} guess/doubtful, ${amber} worth a look. Use the review button or F2.`,"success")}catch(e){console.error("[confidence]",e),toast("Scoring failed: "+(e.message||e),"error")}}}])),window._abRunConsistencyPass=runConsistencyPass,castMenu==null||castMenu.addEventListener("click",()=>_abToggleFootMenu(castMenu,[{icon:"mdi-account-multiple-plus-outline",label:"Cast all character roles",title:"Generate / refresh the character sheets for every cast character",onClick:runCastAll},{icon:"mdi-account-arrow-right-outline",label:"Continue uncasted characters",title:"Only generate profiles for characters with no detail yet \u2014 skips anyone already fully cast",onClick:runCastContinueUncasted},{icon:"mdi-account-check-outline",label:"Select characters to cast or recast roles",title:existing.length?"Choose character roles to generate or refresh":"No character sheets found yet for this book",disabled:!existing.length,onClick:runCastSelected},{divider:!0},{icon:"mdi-refresh",label:"New recast (discard & rebuild all)",title:"Discard every generated character sheet and rebuild every profile from scratch",onClick:runCastFresh,danger:!0}])),viewCastMenu==null||viewCastMenu.addEventListener("click",()=>_abToggleFootMenu(viewCastMenu,[{icon:"mdi-eye-outline",label:"View cast",title:"View the cast overview in the library",onClick:runViewCast},{icon:"mdi-folder-zip-outline",label:"Export cast archive (.zip)",title:"Download one zip: the cast as a readable Markdown script plus a Markdown sheet per character",onClick:audiobookExportCastMd},{divider:!0},{icon:"mdi-broom",label:"Clean up library to current roster",title:"Remove saved character records that aren't in the current roster \u2014 old spelling-variant duplicates and superseded names from past casting runs",onClick:runLibraryCleanup,danger:!0}])),(_b3=foot.querySelector("#ab-cv-continue"))==null||_b3.addEventListener("click",()=>applyPromptAndRun2((u,m)=>audiobookCast(u,m,{startIndex:_audiobook.completedChunks,segments:_audiobook.segments,roster:_audiobook.roster,narrationOnly:_audiobook.narratedPassages,degraded:_audiobook.degraded}))),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},done(options={}){var _a3,_b3,_c3,_d3,_e3;if(_abHideCastingControl(),!(options.stopped||_audiobook.cancel)){closePanel();return}this.clearProcessing(),count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const message=options.message||"Casting stopped before any passage completed.";if(feed.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider, .ab-cv-page")){if(options.message){const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}}else{feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='No completed characters yet.');const foot=panel.querySelector("#ab-cv-foot"),hasSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length>0;foot.hidden=!1,foot.innerHTML=` ${escHtml(message)}${hasSegments?'':""}`,(_c3=foot.querySelector("#ab-cv-stopped-back"))==null||_c3.addEventListener("click",closePanel),(_d3=foot.querySelector("#ab-cv-stopped-review"))==null||_d3.addEventListener("click",async()=>{closePanel(),await audiobookOpenCurrentInRehearser()}),(_e3=foot.querySelector("#ab-cv-stopped-recast"))==null||_e3.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt||""})}).finally(()=>{closePanel(),audiobookCast(choice.url,choice.model)})}),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},loadingRestore(){var _a3,_b3;(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent="Loading saved cast\u2026",feed.appendChild(r),chars.innerHTML='restoring\u2026';const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent="Checking saved cast before starting a new one.");const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="none")},setFreshState(message="Ready to identify characters."){var _a3,_b3;if((_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),!feed.querySelector(".ab-cv-row, .ab-cv-divider, .ab-cv-page")){feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='No saved cast found.');const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent=message);const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="inline-block",castBtn.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),audiobookCast(choice.url,choice.model)}))}}}function _abBusyGuard(what){if(!_audiobook.running)return!1;try{toast(`${what} can't start yet \u2014 another pass is still running. Use Stop in the casting panel first.`,"error")}catch{}return!0}async function audiobookRecastUnknown(overrideUrl,overrideModel,options={}){var _a2,_b2,_c2,_d2,_e2,_f2;if(_abBusyGuard("Recast / verify"))return;const segs=_audiobook.segments;if(!segs||!segs.length)return;const originalSegments=segs.map(s=>({...s})),countUnknownDialogue=arr=>(arr||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length,beforeUnknownCount=countUnknownDialogue(segs),preResolved=audiobookResolveUnknowns(segs,[],_audiobook.roster||[]);preResolved.length&&toast(`${preResolved.length} Unknown line${preResolved.length!==1?"s":""} resolved by grammar rules`,"success");const unknownIdxs=[],_abRepairSuspects=options.repairNarration?_abSuspectNarration(segs):null;for(let i=0;iac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang();let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel()),promptOverride=typeof options.prompt=="string"?options.prompt:null;!promptOverride&&options.repairNarration&&typeof window._abVerificationPrompt=="string"&&(promptOverride=window._abVerificationPrompt);const groups=audiobookRecastGroups(unknownIdxs,segs),view=audiobookCastView(unknownIdxs.length,llm_url,model);view.recountRoster(segs);const _rcAliasMap=new Map;try{const bookTitle=((_a2=window.readerState)==null?void 0:_a2.title)||"",records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(bookTitle):[];for(const rec of records||[])if(rec!=null&&rec.name&&(_rcAliasMap.set(rec.name.toLowerCase(),rec.name),typeof clSplitIdentityTokens=="function"))for(const a of clSplitIdentityTokens(rec.aliases,{aliases:!0}))_rcAliasMap.set(a.toLowerCase(),rec.name)}catch{}const canonicalizeSpeaker=name=>name&&_rcAliasMap.get(name.toLowerCase())||name;view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_b2=document.getElementById("ab-cv-llm-url"))==null?void 0:_b2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_c2=document.getElementById("ab-cv-llm-select"))==null?void 0:_c2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Character definition stopped before any lines were updated."}),toast("Character definition stopped. Existing cast preserved.","info");return}}let done=0,prevIdx=-2,groupsSinceSave=0;const pendingReplacements=new Map,normalizeRecastSegment=(seg,fallback)=>{const type=(seg==null?void 0:seg.type)==="narration"?"narration":"dialogue";return{speaker:type==="narration"?"Narrator":canonicalizeSpeaker((seg==null?void 0:seg.speaker)||(fallback==null?void 0:fallback.speaker)||"Unknown"),type,emotion:type==="dialogue"&&((seg==null?void 0:seg.emotion)||(fallback==null?void 0:fallback.emotion))||"",text:(seg==null?void 0:seg.text)||(fallback==null?void 0:fallback.text)||""}};try{for(let group of groups){if(_audiobook.cancel)break;const unresolvedGroup=[];for(const idx of group)audiobookIsSpeechTagOnly((_d2=segs[idx])==null?void 0:_d2.text)?(segs[idx].type="narration",segs[idx].speaker="Narrator",segs[idx].emotion=""):unresolvedGroup.push(idx);if(!unresolvedGroup.length){group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1],view.update(done,`Correcting narration tags ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}group=unresolvedGroup,group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1];const recastCtx=audiobookRecastContext(segs,group),passageText=recastCtx.text,lineLabel=group.length>1?`Attributing lines ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`:`Attributing line ${done+1} / ${unknownIdxs.length}\u2026`;view.update(done,lineLabel),view.processing(passageText.trim());let data=null;const recastBody=audiobookAttributeBody({text:passageText.trim(),known_characters:_audiobook.roster.slice(-40),recent:recastCtx.recent||"",language,llm_url:((_e2=document.getElementById("ab-cv-llm-url"))==null?void 0:_e2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_f2=document.getElementById("ab-cv-llm-select"))==null?void 0:_f2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_RECAST_TIMEOUT_MS)},promptOverride);try{try{data=await audiobookAttributeStream(recastBody,view,ac.signal,AUDIOBOOK_RECAST_TIMEOUT_MS)}catch(streamErr){if(streamErr.name==="AbortError")throw streamErr;const r=await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify(recastBody)},AUDIOBOOK_RECAST_TIMEOUT_MS+5e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||e.error||r.statusText||"HTTP "+r.status)}data=await r.json()}}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0;break}view.note(`API Error while checking ${group.length>1?"a group of Unknown lines":"Unknown line"}: ${err.message||err}`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}if(data&&data.segments){const used=new Set;for(const idx of group){const targetSeg=segs[idx],seqMatch=audiobookFindReturnedSegmentSequence(targetSeg,data.segments,used);if(seqMatch&&seqMatch.segs.length>1){const repl=seqMatch.segs.map(s=>normalizeRecastSegment(s,targetSeg)).filter(s=>(s.text||"").trim()),hasResolved=repl.some(s=>s.type==="narration"||s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)),hasUnresolvedDialogue=repl.some(s=>s.type==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))),hasImplausibleDialogue=repl.some(s=>s.type==="dialogue"&&_abImplausibleDialoguePiece(s.text));if(repl.length&&hasResolved&&!hasUnresolvedDialogue&&!hasImplausibleDialogue){seqMatch.idxs.forEach(i=>used.add(i)),pendingReplacements.set(idx,repl),repl.forEach(s=>{s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)&&!_audiobook.roster.includes(s.speaker)&&_audiobook.roster.push(s.speaker)});continue}}const match=audiobookFindReturnedSegment(targetSeg,data.segments,used);if(match&&match.seg.speaker){const speaker=match.seg.type==="narration"?"Narrator":canonicalizeSpeaker(match.seg.speaker);if(!speaker||/^Unknown|Unbekannt/i.test(speaker))continue;used.add(match.idx),targetSeg.type=match.seg.type==="narration"?"narration":"dialogue",targetSeg.speaker=speaker,targetSeg.emotion=targetSeg.type==="dialogue"&&(match.seg.emotion||targetSeg.emotion)||"",targetSeg.type==="dialogue"&&!_audiobook.roster.includes(speaker)&&_audiobook.roster.push(speaker)}}}for(const idx of group)view.addSegments(pendingReplacements.get(idx)||[segs[idx]]),done++;groupsSinceSave++,groupsSinceSave>=10&&_audiobook.lastText&&(groupsSinceSave=0,_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0),view.recountRoster(segs))}view.update(_audiobook.cancel?done:unknownIdxs.length)}catch(e){e.name!=="AbortError"&&view.note("Error defining unknowns: "+e.message)}finally{_audiobook.running=!1,_audiobook.abort=null,_abHideCastingControl()}try{pendingReplacements.size&&[...pendingReplacements.entries()].sort((a,b)=>b[0]-a[0]).forEach(([idx,repl])=>segs.splice(idx,1,...repl));const{segments:deduped,removed:dupRemoved}=_audiobookDedupNearbyDuplicates(segs);dupRemoved&&(segs.splice(0,segs.length,...deduped),view.note(`Removed ${dupRemoved} duplicated line${dupRemoved!==1?"s":""} introduced by this verification pass.`));const afterUnknownCount=countUnknownDialogue(segs);if(afterUnknownCount>beforeUnknownCount){segs.splice(0,segs.length,...originalSegments);const how=_audiobook.cancel?"Stopped run":"Quality run";view.note(`${how} rolled back: Unknown segments increased from ${beforeUnknownCount} to ${afterUnknownCount}. Existing cast preserved.`),toast(`${how} rolled back because it increased Unknown speakers.`,"error")}const _rcDone=_audiobook.completedChunks||segs.length,_rcTotal=_audiobook.completedTotal||_rcDone;if(_audiobook.cancel){_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal),view.done({stopped:!0,message:"Character definition stopped. Existing cast preserved."}),toast("Character definition stopped. Existing cast preserved.","info");return}{const _ph=_audiobookDropPhantomSpeakers(segs);_ph.dropped&&console.info("[recast] dropped",_ph.dropped,"phantom speaker line(s):",_ph.names.join(", "))}{const _role=_audiobookResolveRoleAliases(segs,_audiobook.lastText||"");_role.moved&&(console.info("[recast] resolved role labels to names:",_role.pairs.join(", "),`(${_role.moved} lines)`),view.note(`Resolved ${_role.merged} role label${_role.merged!==1?"s":""} to character names (${_role.moved} lines).`))}{const _ident=_audiobookConsolidateSpeakerAliases(segs);_ident.merged&&(console.info("[recast] consolidated",_ident.merged,"speaker label variant(s),",_ident.moved,"line(s)"),view.note(`Merged ${_ident.merged} duplicate character label${_ident.merged!==1?"s":""} (${_ident.moved} lines).`))}{const _dm=_audiobookMergeAdjacentDialogue(segs);_dm.merged&&(segs.splice(0,segs.length,..._dm.segments),console.info("[recast] merged",_dm.merged,"consecutive same-speaker dialogue segment(s)"),view.note(`Merged ${_dm.merged} consecutive same-speaker line${_dm.merged!==1?"s":""}.`))}_abStampConfidence(segs),_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal);const speakers=new Set(segs.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${segs.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)}catch(e){console.error("[audiobookRecastUnknown] post-processing failed",e),view.done({stopped:!0,message:"Finished checking speakers, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}const AB_AUTO_REPAIR_KEY="ab-auto-repair-enabled";function audiobookAutoRepairEnabled(){try{return localStorage.getItem(AB_AUTO_REPAIR_KEY)!=="0"}catch{return!0}}async function audiobookAutoRepairCast(threshold=10,maxRounds=4){if(!(_audiobook.segments||[]).length){toast("Nothing cast yet to repair","error");return}const countUnknown=()=>(_audiobook.segments||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Cast looks clean \u2014 ${prev} unknown speaker${prev!==1?"s":""}`,"success");return}toast(`Auto-repair: ${prev} unknown speakers \u2014 running repair passes\u2026`,"info");for(let round=1;round<=maxRounds;round++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Auto-repair stopped.","info");return}const now=countUnknown();if(now<=threshold){toast(`Auto-repair done: ${now} unknown left after ${round} round${round!==1?"s":""}.`,"success");return}if(now>=prev)break;prev=now}toast(`2.4 Still ${prev} unknown \u2014 running full verification pass\u2026`,"info"),await audiobookRecastUnknown(null,null,{repairNarration:!0});const afterVerify=countUnknown();if(typeof window._abRunConsistencyPass=="function"&&!_audiobook.cancel){toast("2.5 Checking voice consistency\u2026","info");try{await window._abRunConsistencyPass()}catch(e){console.warn("[auto-repair 2.5]",e)}}const final=countUnknown();toast(final<=threshold?`Auto-repair done: ${final} unknown left.`:`Auto-repair finished: ${final} unknown remain (target was under ${threshold}).`,final<=threshold?"success":"error")}window.audiobookAutoRepairCast=audiobookAutoRepairCast;async function audiobookRecastUntilThreshold(threshold=10,maxPasses=8){if(_audiobook.running){toast("Casting is already running","error");return}const segs=_audiobook.segments;if(!segs||!segs.length){toast("No cast to check","error");return}const countUnknown=()=>(segs||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Already at ${prev} unknown speakers (target: <${threshold})`,"success");return}toast(`Running recast + verify passes until under ${threshold} unknown speakers (currently ${prev})\u2026`,"info");for(let pass=1;pass<=maxPasses;pass++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Stopped \u2014 cancelled mid-pass.","info");return}const now=countUnknown();if(now<=threshold){toast(`Done: ${now} unknown speakers remain (target reached in ${pass} pass${pass!==1?"es":""}).`,"success");return}if(now>=prev){toast(`Stopped after ${pass} pass${pass!==1?"es":""}: no further progress (${now} unknown remain, target was <${threshold}). Likely GPU/LLM contention or genuinely ambiguous lines.`,"error");return}prev=now}toast(`Stopped after ${maxPasses} passes: ${prev} unknown speakers remain (target was <${threshold}).`,"error")}window.audiobookRecastUntilThreshold=audiobookRecastUntilThreshold;async function audiobookOpenCastView(){var _a2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}const _curBook=window.readerState&&readerState.savedId||null;_curBook&&_audiobook.bookId&&_audiobook.bookId!==_curBook&&(_audiobook.segments=null,_audiobook.lastText=null,_audiobook.roster=null),_audiobook.bookId=_curBook;const llm_url=audiobookLlmUrl(),model=audiobookLlmModel(),chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text];if(_audiobook.segments&&_audiobook.segments.length>0&&_audiobook.lastText===text){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.rebuild(_audiobook.segments);const speakers=new Set(_audiobook.segments.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${_audiobook.segments.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown);return}const _applyDraft=(draft,view,source)=>{const draftDone=Number.isFinite(Number(draft.done))?Number(draft.done):0,draftTotal=Number.isFinite(Number(draft.total))?Number(draft.total):0;_abStampSegmentPages(draft.segments,draft.pageMarks,text),_audiobook.segments=draft.segments,_audiobook.roster=draft.roster||[],_audiobook.lastText=text,_audiobook.pageMarks=draft.pageMarks||[],_audiobook.rehId=draft.rehId||null,_audiobook.completedChunks=draftDone>0?draftDone:0,_audiobook.completedTotal=draftTotal>0?draftTotal:0,view.rebuild(draft.segments);const ageMs=Date.now()-(draft.savedAt||0),ageMins=Math.round(ageMs/6e4),ageStr=ageMins<1?"gerade eben":ageMins<60?`vor ${ageMins} Min.`:`vor ${Math.round(ageMins/60)} Std.`,pct=draftTotal>0?Math.max(0,Math.min(100,Math.round(draftDone/draftTotal*100))):100,wasDone=draftTotal>0&&draftDone>=draftTotal,canContinue=draftDone>0&&draftTotal>0&&draftDones.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} Charakter${speakers.size!==1?"e":""} \xB7 ${draft.segments.length} Segmente`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)},_localDraft=_abLoadDraft(text);if(_localDraft&&_localDraft.segments&&_localDraft.segments.length>0){const view=audiobookCastView(chunks.length,llm_url,model,!1),bookId2=_abBookId();let chosen=_localDraft,source="local",serverDraft=null;if(bookId2){try{serverDraft=await _abLoadDraftServer(bookId2)}catch{}if(serverDraft&&(serverDraft.savedAt||0)>(_localDraft.savedAt||0)){chosen=serverDraft,source="server";try{localStorage.setItem(_abDraftKey(bookId2),JSON.stringify(serverDraft))}catch{}}}_applyDraft(chosen,view,source),bookId2&&source==="local"&&(!serverDraft||(_localDraft.savedAt||0)>(serverDraft.savedAt||0))&&fetch(`/api/reader/docs/${encodeURIComponent(bookId2)}/scripts/cast`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({..._localDraft,bookId:bookId2,title:((_a2=window.readerState)==null?void 0:_a2.title)||_localDraft.title||""})}).catch(()=>{});return}const bookId=_abBookId();if(bookId){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.loadingRestore();try{const serverDraft=await _abLoadDraftServer(bookId);if(serverDraft&&serverDraft.segments&&serverDraft.segments.length>0){try{localStorage.setItem(_abDraftKey(bookId),JSON.stringify(serverDraft))}catch{}_applyDraft(serverDraft,view,"server")}else view.setFreshState("No saved cast was found for this saved book. Starting a new cast will create a new draft.")}catch{view.setFreshState("Saved cast could not be checked. Browser autosave was already searched; starting a new cast will create a new draft.")}return}audiobookCastView(chunks.length,llm_url,model,!0)}function _abSourceSentences(text){const out=[];for(const line of String(text||"").split(` -`)){if(!line.trim())continue;const sentences=line.match(/[^.!?]+[.!?]+\s*/g)||[],rest=line.replace(/[^.!?]+[.!?]+\s*/g,"").trim();for(const s of sentences){const t=s.trim();t&&out.push(t)}rest&&out.push(rest)}return out}function _abNormForCoverage(t){return String(t||"").replace(/[«»„“”"'’‚‹›]/g,"").replace(/\s+/g," ").trim().toLowerCase()}function _abRecoverDroppedText(sourceText,segs){if(!segs.length)return{segs,recovered:0};const sentences=_abSourceSentences(sourceText).filter(s=>_abNormForCoverage(s).length>=8);if(!sentences.length)return{segs,recovered:0};const segNorm=segs.map(s=>_abNormForCoverage(s.text)),matchIdx=sentences.map(s=>segNorm.findIndex(t=>t.includes(_abNormForCoverage(s)))),out=[];let copiedUpTo=-1,gap=[],recovered=0;const flushGap=beforeSegIdx=>{if(gap.length){for(;copiedUpTo+1{if(matchIdx[i]===-1){gap.push(s);return}flushGap(matchIdx[i])});copiedUpTo+10&&resume.startIndex0;_audiobook.cancel=!1,isResume||_abClearDraft(),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!0);const ac=new AbortController;_audiobook.abort=()=>ac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang(text);let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel());const view=audiobookCastView(chunks.length,llm_url,model,!1);isResume&&resume.segments&&resume.segments.length&&view.rebuild(resume.segments),view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_a2=document.getElementById("ab-cv-llm-url"))==null?void 0:_a2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_b2=document.getElementById("ab-cv-llm-select"))==null?void 0:_b2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}}const allSegments2=isResume?resume.segments.slice():[];_audiobook.liveSegments=allSegments2;const roster=isResume?(resume.roster||[]).slice():[];let narrationOnly=isResume&&resume.narrationOnly||0,degraded=isResume&&resume.degraded||0,recoveredGaps=isResume&&resume.recoveredGaps||0,completedChunks=startIndex;_abStartDraftAutosave(()=>{allSegments2.length&&_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)});const _pgMarks=(_audiobook.pageMarks||[]).slice();_pgMarks.length&&_pgMarks[0].offset<=2&&_pgMarks.shift();let _pgMarkIdx=0,_pgCharPos=0,_curPageNum=1;if(isResume){for(let k=0;k0&&(_curPageNum=_pgMarks[_pgMarkIdx-1].page+1)}try{for(let i=startIndex;i=_pgMarks[_pgMarkIdx].offset;)_curPageNum=_pgMarks[_pgMarkIdx].page+1,view.pagemark(_curPageNum),_pgMarkIdx++;if(_pgCharPos+=chunks[i].length+1,!audiobookHasDialogue(chunks[i])){const seg={speaker:"Narrator",type:"narration",text:chunks[i],emotion:"",page:_curPageNum};allSegments2.push(seg),narrationOnly++,view.addSegments([seg]),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length);continue}const recent=allSegments2.filter(s=>s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)).slice(-6).map(s=>`${s.speaker}: ${(s.text||"").slice(0,80)}`).join(` -`);view.processing(chunks[i]);const attributeChunk=async(chunkText,recentCtx,timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS)=>{var _a3,_b3;const body={text:chunkText,known_characters:roster.slice(-40),recent:recentCtx,language,llm_url:((_a3=document.getElementById("ab-cv-llm-url"))==null?void 0:_a3.value.trim())||llm_url,model:audiobookSafeLlmModel(((_b3=document.getElementById("ab-cv-llm-select"))==null?void 0:_b3.value)||model),timeout_seconds:audiobookTimeoutSeconds(timeoutMs)};try{const d=await audiobookAttributeStream(body,view,ac.signal,timeoutMs);return Array.isArray(d.segments)?d.segments:null}catch(err){if(err.name==="AbortError")throw err}try{const r=await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify(body)},timeoutMs+5e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||e.error||r.statusText||"HTTP "+r.status)}const d=await r.json();return Array.isArray(d.segments)?d.segments:null}catch(err){if(err.name==="AbortError")throw err;return{error:err.message}}};let segs;try{let result=await attributeChunk(chunks[i],recent),data=null;if(result&&!result.error)data=result;else if(result&&result.error){view.note(`\u26A0\uFE0F Passage ${i+1} timed out \u2014 retrying in two halves\u2026`);const half=Math.floor(chunks[i].length/2),splitAt=chunks[i].lastIndexOf(" ",half)||half,chunkA=chunks[i].slice(0,splitAt).trim(),chunkB=chunks[i].slice(splitAt).trim(),resA=await attributeChunk(chunkA,recent,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS),resB=await attributeChunk(chunkB,recent,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS),segsA=Array.isArray(resA)?resA:null,segsB=Array.isArray(resB)?resB:null;if(segsA||segsB){const arrA=segsA||audiobookSplitByQuotes(chunkA),arrB=segsB||audiobookSplitByQuotes(chunkB);data=[...arrA,...arrB],segsA||(degraded++,view.note(`\u26A0\uFE0F Passage ${i+1} (first half) \u2014 auto-detected (retry also failed)`)),segsB||(degraded++,view.note(`\u26A0\uFE0F Passage ${i+1} (second half) \u2014 auto-detected (retry also failed)`))}else view.note(`\u274C Passage ${i+1} \u2014 LLM error: ${result.error}`),data=null}segs=Array.isArray(data)?data:data&&Array.isArray(data.segments)?data.segments:[];const hasDialogue=segs.some(s=>s.type==="dialogue");if(!segs.length||!hasDialogue&&audiobookHasDialogue(chunks[i])){segs=audiobookSplitByQuotes(chunks[i]),degraded++;const named=segs.filter(s=>s.type==="dialogue"&&!/^Unknown|Unbekannt/i.test(s.speaker)).length;view.note(`Passage ${i+1} \u2014 auto-detected dialogue${named?` (${named} speaker${named!==1?"s":""} from tags)`:" (set speakers in review)"}`)}else{let cleanedSegs=[];for(let s of segs){if(s.text=s.text||"",s.type==="dialogue"&&audiobookIsSpeechTagOnly(s.text)&&(s.type="narration",s.speaker="Narrator",s.emotion=""),s.type==="dialogue"&&s.text.trim().length>10){const tText=s.text.trim(),idx=chunks[i].indexOf(tText);if(idx!==-1){const surround=chunks[i].slice(Math.max(0,idx-8),idx)+chunks[i].slice(idx+tText.length,idx+tText.length+8);/[«»„“”"‟‚‘’›‹『「—–]/.test(surround)||(s.type="narration",s.speaker="Narrator",s.emotion="")}}if(s.type==="dialogue"&&_abImplausibleDialoguePiece(s.text)&&(s.type="narration",s.speaker="Narrator",s.emotion=""),cleanedSegs.length>0){let last=cleanedSegs[cleanedSegs.length-1];if(s.text.trim()===last.text.trim())if(s.type==="dialogue"&&last.type!=="dialogue"){cleanedSegs[cleanedSegs.length-1]=s;continue}else{if(s.type!=="dialogue"&&last.type==="dialogue")continue;continue}if(s.type==="dialogue"&&last.type==="narration"){const tS=s.text.trim(),tL=last.text.trim();tL.endsWith(tS)&&(last.text=tL.slice(0,-tS.length).trim(),last.text||cleanedSegs.pop())}const _cut=_abPrefixDupCut(s.text,last.text);_cut>0&&(s.text=s.text.slice(_cut).replace(/^[\s«»„“”"‟‚‘’›‹]*,?\s*/,"").trim(),s.text&&last.type==="dialogue"&&s.type==="dialogue"&&(s.type="narration",s.speaker="Narrator",s.emotion=""))}s.text.trim()&&cleanedSegs.push(s)}let mergedSegs=[];for(let s of cleanedSegs){if(mergedSegs.length>0){let last=mergedSegs[mergedSegs.length-1];if(s.type==="narration"&&last.type==="narration"&&(s.speaker||"Narrator").toLowerCase()==="narrator"&&(last.speaker||"Narrator").toLowerCase()==="narrator"){/[.!?]$/.test(last.text.trim())?last.text=last.text.trimEnd()+` - -`+s.text.trimStart():last.text=last.text.trimEnd()+" "+s.text.trimStart();continue}}mergedSegs.push(s)}let respiltSegs=[];for(const s of mergedSegs)s.type==="narration"&&audiobookHasDialogue(s.text)?respiltSegs.push(...audiobookSplitByQuotes(s.text)):respiltSegs.push(s);for(const s of respiltSegs)s.type==="dialogue"&&_abImplausibleDialoguePiece(s.text)&&(s.type="narration",s.speaker="Narrator",s.emotion="");segs=respiltSegs}}catch(chunkErr){if(chunkErr.name==="AbortError")throw chunkErr;degraded++,segs=audiobookSplitByQuotes(chunks[i]),view.note(`\u274C Passage ${i+1} \u2014 unexpected error (${chunkErr.message}) \u2014 auto-detected dialogue instead`)}{const _rec=_abRecoverDroppedText(chunks[i],segs);_rec.recovered&&(segs=_rec.segs,recoveredGaps+=_rec.recovered,view.note(`\u26A0\uFE0F Passage ${i+1} \u2014 recovered ${_rec.recovered} sentence/paragraph gap${_rec.recovered!==1?"s":""} the LLM dropped`))}audiobookResolveUnknowns(segs,allSegments2.slice(-2),roster),segs.forEach(s=>{const speakerName=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator"?"Narrator":s.speaker;!/^Unknown|Unbekannt/i.test(speakerName)&&!roster.includes(speakerName)&&roster.push(speakerName)}),segs.forEach(s=>{s.page=_curPageNum,allSegments2.push(s)}),view.addSegments(segs),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)}view.update(_audiobook.cancel?completedChunks:chunks.length)}catch(err){err.name==="AbortError"?(_audiobook.cancel=!0,view.note("Casting stopped. Completed passages were preserved.")):(_audiobook.cancel=!0,view.note(`\u274C Casting stopped \u2014 unexpected error: ${err.message}`))}finally{_abStopDraftAutosave(),_audiobook.running=!1,_audiobook.abort=null,_abHideCastingControl()}if(!allSegments2.length){if(_audiobook.cancel){view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}view.done(),toast("No segments produced","error");return}try{const _quoteFixedSegs=_audiobookFixOrphanedQuoteMarks(allSegments2),{segments:_rejoined,merged:_mergedCount}=_audiobookMergeSplitSentences(_quoteFixedSegs);_mergedCount&&console.info("[cast] rejoined",_mergedCount,"sentence(s) split by quoted terms");const _mergedSegs=_audiobookMergeAdjacentSameSpeaker(_rejoined);allSegments2.length=0,allSegments2.push(..._mergedSegs);{const _ph=_audiobookDropPhantomSpeakers(allSegments2);_ph.dropped&&console.info("[cast] dropped",_ph.dropped,"phantom speaker line(s):",_ph.names.join(", "))}{const _role=_audiobookResolveRoleAliases(allSegments2,text);if(_role.moved){console.info("[cast] resolved role labels to names:",_role.pairs.join(", "),`(${_role.moved} lines)`);try{toast(`Resolved ${_role.merged} role label${_role.merged!==1?"s":""} to character names (${_role.moved} lines)`,"success")}catch{}}}{const _ident=_audiobookConsolidateSpeakerAliases(allSegments2);if(_ident.merged){console.info("[cast] consolidated",_ident.merged,"speaker label variant(s),",_ident.moved,"line(s)");try{toast(`Merged ${_ident.merged} duplicate character label${_ident.merged!==1?"s":""} (${_ident.moved} lines)`,"success")}catch{}}}_abStampConfidence(allSegments2),_audiobook.segments=allSegments2,_audiobook.lastText=text,_audiobook.roster=roster,_audiobook.narratedPassages=narrationOnly,_audiobook.degraded=degraded,_audiobook.recoveredGaps=recoveredGaps,_audiobook.completedChunks=_audiobook.cancel?completedChunks:chunks.length,_audiobook.completedTotal=chunks.length,_abSaveDraft(allSegments2,roster,text,_audiobook.completedChunks,chunks.length),audiobookSaveAsRehearsal({silent:!0}),_audiobook.cancel&&toast("Casting stopped early. Progress preserved.","info");const speakers=new Set(allSegments2.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),stoppedEarly=_audiobook.cancel&&completedChunks(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length>10&&setTimeout(()=>{audiobookAutoRepairCast().catch(e=>console.warn("[auto-repair]",e))},800)}catch(e){console.error("[audiobookCast] post-processing failed",e),view.done({stopped:!0,message:"Finished casting, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}async function audiobookOpenCurrentInRehearser(){var _a2;const segs=_audiobook.segments||[];if(!segs.length)return toast("No cast to open","error"),!1;if(typeof window.rehLoadRecord=="function"&&typeof _audiobookBuildRehRecord=="function")try{const rec=await _audiobookBuildRehRecord(segs);return _audiobook.rehId&&(rec.id=_audiobook.rehId),(_a2=document.getElementById("audiobook-preview"))==null||_a2.remove(),typeof navTo=="function"&&navTo("s-rehearser"),window.rehLoadRecord(rec),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),toast("Opened cast in Script Rehearser","success"),audiobookSaveAsRehearsal({silent:!0}).catch(()=>{}),!0}catch(err){console.warn("[audiobook] failed to build rehearser record directly, falling back:",err)}await audiobookSaveAsRehearsal({silent:!0});const{script,emotions}=audiobookBuildScript(segs);return await audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),!0}function audiobookShowPreview(){var _a2;audiobookOpenCurrentInRehearser()}function audiobookApplyPreviewAndOpen(){const segs=_audiobook.segments;document.querySelectorAll("#audiobook-seglist .audiobook-seg-sp").forEach(inp=>{const i=+inp.dataset.i,v=inp.value.trim()||"Narrator";segs[i].speaker=v,segs[i].type=v.toLowerCase()==="narrator"?"narration":"dialogue"}),document.querySelectorAll("#audiobook-seglist .audiobook-seg-emo").forEach(inp=>{const i=+inp.dataset.i;segs[i].emotion=inp.value.trim()});const{script,emotions}=audiobookBuildScript(segs);audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions)}function audiobookBuildScript(segments){let script="";const emotions=[],marks=_audiobook.pageMarks||[],src=_audiobook.lastText||"";let markIdx=0,searchPos=0;marks.length&&marks[0].offset<=2&&(markIdx=1);for(const s of segments){const t=(s.text||"").trim();if(!t)continue;if(src&&markIdx=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)script.trim()&&(script+=` -\f${marks[markIdx].page+1} -`),markIdx++}s.type==="dialogue"&&s.speaker&&s.speaker.toLowerCase()!=="narrator"?(script+=` -`+s.speaker.toUpperCase()+` -`+t+` -`,emotions.push(s.emotion||"")):script+=` -`+t+` -`}return{script:script.trim(),emotions}}async function audiobookOpenInRehearser(script,title,dialogueEmotions){$("reh-script-text")&&($("reh-script-text").value=script),$("reh-script-title")&&($("reh-script-title").value=title),typeof navTo=="function"&&navTo("s-rehearser");const btn=$("reh-parse-btn");btn?btn.click():typeof parseScript=="function"&&(rehState.lines=parseScript(script));let speakers=0,lines=0;if(window.rehState&&Array.isArray(rehState.lines)){let k=0;rehState.lines.forEach(l=>{if(l.type==="dialog"){const e=dialogueEmotions[k++];e&&(l.emotion=e),lines++}}),speakers=Object.keys(rehState.cast||{}).filter(s=>!String(s).includes("NARRATOR")).length}let saved=!1;if(typeof saveToLibrary=="function")try{rehState.savedId=null,await saveToLibrary(),saved=!0}catch{}toast(`Cast ${speakers} character${speakers!==1?"s":""} \xB7 ${lines} lines`+(saved?" \u2014 saved to Rehearser \u2192 Bibliothek":""),"success")}function audiobookIsChapter(line){if(line.type==="act"||line.type==="scene")return!0;const t=(typeof stripMarkdown=="function"?stripMarkdown(line.text||""):line.text||"").trim();if(!t||t.length>60)return!1;const NUM=/^(?:[ivxlcdm]+(?=[.\s]|$)|\d{1,3})\.?\s*/i,rest=t.replace(NUM,""),KEYWORD=/^(chapter|kapitel|chap\.?|part|book|prologue|epilogue|prolog|epilog|teil)\b/i;if(!KEYWORD.test(rest))return!1;const afterKeyword=rest.replace(KEYWORD,"").trim();return afterKeyword===""||/^[ivxlcdm\d]{1,6}\.?$/i.test(afterKeyword)?!0:/^[ivxlcdm\d]{0,6}\.?\s*[-:–—]\s*.{1,40}$/i.test(afterKeyword)}const _AB_DE_ORDINAL_STEM_1_19=["","erst","zweit","dritt","viert","f\xFCnft","sechst","siebt","acht","neunt","zehnt","elft","zw\xF6lft","dreizehnt","vierzehnt","f\xFCnfzehnt","sechzehnt","siebzehnt","achtzehnt","neunzehnt"],_AB_DE_CARDINAL_TENS={2:"zwanzig",3:"drei\xDFig",4:"vierzig",5:"f\xFCnfzig",6:"sechzig",7:"siebzig",8:"achtzig",9:"neunzig"},_AB_DE_CARDINAL_ONES=["","ein","zwei","drei","vier","f\xFCnf","sechs","sieben","acht","neun"];function _abGermanOrdinalWord(n,gender){let stem;if(n>=1&&n<=19)stem=_AB_DE_ORDINAL_STEM_1_19[n];else if(n>=20&&n<=99){const tens=_AB_DE_CARDINAL_TENS[Math.floor(n/10)],ones=n%10;if(!tens)return null;stem=(ones?_AB_DE_CARDINAL_ONES[ones]+"und":"")+tens+"st"}else return null;return stem+(gender==="m"?"er":"es")}const _AB_DE_HEADING_GENDER={kapitel:"n",buch:"n",teil:"m"};function _abSpokenChapterHeading(text,language){if(!/^de/i.test(language||""))return text;const m=/^(\d{1,2})\.\s*(Kapitel|Teil|Buch)\b(.*)$/i.exec((text||"").trim());if(!m)return text;const gender=_AB_DE_HEADING_GENDER[m[2].toLowerCase()]||"n",word=_abGermanOrdinalWord(parseInt(m[1],10),gender);return word?`${word[0].toUpperCase()}${word.slice(1)} ${m[2]}${m[3]}`:text}function _abChapterLabels(buckets){const frontMatter=buckets.length>1&&!buckets[0].title;let num=0;return buckets.map((b,i)=>frontMatter&&i===0?"Front matter":"Chapter "+ ++num+(b.title?": "+b.title:""))}function _abPauseSettings(){const num=(key,def)=>{let v;try{v=parseFloat(localStorage.getItem(key))}catch{v=NaN}return Number.isFinite(v)&&v>=0?v:def};return{paragraphMs:num("ttsvc_ab_pause_paragraph_s",2)*1e3,chapterMs:num("ttsvc_ab_pause_chapter_s",4)*1e3}}function _abSetPauseSetting(key,seconds){try{localStorage.setItem(key,String(seconds))}catch{}}function _abChapterSfxDataUrl(){try{return localStorage.getItem("ttsvc_ab_chapter_sfx")||null}catch{return null}}function _abSetChapterSfx(dataUrl){try{dataUrl?localStorage.setItem("ttsvc_ab_chapter_sfx",dataUrl):localStorage.removeItem("ttsvc_ab_chapter_sfx")}catch{toast("Could not save chapter sound \u2014 file may be too large","error")}}async function _abChapterSfxWavBlob(fmt){const dataUrl=_abChapterSfxDataUrl();if(!dataUrl)return null;try{const arrayBuf=await(await fetch(dataUrl)).arrayBuffer(),AC=window.AudioContext||window.webkitAudioContext,OAC=window.OfflineAudioContext||window.webkitOfflineAudioContext,tmpCtx=new AC,decoded=await tmpCtx.decodeAudioData(arrayBuf.slice(0));typeof tmpCtx.close=="function"&&tmpCtx.close();const offline=new OAC(fmt.channels,Math.max(1,Math.ceil(decoded.duration*fmt.sampleRate)),fmt.sampleRate),src=offline.createBufferSource();src.buffer=decoded,src.connect(offline.destination),src.start();const rendered=await offline.startRendering(),frames=rendered.length,pcm=new Int16Array(frames*fmt.channels);for(let ch=0;ch_abSetPauseSetting("ttsvc_ab_pause_paragraph_s",Math.max(0,parseFloat(pEl.value)||0))),cEl.addEventListener("change",()=>_abSetPauseSetting("ttsvc_ab_pause_chapter_s",Math.max(0,parseFloat(cEl.value)||0))),sfxClear==null||sfxClear.addEventListener("click",()=>{_abSetChapterSfx(null),sfxFile&&(sfxFile.value=""),sfxCurrent&&(sfxCurrent.textContent="No custom sound")}),sfxFile==null||sfxFile.addEventListener("change",()=>{const file=sfxFile.files&&sfxFile.files[0];if(!file)return;if(file.size>2*1024*1024){toast("Chapter sound is too large (max 2 MB) \u2014 trim it to a short chime/sting","error"),sfxFile.value="";return}const reader=new FileReader;reader.onload=()=>{_abSetChapterSfx(reader.result),sfxCurrent&&(sfxCurrent.textContent=file.name)},reader.onerror=()=>toast("Could not read that audio file","error"),reader.readAsDataURL(file)}))}window._abInitPauseUI=_abInitPauseUI;function audiobookLineVoice(l){if(l.type==="dialog"){const c=rehState.cast[l.speaker]||{};return{voice:c.voice,instruct:typeof _buildInstruct=="function"?_buildInstruct(c.instruct,l.emotion,c.voice):""}}return{voice:rehState.narratorVoice,instruct:""}}async function _abSynthesizeLongText(voice,text,instruct,backend){const pieces=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,900):[text];if(pieces.length<=1)return fetchTtsPreviewBlob(voice,text,"wav",instruct,backend);const blobs=[];for(const piece of pieces)blobs.push(await fetchTtsPreviewBlob(voice,piece,"wav",instruct,backend));return mergeWavBlobs(blobs,120)}async function audiobookExport(opts={}){var _a2,_b2;if(_abBusyGuard("Export"))return;if(!window.rehState||!(rehState.lines||[]).length){toast("Open a script in the rehearser first","error");return}if(!rehState.backend){toast("Select a TTS backend in the rehearser first","error");return}typeof _ensureNarrator=="function"&&_ensureNarrator();const speakable=i=>{const l=rehState.lines[i];if(!l||l.ignored||l.hidden)return!1;if(l.type==="dialog"){const c=rehState.cast[l.speaker];return!!(c&&c.voice&&c.voice!=="me")}return!!(rehState.narratorVoice&&(l.text||"").trim())},buckets=[];let cur=null;rehState.lines.forEach((l,i)=>{audiobookIsChapter(l)&&(cur={title:(typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text).trim().slice(0,50),idx:[]},buckets.push(cur)),speakable(i)&&(cur||(cur={title:"",idx:[]},buckets.push(cur)),cur.idx.push(i))});let activeBuckets=buckets;opts.selectedChapterIndices&&(activeBuckets=buckets.filter((_,i)=>opts.selectedChapterIndices.includes(i)));const allIdx=activeBuckets.flatMap(b=>b.idx);if(!allIdx.length){toast("Nothing to synthesise \u2014 cast voices first","error");return}const activeLabels=_abChapterLabels(activeBuckets),lineChapterPos=new Map;activeBuckets.forEach((b,pos)=>b.idx.forEach(idx=>lineChapterPos.set(idx,pos)));const progLabel=(doneCount,i)=>{const label=activeLabels[lineChapterPos.get(i)];return(label?label+" \xB7 ":"")+`Synthesising line ${doneCount} / ${allIdx.length}\u2026`};_audiobook.running=!0,_audiobook.cancel=!1;const prog=audiobookProgress(allIdx.length),wavClips=new Map,failedLines=[];let done=0;const queue=allIdx.slice(),worker=async()=>{for(;queue.length&&!_audiobook.cancel;){const i=queue.shift();if(rehState.synthCache.has(i)&&!rehState.staleLines.has(i)){wavClips.set(i,rehState.synthCache.get(i)),prog.update(++done,progLabel(done,i));continue}try{const l=rehState.lines[i],{voice,instruct}=audiobookLineVoice(l);let text=typeof _rehInlineTone=="function"?_rehInlineTone(stripMarkdown(l.text),l.emotion):typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text;l.type!=="dialog"&&audiobookIsChapter(l)&&(text=_abSpokenChapterHeading(text,audiobookLang()));let blob=null,cacheKey=null,book=null;if(typeof _lineAudioCacheKey=="function"&&(book=_lineAudioBookName(),cacheKey=await _lineAudioCacheKey(text,voice,instruct),blob=await _lineAudioCacheGet(book,cacheKey)),!blob){let lastErr;for(let attempt=1;attempt<=3;attempt++)try{blob=await _abSynthesizeLongText(voice,text,instruct,_ttsBackendForVoice(voice,rehState.backend)),lastErr=null;break}catch(e){lastErr=e,attempt<3&&await new Promise(r=>setTimeout(r,3e3*attempt))}if(lastErr)throw lastErr;cacheKey&&_lineAudioCachePut(book,cacheKey,blob)}wavClips.set(i,blob),rehState.synthCache.set(i,blob)}catch(e){failedLines.push(i),console.error("[audiobook export] synth failed for line",i,e)}prog.update(++done,progLabel(done,i))}};try{await worker()}catch(e){console.error("[audiobook export] worker crashed:",e),toast("Export stopped unexpectedly: "+(e.message||e),"error");return}finally{prog.done(),_audiobook.running=!1}if(_audiobook.cancel){toast("Export cancelled","error");return}if(failedLines.length){toast(failedLines.length+" line(s) failed to synthesise \u2014 retry them (Script Rehearser \u2192 re-synthesise stale) before exporting, or they will silently drop from the audiobook","error");return}const title=typeof readerSafeName=="function"?readerSafeName(((_a2=$("reh-script-title"))==null?void 0:_a2.value)||"Audiobook"):((_b2=$("reh-script-title"))==null?void 0:_b2.value)||"Audiobook",realChapters=buckets.filter(b=>b.title).length>0;let files=0;const savedFiles=[],bookForExport=_lineAudioBookName(),_folderRaw=(opts.folder||"").trim().replace(/\/+$/,""),folder=_folderRaw&&_folderRaw.toLowerCase()!=="exports"?_folderRaw:"",folderQS=folder?"?folder="+encodeURIComponent(folder):"";try{const key="ttsvc_export_folder:"+bookForExport;folder?localStorage.setItem(key,folder):localStorage.removeItem(key)}catch{}const encMsg=$("audiobook-msg"),pauses=_abPauseSettings();if(opts.format==="single"){let masterBlobs=[];for(let c=0;cwavClips.get(i)).filter(Boolean);if(!blobs.length)continue;encMsg&&(encMsg.textContent=`Merging chapter ${c+1} / ${activeBuckets.length}\u2026`);let mergedWav=await mergeWavBlobs(blobs,pauses.paragraphMs);if(c>0&&activeBuckets[c].title&&(pauses.chapterMs>0||_abChapterSfxDataUrl())){const ref=_parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt,lead=[],sfx=await _abChapterSfxWavBlob(ref);sfx&&lead.push(sfx),pauses.chapterMs>0&&lead.push(_buildWavBlob(_silencePcmBytes(pauses.chapterMs,ref),ref)),lead.length&&(mergedWav=await mergeWavBlobs([...lead,mergedWav]))}masterBlobs.push(mergedWav)}if(masterBlobs.length){encMsg&&(encMsg.textContent="Encoding full book to MP3\u2026");let finalWav=await mergeWavBlobs(masterBlobs,0),blob=finalWav;try{const encResp=await fetch("/api/audio/encode-mp3",{method:"POST",body:finalWav});encResp.ok&&(blob=await encResp.blob())}catch{}const name=`${title}.${blob===finalWav?"wav":"mp3"}`;typeof readerDownload=="function"&&readerDownload(blob,name);try{const exportUrl=`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}${folderQS}`;(await fetch(exportUrl,{method:"POST",body:blob})).ok&&savedFiles.push({name,url:exportUrl})}catch{}files++}}else for(let c=0;cwavClips.get(i)).filter(Boolean);if(!blobs.length)continue;encMsg&&(encMsg.textContent=`Merging chapter ${c+1} / ${activeBuckets.length}\u2026`);let mergedWav=await mergeWavBlobs(blobs,pauses.paragraphMs);if(c>0&&activeBuckets[c].title&&(pauses.chapterMs>0||_abChapterSfxDataUrl())){const ref=_parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt,lead=[],sfx=await _abChapterSfxWavBlob(ref);sfx&&lead.push(sfx),pauses.chapterMs>0&&lead.push(_buildWavBlob(_silencePcmBytes(pauses.chapterMs,ref),ref)),lead.length&&(mergedWav=await mergeWavBlobs([...lead,mergedWav]))}encMsg&&(encMsg.textContent=`Encoding chapter ${c+1} / ${activeBuckets.length}\u2026`);let blob=mergedWav;try{const encResp=await fetch("/api/audio/encode-mp3",{method:"POST",body:mergedWav});encResp.ok?blob=await encResp.blob():console.error("[audiobook export] mp3 encode failed, shipping wav instead:",encResp.status)}catch(e){console.error("[audiobook export] mp3 encode request failed, shipping wav instead:",e)}const ext=blob===mergedWav?"wav":"mp3",ch=buckets[c].title?" "+readerSafeName(buckets[c].title):"",name=realChapters||buckets.length>1?`${title} - ${String(c+1).padStart(2,"0")}${ch}.${ext}`:`${title}.${ext}`;typeof readerDownload=="function"&&readerDownload(blob,name);try{const saveUrl=`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}${folderQS}`;(await fetch(saveUrl,{method:"POST",body:blob})).ok&&savedFiles.push({name,url:saveUrl})}catch{}files++,await new Promise(r=>setTimeout(r,400))}toast("Exported audiobook \xB7 "+files+(realChapters?" chapter file(s)":" file(s)"),"success"),savedFiles.length&&typeof _abShowExportResults=="function"&&_abShowExportResults(bookForExport,savedFiles,{folder})}function _abShowExportResults(book,files,opts={}){var _a2;(_a2=document.getElementById("ab-export-results"))==null||_a2.remove();const ov=document.createElement("div");ov.id="ab-export-results",ov.className="audiobook-overlay";const folderQS=opts.folder?"?folder="+encodeURIComponent(opts.folder):"",zipUrl=`/api/audiobook-export/${encodeURIComponent(book)}/zip${folderQS}`;ov.innerHTML=`
-
${opts.browsing?"Saved audiobook exports":"Audiobook exported"}
- ${opts.browsing?"":`
Export complete \u2014 ${files.length} file${files.length===1?"":"s"} saved
`} -

Saved on the server${opts.browsing?"":", in case the browser\u2019s own download went somewhere you don\u2019t check"} \u2014 come back and download or play any of these again any time, without re-exporting.

-
- ${files.map((f,i)=>` -
- - ${escHtml(f.name)} - Download -
`).join("")} -
- ${files.length>1?``:""} -
`,document.body.appendChild(ov);const audioEl=new Audio;let playingIdx=null;const setIcon=(idx,playing)=>{const icon=ov.querySelector(`.ab-export-file-row[data-idx="${idx}"] .ab-export-play-btn .mdi`);icon&&(icon.className="mdi "+(playing?"mdi-pause":"mdi-play"))},stop=()=>{audioEl.pause(),playingIdx!==null&&setIcon(playingIdx,!1),playingIdx=null};ov.querySelectorAll(".ab-export-play-btn").forEach(btn=>{btn.addEventListener("click",()=>{const idx=parseInt(btn.closest(".ab-export-file-row").dataset.idx,10);if(playingIdx===idx&&!audioEl.paused){stop();return}const prevIdx=playingIdx;audioEl.src=files[idx].url,audioEl.play().catch(e=>toast("Could not play: "+(e.message||e),"error")),prevIdx!==null&&setIcon(prevIdx,!1),playingIdx=idx,setIcon(idx,!0)})}),audioEl.addEventListener("ended",stop);const close=()=>{stop(),ov.remove()};ov.querySelector("#ab-export-close").addEventListener("click",close),ov.addEventListener("click",e=>{e.target===ov&&close()})}async function audiobookBrowseExports(){var _a2;const book=typeof _lineAudioBookName=="function"?_lineAudioBookName():((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled";let folder="";try{folder=localStorage.getItem("ttsvc_export_folder:"+book)||""}catch{}const folderQS=folder?"?folder="+encodeURIComponent(folder):"";try{const r=await fetch(`/api/audiobook-export/${encodeURIComponent(book)}${folderQS}`);if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();if(!d.files||!d.files.length){toast('No saved exports yet for this book \u2014 run "Audiobook" first',"info");return}const files=d.files.map(f=>({name:f.name,url:`/api/audiobook-export/${encodeURIComponent(book)}/${encodeURIComponent(f.name)}${folderQS}`}));_abShowExportResults(book,files,{browsing:!0,folder})}catch(e){toast("Could not load saved exports: "+(e.message||e),"error")}}(_qc=$("reh-tb-browse-exports"))==null||_qc.addEventListener("click",audiobookBrowseExports),(_rc=$("reader-audiobook-btn"))==null||_rc.addEventListener("click",audiobookOpenCastView),(_sc=$("reh-tb-audiobook"))==null||_sc.addEventListener("click",audiobookExportDialog);async function _audiobookBuildRehRecord(segs){const{script,emotions}=audiobookBuildScript(segs),title=typeof readerState!="undefined"&&readerState.title?readerState.title:"Audiobook",lines=typeof parseScript=="function"?parseScript(script):[];if(emotions&&emotions.length){let eIdx=0;lines.forEach(l=>{l.type==="dialog"&&eIdx{cast[sp]={voice:def.voice,color:def.color,instruct:"",lang:"",gender:"",tags:"",soul:"",ignored:!1,hidden:!1,voiceData:null}})}let existing=null;if(_audiobook.rehId&&typeof window.rehDbGetById=="function")try{existing=await window.rehDbGetById(_audiobook.rehId)}catch{}existing&&existing.cast&&Object.entries(existing.cast).forEach(([sp,info])=>{cast[sp]?cast[sp]={...cast[sp],...info}:cast[sp]=info});const emotions_map={};return lines.forEach((l,i)=>{l.type==="dialog"&&l.emotion&&(emotions_map[i]=l.emotion)}),{title,script,cast,emotions:emotions_map,notes:existing?existing.notes||{}:{},ignored:existing?existing.ignored||{}:{},hidden:existing?existing.hidden||{}:{},backend:existing&&existing.backend||"",narratorVoice:existing&&existing.narratorVoice||"",lineIndex:existing&&existing.lineIndex||0,clips:existing?existing.clips||[]:[],created:existing?existing.created:new Date,updated:new Date}}let _abRehSaveTimer=null;async function audiobookSaveAsRehearsal(opts){const silent=opts&&opts.silent,segs=_audiobook.segments;if(!segs||!segs.length){silent||toast("No segments to save","error");return}if(typeof rehDbAdd!="function"){silent||toast("Rehearser DB not available","error");return}try{const rec=await _audiobookBuildRehRecord(segs);_audiobook.rehId?(rec.id=_audiobook.rehId,await rehDbPut(rec)):_audiobook.rehId=await rehDbAdd(rec),typeof renderLibraryList=="function"&&renderLibraryList(),silent||(_abClearDraft(),toast("Saved as Script Rehearsal","success"))}catch(e){silent?console.warn("[audiobook] auto-save failed:",e):toast("Failed to save rehearsal: "+e.message,"error")}}function _audiobookDebouncedSave(){clearTimeout(_abRehSaveTimer),_abRehSaveTimer=setTimeout(()=>audiobookSaveAsRehearsal({silent:!0}),1500)}function audiobookExportDialog(){var _a2,_b2,_c2;if(_audiobook.running)return;if(!window.rehState||!(rehState.lines||[]).length){toast("Open a script in the rehearser first","error");return}if(!rehState.backend){toast("Select a TTS backend in the rehearser first","error");return}const speakable=i=>{const l=rehState.lines[i];if(!l||l.ignored||l.hidden)return!1;if(l.type==="dialog"){const c=rehState.cast[l.speaker];return!!(c&&c.voice&&c.voice!=="me")}return!!(rehState.narratorVoice&&(l.text||"").trim())},buckets=[];let cur=null;rehState.lines.forEach((l,i)=>{audiobookIsChapter(l)&&(cur={title:(typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text).trim().slice(0,50),idx:[]},buckets.push(cur)),speakable(i)&&(cur||(cur={title:"",idx:[]},buckets.push(cur)),cur.idx.push(i))});const listEl=document.getElementById("ab-export-chapter-list");if(listEl){listEl.innerHTML="";const countEl=document.getElementById("ab-export-chapter-count");countEl&&(countEl.textContent=buckets.length);const labels=_abChapterLabels(buckets);buckets.forEach((b,i)=>{const lbl=document.createElement("label");lbl.style.display="block",lbl.style.marginBottom="4px";const chk=document.createElement("input");chk.type="checkbox",chk.checked=!0,chk.value=i,chk.className="ab-export-chapter-cb",lbl.appendChild(chk),lbl.appendChild(document.createTextNode(" "+labels[i])),listEl.appendChild(lbl)})}const warn=document.getElementById("reh-tone-warn"),slot=document.getElementById("ab-export-tone-warn-slot");warn&&slot&&(warn.hidden=!1,slot.appendChild(warn)),typeof _abInitPauseUI=="function"&&_abInitPauseUI();const modal=document.getElementById("audiobook-export-modal");modal&&(modal.parentElement!==document.body&&(document.body.appendChild(modal),(_a2=document.getElementById("ab-export-modal-close"))==null||_a2.addEventListener("click",()=>modal.style.display="none"),(_b2=document.getElementById("ab-export-modal-cancel"))==null||_b2.addEventListener("click",()=>modal.style.display="none"),(_c2=document.getElementById("ab-export-modal-start"))==null||_c2.addEventListener("click",()=>{modal.style.display="none";const formatEl=document.querySelector('input[name="ab_export_format"]:checked'),format=formatEl?formatEl.value:"chapter",folderEl=document.getElementById("ab-export-server-folder"),folder=folderEl?folderEl.value:"",selectedChapterIndices=Array.from(document.querySelectorAll(".ab-export-chapter-cb")).filter(cb=>cb.checked).map(cb=>parseInt(cb.value,10));audiobookExport({format,selectedChapterIndices,folder})})),modal.style.display="flex")}async function audiobookFetchWithTimeout(url,options={},timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const parentSignal=options.signal,ac=new AbortController;let timedOut=!1;const timer=setTimeout(()=>{timedOut=!0,ac.abort()},timeoutMs),onParentAbort=()=>ac.abort(parentSignal==null?void 0:parentSignal.reason);parentSignal&&(parentSignal.aborted?onParentAbort():parentSignal.addEventListener("abort",onParentAbort,{once:!0}));try{return await fetch(url,{...options,signal:ac.signal})}catch(err){if(timedOut){const timeoutErr=new Error(`Timed out after ${Math.ceil(timeoutMs/1e3)}s`);throw timeoutErr.name="TimeoutError",timeoutErr}throw err}finally{clearTimeout(timer),parentSignal&&parentSignal.removeEventListener("abort",onParentAbort)}}function audiobookTimeoutSeconds(timeoutMs){return Math.max(5,Math.round(timeoutMs/1e3))}async function audiobookAttributeStream(body,view,outerSignal,idleTimeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/attribute-dialogue/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify({...body,want_reasoning:!0})});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(` - -`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&(view!=null&&view.thinking)&&view.thinking(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}catch(err){throw(err==null?void 0:err.name)==="AbortError"&&!(outerSignal&&outerSignal.aborted)?new Error("stream idle timeout"):err}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}const CS_CHUNK_CHARS=4e3,_cs={running:!1,cancel:!1,cache:{}},CS_DEFAULT_PROMPT=`You are an expert dramaturge, developmental editor, and tabletop RPG game master building rich character sheets passage by passage as a book is read. Extract playable, action-oriented sheets an actor can use to immediately know how to PLAY the character. - PROGRESSIVE FILLING: you may be given the sheets built so far. For returning characters, ADD any NEW detail this passage reveals and refine vague fields; do not contradict solid earlier facts or blank out a field you cannot improve. In this cast-character pass, ONLY refine the already-casted roster and do NOT invent new profiles, places, or institutions. The text may be a focused evidence window around a mention, so use the nearby paragraphs as context. Leave a field empty if the book genuinely hasn't shown it yet (a later passage can fill it). Extrapolate from dialogue and actions when reasonable, and mark any deduced value with a trailing ' *'. -For each character output these fields: -- name: canonical display name for this one character. Use the real personal name if known; otherwise use the most stable role/title. -- aliases: ONLY alternate names, roles, epithets, mistranscriptions, and titles proven to refer to the SAME character, comma-separated (max 6 items; e.g. 'the Executioner, Bloodfang, Marcus the Executioner'). Leave empty when uncertain. - - first_name, last_name: split the character identity when known. Leave unknown parts empty. - - title: nobility title only, if the text explicitly gives one (e.g. 'Graf', 'Baron', 'Ritter'). - - profession: occupation / job / role in the story (e.g. 'Inquisitor', 'Soldier', 'Merchant', 'Priest'). -- age_estimate: estimated age or age range, if inferable -- race_species: species, race, or kind (human, elf, ork, vampire, etc.) if relevant -- languages: spoken languages / dialects / tongues, comma-separated if multiple -- nationality_background: homeland, culture, origin, or social background if known -- social_class: rank or class if the text makes it clear (noble, soldier, slave, merchant, priesthood, etc.) -- archetype: a two-word role summary (e.g. 'Ruthless Scholar') -- gender: 'male', 'female', or 'nonbinary' \u2014 as apparent from the text (pronouns, roles, physical description). Leave empty if genuinely indeterminable. -- physical: the character's BASELINE appearance \u2014 how they normally look through most of the book. Height, weight, build, hair, eyes, skin, posture, gait, permanent distinguishing features (scars they carry throughout, lasting disabilities), fantasy-specific extras. Use metric units when size/weight is known. Do NOT describe a temporary state: wounds received in one scene, blood, gore, nudity, dirt, being maimed, flayed, transformed, dying or dead, unconscious, or how they look at their worst or most monstrous moment. Those belong in 'notes' or 'arc_note'. If the book only ever describes this character vividly during such a scene, still write their ORDINARY appearance here (clothed, uninjured, in their normal form) and infer it from their role, rank and origin, marking inferred traits with '*'. This field is used verbatim to generate the character's portrait, so a transient injury or transformation written here becomes their permanent face. -- clothing: day-to-day wear, work attire, formal wear, sleepwear, undergarments, accessories, and visible weapons/gear if they define the look -- alignment: strict moral code + the one line they will never cross -- moral_alignment_score: integer 0\u2013100. 100 = purely good/heroic, 0 = purely evil/villainous, 50 = neutral/ambiguous -- arc_direction: one of: 'stable-good', 'stable-bad', 'neutral', 'good-to-bad', 'bad-to-good', 'complex' -- arc_note: one sentence explaining the arc or moral position visible so far -- attribute_high / attribute_low: highest and lowest natural attribute (Charisma, Intelligence, Wisdom, Agility\u2026) -- skills: what they are demonstrably good at in the story -- capabilities: combat, magic, social, technical, or other demonstrated abilities -- backstory: origin, formative background and history revealed in the text -- relationships: key allies, family, rivals and enemies \u2014 name them and how they relate -- motivation: the inner drive \u2014 WHY they pursue what they pursue (distinct from the win condition) -- fears: their deepest fears, phobias or dread -- mannerisms: habitual gestures, tics, body language, habits and quirks -- communication_style: how they communicate socially \u2014 blunt, formal, warm, guarded, sarcastic, etc. -- voice_pattern: speech style \u2014 accent, pacing, vocabulary, register and verbal tics (for voice casting) -- voice_design_prompt: concise English voice-design prompt, maximum 480 characters. Use only objective, audible traits; no lore-only labels or repeated synonyms. Include gender as a hard audible constraint, age impression, pitch, timbre, pace, accent/register, emotion and audiobook use. Translate species/role into audible cues (orc: chest-led/gravel-edged; vampire: cool/velvety/controlled; elf: airy/precise/adult; demon: dark/subtly otherworldly; warrior: disciplined command cadence; blacksmith: work-worn/practical; mage: measured/precise). Give one distinctive vocal fingerprint. Do NOT mention plot spoilers. -- image_prompt: detailed English image-generation prompt describing WHO this character is: face, age impression, build, hair/eyes/skin if known, clothing, posture, props, visible symbols, and overall bearing. Mark inferred traits with '*'. Describe their normal appearance, never a wound or death scene. Describe ONLY the person: face, hair, build, clothing and what they carry. Do NOT describe a setting, location, landscape, room, weather, background, or an action they are performing (no "standing in a grassy landscape", "riding a horse", "the background shows a campfire"), and do NOT specify an art style, medium, rendering technique, framing or lighting (no "painterly", "illustration", "concept art", "dramatic lighting"). The application appends its own framing and rendering directive; scene or style written here fights it and turns the casting portrait into a full-body scene. -- forms: array of distinct transformations, disguises, or physical appearances for this same character (empty when none). Each form is {id, name, physical, clothing, image_prompt, concept_art_prompt, inventory}; keep one character and one voice, and only add a form when the text establishes a genuinely different appearance. -- inventory: 1-3 defining items/props/clothing (array of short strings) -- secret: dark secret or fatal flaw -- conflict_style: fight, flight, or manipulate \u2014 how they act when cornered -- win_condition: the specific event that would make them feel they have won -- reputation: how other characters or society see them -- religious_beliefs: faith, religion, worship, or lack of belief if the text shows it -- notes: brief catch-all notes for useful details that do not fit elsewhere -- tier: 'main' or 'supporting' -- sources: array of {page, quote, line_hint} \u2014 the page number from the nearest [p.N] marker, a short verbatim quote that supports the sheet (1-12 entries), and a brief label (e.g. 'physical', 'clothing', 'relationships', 'motivation'). Use null page if unknown. line_hint MUST name the supported field when possible: physical, clothing, relationships, motivation, fears, mannerisms, voice_pattern, backstory, alignment, skills, capabilities, secret, conflict_style, or win_condition. -IDENTITY MERGING: A character may appear under multiple names in the book (first name, last name, title, role, alias, nickname). Examples: 'Marcus', 'the Executioner', and 'Bloodfang' may all refer to ONE profile if context shows they are the same person. Do NOT create separate sheets for aliases/titles of the same person; put the alternate forms in aliases/title and keep one canonical name. -STRICT PER-CHARACTER ATTRIBUTION: a passage often describes several characters together (a leader addressing followers, a group scene, a comparison between two people). Attribute every fact \u2014 title, role, profession, race/species, physical trait, relationship, backstory detail, or any other field \u2014 ONLY to the character it explicitly and specifically describes. NEVER copy a detail onto another character's sheet just because that character appears in the same scene, sentence, or paragraph, is compared or contrasted with them, or belongs to the same faction/group (e.g. only the actual leader of a group gets 'leader' in their profession/title \u2014 the group's other members do NOT). If it is ambiguous which character a detail belongs to, leave it out of every sheet rather than guessing. -Reuse the EXACT names from the known-characters list for returning characters when they are the canonical name or an alias of this character. Do not merge characters merely because their names appear near each other, in the known-character list, or in relationships. -Respond with STRICT JSON only: -{"sheets":[{"name":"","aliases":"","first_name":"","last_name":"","title":"","profession":"","age_estimate":"","race_species":"","languages":"","nationality_background":"","social_class":"","archetype":"","gender":"","physical":"","clothing":"","alignment":"","moral_alignment_score":50,"arc_direction":"neutral","arc_note":"","attribute_high":"","attribute_low":"","skills":"","capabilities":"","backstory":"","relationships":"","motivation":"","fears":"","mannerisms":"","communication_style":"","voice_design_prompt":"","image_prompt":"","forms":[],"inventory":[],"secret":"","conflict_style":"","win_condition":"","reputation":"","religious_beliefs":"","notes":"","tier":"main","sources":[{"page":1,"quote":"","line_hint":""}]}]} -/no-think`;function csLlmUrl(){var _a2;const explicit=(_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim();if(explicit)return explicit;const fromSettings=typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url?_appSettings.llm_url:"";return fromSettings&&!/localhost:11434|127\.0\.0\.1:11434/.test(fromSettings)?fromSettings:""}function csLlmModel(){var _a2;return((_a2=$("reh-llm-model"))==null?void 0:_a2.value)||""}function csLang(){var _a2;return((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||""}function csReaderPageHost(){return $("reader-charsheets-panel")}async function csEnsureBookContext(book,text,roster,opts={}){if(!book||!text||typeof _getBookProfile!="function")return{};let existing={};try{existing=await _getBookProfile(book)}catch{}if(!["genre","setting","era","language"].filter(k=>!String((existing==null?void 0:existing[k])||"").trim()).length&&!opts.force)return existing;try{const r=await fetch("/api/book-profile/generate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({book,text,known_characters:Array.isArray(roster)?roster:[],llm_url:csLlmUrl(),model:csLlmModel(),language:csLang()})});if(!r.ok)throw new Error((await r.text()).slice(0,300)||"HTTP "+r.status);const data=await r.json();return typeof _bookProfileCache!="undefined"&&_bookProfileCache.set(book,data.profile||{}),data.profile||existing}catch(e){return console.warn("[book context] automatic generation failed:",e),typeof toast=="function"&&toast("Book context could not be generated; profile casting will continue","info"),existing}}function csReaderText(){var _a2,_b2,_c2;if(typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";let out="",lastPage=-1;for(const i of readerScopeIndices()){const u=readerState.sentences[i],pg=(_c2=(_b2=u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;readerState.mode==="pdf"&&pg!=null&&pg!==lastPage&&(out+=` -[p.${pg+1}] `,lastPage=pg),out+=u.text+" "}return out.trim()}function csRehearserText(){if(!window.rehState||!(rehState.lines||[]).length)return"";let out="",page=1,started=!1;for(const l of rehState.lines){if(l.type==="pagebreak"){page++,out+=` -[p.${page}] `;continue}const t=(typeof stripMarkdown=="function"?stripMarkdown(l.text||""):l.text||"").trim();t&&(started||(out+="[p.1] ",started=!0),out+=(l.type==="dialog"&&l.speaker?l.speaker+": ":"")+t+` -`)}return out.trim()}const CS_SCALAR_FIELDS=["aliases","first_name","last_name","full_name","title","profession","age_estimate","race_species","languages","nationality_background","social_class","archetype","physical","clothing","alignment","arc_note","attribute_high","attribute_low","skills","capabilities","backstory","relationships","motivation","fears","mannerisms","communication_style","voice_pattern","secret","conflict_style","win_condition","reputation","religious_beliefs","notes","voice_design_prompt","image_prompt","silly_tavern_prompt","concept_art_prompt"],CS_DETAIL_FIELDS=CS_SCALAR_FIELDS.filter(f=>!["aliases","first_name","last_name","full_name","title"].includes(f)),CS_IDENTITY_FIELDS=["name","aliases","first_name","last_name","full_name"],CS_ALIAS_MAX_TOKENS=12,CS_ALIAS_MAX_CHARS=500,CS_SOURCE_FIELDS={physical:["physical","appearance","body","look"],clothing:["clothing","appearance","armour","armor","item"],age_estimate:["age","older","young","teen","adult"],race_species:["race","species","kind","orc","elf","human","vampire"],languages:["language","languages","tongue","dialect","speech"],nationality_background:["nationality","background","origin","homeland","culture"],social_class:["class","social","status","rank"],relationships:["relationship","ally","rival","enemy","family"],motivation:["motivation","intention","goal","desire"],fears:["fear","dread"],mannerisms:["mannerism","habit","gesture","voice"],communication_style:["communication","communicate","demeanor","style"],voice_pattern:["voice","speech","dialogue"],backstory:["backstory","origin","history"],alignment:["alignment","ethos","morality"],profession:["profession","occupation","job","role"],skills:["skill","capability","ability"],capabilities:["capability","ability","combat","magic"],reputation:["reputation","known as","status","perceived"],religious_beliefs:["religion","faith","belief","priest","god","gods","temple"],notes:["note","misc","miscellaneous"],secret:["secret","flaw"],conflict_style:["conflict","fight","flight","manipulate"],win_condition:["win","goal"]};function csExistingSummary(map){if(!map.size)return"";const KEY_FIELDS=["age_estimate","race_species","languages","nationality_background","social_class","profession","physical","clothing","communication_style","voice_pattern","backstory","motivation","relationships","reputation","religious_beliefs","notes"];return[...map.values()].slice(0,40).map(s=>{const missing=KEY_FIELDS.filter(f=>!(s[f]||"").trim()),suffix=missing.lengths.trim()).filter(Boolean).filter(s=>s.length<=80&&!/^needs?:/i.test(s)&&!/^complete$/i.test(s));return opts.aliases&&(raw.length>CS_ALIAS_MAX_CHARS||parts.length>CS_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CS_ALIAS_MAX_TOKENS:void 0)}const CS_ROLE_LABEL_MIN_LINES=4;function csCleanRoster(names,lineCounts){const out=[],seen=new Set,counts=lineCounts instanceof Map?lineCounts:null,isGeneric=typeof _abIsGenericRoleLabel=="function"?_abIsGenericRoleLabel:()=>!1;return(names||[]).forEach(n=>{const name=_csStr(n).trim(),key=name.toLowerCase();!name||seen.has(key)||key==="narrator"||/^unknown|unbekannt$/i.test(name)||counts&&isGeneric(name)&&(counts.get(key)||0){name=_csStr(name).trim();const key=name.toLowerCase();!name||key==="narrator"||/^unknown|unbekannt$/i.test(name)||names.some(n=>n.toLowerCase()===key)||names.push(name)},ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;return ab&&((ab.roster||[]).forEach(add),[ab.segments,ab.liveSegments].forEach(segs=>{(segs||[]).forEach(s=>{(s==null?void 0:s.type)==="dialogue"&&add(s.speaker)})})),document.querySelectorAll("#ab-cv-chars .ab-char-item[data-name], #audiobook-roster option[value]").forEach(el=>{add(el.dataset.name||el.value||el.textContent)}),csCleanRoster(names)}function csBlankSheet(name){return{name,aliases:"",first_name:"",last_name:"",full_name:"",title:"",profession:"",age_estimate:"",race_species:"",languages:"",nationality_background:"",social_class:"",archetype:"",gender:"",physical:"",clothing:"",alignment:"",arc_note:"",attribute_high:"",attribute_low:"",skills:"",capabilities:"",backstory:"",relationships:"",motivation:"",fears:"",mannerisms:"",communication_style:"",voice_pattern:"",secret:"",conflict_style:"",win_condition:"",reputation:"",religious_beliefs:"",notes:"",voice_design_prompt:"",image_prompt:"",forms:[],active_form_id:"",inventory:[],sources:[],moral_alignment_score:50,arc_direction:"neutral",tier:"supporting"}}function csSeedSheet(raw){const name=_csStr(raw==null?void 0:raw.name).trim(),seed=csBlankSheet(name);if(!name)return seed;raw!=null&&raw.image&&(seed._image=raw.image);const src=raw!=null&&raw.sheet&&typeof raw.sheet=="object"?raw.sheet:raw;if(CS_SCALAR_FIELDS.forEach(f=>{src&&src[f]!=null&&(seed[f]=_csStr(src[f]))}),Array.isArray(src==null?void 0:src.inventory)?seed.inventory=[...src.inventory]:typeof(src==null?void 0:src.inventory)=="string"&&(seed.inventory=src.inventory.split(",").map(x=>x.trim()).filter(Boolean)),Array.isArray(src==null?void 0:src.sources)&&(seed.sources=src.sources.map(item=>item&&typeof item=="object"?{page:item.page!=null?item.page:null,quote:_csStr(item.quote).slice(0,240),line_hint:_csStr(item.line_hint).slice(0,60)}:null).filter(Boolean)),Array.isArray(src==null?void 0:src.forms)&&(seed.forms=src.forms.slice(0,8).filter(form=>form&&typeof form=="object").map((form,index)=>({id:String(form.id||`form-${index+1}`).slice(0,120),name:String(form.name||`Form ${index+1}`).slice(0,160),physical:_csStr(form.physical).slice(0,12e3),clothing:_csStr(form.clothing).slice(0,12e3),image_prompt:_csStr(form.image_prompt).slice(0,8e3),concept_art_prompt:_csStr(form.concept_art_prompt).slice(0,8e3),inventory:Array.isArray(form.inventory)?form.inventory.filter(x=>typeof x=="string").slice(0,12):[]}))),src!=null&&src.active_form_id&&(seed.active_form_id=String(src.active_form_id).slice(0,120)),src!=null&&src.tier&&(seed.tier=String(src.tier).toLowerCase().startsWith("main")?"main":"supporting"),(src==null?void 0:src.moral_alignment_score)!=null){const mas=parseInt(src.moral_alignment_score,10);Number.isNaN(mas)||(seed.moral_alignment_score=Math.max(0,Math.min(100,mas)))}return src!=null&&src.arc_direction&&(seed.arc_direction=src.arc_direction),src!=null&&src.gender&&(seed.gender=String(src.gender).trim().toLowerCase()),(src==null?void 0:src.line_count)!=null&&(seed.line_count=src.line_count),seed}function csNameTokens(sheet){const out=new Set;for(const f of CS_IDENTITY_FIELDS){const raw=f==="name"?sheet==null?void 0:sheet.name:sheet==null?void 0:sheet[f];csSplitIdentityTokens(raw,{aliases:f==="aliases"}).forEach(s=>out.add(s.toLowerCase()))}return out}function csSheetMatchesRoster(sheet,roster=[]){const allowed=new Set((roster||[]).map(csRosterKey).filter(Boolean));if(!allowed.size)return!0;for(const token of csNameTokens(sheet))for(const name of allowed)if(token===name||token.includes(name)||name.includes(token))return!0;return!1}function csRosterCanonicalName(sheet,roster=[]){const names=csCleanRoster(roster);if(!names.length)return"";const tokens=csNameTokens(sheet);return names.find(n=>tokens.has(n.toLowerCase()))||""}function csCanonicalizeRosterNames(sheets,roster=[]){const canonical=new Map;for(const original of sheets||[]){if(!original)continue;const sheet={...original},rosterName=csRosterCanonicalName(sheet,roster);rosterName&&sheet.name&&sheet.name.toLowerCase()!==rosterName.toLowerCase()&&(sheet.aliases=csMergeAliases({name:rosterName,aliases:sheet.aliases},{name:sheet.name,aliases:""}),sheet.name=rosterName),rosterName&&(sheet._rosterNameAuthoritative=!0);const key=String(sheet.name||"").trim().toLowerCase();key&&(canonical.has(key)?csMerge(canonical,[sheet]):canonical.set(key,sheet))}return[...canonical.values()]}function csIdentityNeedles(value){return csSplitIdentityTokens(value,{aliases:!0}).filter(v=>{const n=String(v||"").trim();return n&&n.length>1&&!/^(die|der|das|den|dem|des|ein|eine|einer|er|sie|es|ich|du|wir|ihr)$/i.test(n)})}function csRecordNeedles(rec){var _a2,_b2,_c2;const seen=new Set,out=[],add=value=>{csIdentityNeedles(value).forEach(v=>{const key=v.toLowerCase();seen.has(key)||(seen.add(key),out.push(v))})};return add(rec==null?void 0:rec.name),add((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.aliases),add([(_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name,(_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name].filter(Boolean).join(" ")),out}function csParagraphBlocks(text){return String(text||"").replace(/\r\n/g,` -`).split(/\n\s*\n+/).map(s=>s.trim()).filter(Boolean)}function csReaderParagraphBlocks(){var _a2,_b2,_c2;if(readerState.mode==="text"&&readerState.docText)return csParagraphBlocks(readerState.docText);const base=Array.isArray(readerState.baseSentences)&&readerState.baseSentences.length?readerState.baseSentences:Array.isArray(readerState.sentences)?readerState.sentences:[];if(!base.length)return csParagraphBlocks(csReaderText());const paras=[];let cur="",curPage=null;for(const s of base){const page=(_c2=(_b2=(_a2=s==null?void 0:s.words)==null?void 0:_a2[0])==null?void 0:_b2.page)!=null?_c2:null,pageChanged=readerState.mode==="pdf"&&page!=null&&curPage!=null&&page!==curPage;(s!=null&&s.paraStart||pageChanged)&&cur.trim()&&(paras.push(cur.trim()),cur=""),!cur&&readerState.mode==="pdf"&&page!=null&&(cur+=`[p.${page+1}] `),cur+=(cur&&!/\s$/.test(cur)?" ":"")+String((s==null?void 0:s.text)||"").trim(),curPage=page!=null?page:curPage}return cur.trim()&¶s.push(cur.trim()),paras}function csParagraphHasNeedle(paragraph,needle){const p=String(paragraph||""),n=String(needle||"").trim();if(!n||n.length<2)return!1;const lowerP=p.toLowerCase(),lowerN=n.toLowerCase();if(lowerN.includes(" "))return lowerP.includes(lowerN);const esc=lowerN.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(^|[^\\p{L}\\p{N}_])${esc}(?=$|[^\\p{L}\\p{N}_])`,"iu").test(p)}function csEvidenceWindowText(text,needles,before=2,after=2){const paras=Array.isArray(text)?text.slice():csParagraphBlocks(text);if(!paras.length)return String(text||"").trim();const hits=new Set;return paras.forEach((para,idx)=>{if((needles||[]).some(n=>csParagraphHasNeedle(para,n)))for(let i=idx-before;i<=idx+after;i++)i>=0&&ia-b).map(i=>paras[i]).join(` - -`).trim():""}function csFindMergeKey(map,sheet){const incoming=csNameTokens(sheet);if(!incoming.size)return(sheet.name||"").trim().toLowerCase();for(const[key,existing]of map.entries()){const existingNames=csNameTokens(existing);for(const n of incoming)if(existingNames.has(n))return key}return(sheet.name||"").trim().toLowerCase()}function csMergeAliases(existing,incoming){const names=new Map,add=v=>csSplitIdentityTokens(v,{aliases:!0}).forEach(s=>names.set(s.toLowerCase(),s));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function csMergeForms(existing,incoming){const forms=Array.isArray(existing)?existing.map(f=>({...f})):[];return(Array.isArray(incoming)?incoming:[]).forEach((incomingForm,index)=>{if(!incomingForm||typeof incomingForm!="object")return;const id=String(incomingForm.id||incomingForm.name||`form-${index+1}`).slice(0,120),found=forms.find(form=>String(form.id||form.name||"").toLowerCase()===id.toLowerCase());if(!found){forms.push({id,name:String(incomingForm.name||id).slice(0,160),physical:_csStr(incomingForm.physical).slice(0,12e3),clothing:_csStr(incomingForm.clothing).slice(0,12e3),image_prompt:_csStr(incomingForm.image_prompt).slice(0,8e3),concept_art_prompt:_csStr(incomingForm.concept_art_prompt).slice(0,8e3),inventory:Array.isArray(incomingForm.inventory)?incomingForm.inventory.filter(x=>typeof x=="string").slice(0,12):[]});return}["name","physical","clothing","image_prompt","concept_art_prompt"].forEach(key=>{const value=_csStr(incomingForm[key]);value.length>_csStr(found[key]).length&&(found[key]=value.slice(0,key==="name"?160:key.includes("prompt")?8e3:12e3))}),Array.isArray(incomingForm.inventory)&&incomingForm.inventory.forEach(item=>{typeof item=="string"&&item&&!found.inventory.includes(item)&&found.inventory.length<12&&found.inventory.push(item.slice(0,300))})}),forms.slice(0,8)}function csMerge(map,sheets,opts={}){const changed=[];for(const s of sheets){const name=(s.name||"").trim();if(!name)continue;s.aliases=csMergeAliases({name,aliases:""},s);const key=opts.strictRoster?name.toLowerCase():csFindMergeKey(map,s);if(!map.has(key)){const copy={...s,name,forms:csMergeForms([],s.forms),inventory:[...s.inventory||[]],sources:[...s.sources||[]]};CS_SCALAR_FIELDS.forEach(f=>{copy[f]=_csStr(copy[f])}),map.set(key,copy),changed.push({key,name:copy.name,status:"new"});continue}const e=map.get(key),before=JSON.stringify(e),oldAliases=e.aliases;CS_SCALAR_FIELDS.forEach(f=>{const sv=_csStr(s[f]);sv.length>(e[f]||"").length&&(e[f]=sv)}),e.aliases=csMergeAliases({...e,aliases:oldAliases},s),e.forms=csMergeForms(e.forms,s.forms),s.tier==="main"&&(e.tier="main"),s.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+s.moral_alignment_score)/2):s.moral_alignment_score),s.arc_direction&&s.arc_direction!=="neutral"&&(e.arc_direction=s.arc_direction),s.gender&&!e.gender&&(e.gender=s.gender),(s.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),(s.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),JSON.stringify(e)!==before&&changed.push({key,name:e.name,status:"updated"})}return changed}function csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated,roster=[]){let changed=!0;for(;changed;){changed=!1;for(const[key,s]of map){const dupKey=csSplitIdentityTokens(s.aliases,{aliases:!0}).map(a=>a.toLowerCase()).find(a=>a!==key&&map.has(a));if(!dupKey)continue;const other=map.get(dupKey),sEst=establishedKeys.has(key),oEst=establishedKeys.has(dupKey),rosterNames=csCleanRoster(roster),sRoster=rosterNames.findIndex(n=>n.toLowerCase()===key),oRoster=rosterNames.findIndex(n=>n.toLowerCase()===dupKey);let keepKey=key,dropKey=dupKey;if(sRoster>=0&&oRoster>=0&&sRoster!==oRoster)oRoster=0)keepKey=dupKey,dropKey=key;else if(oEst&&!sEst)keepKey=dupKey,dropKey=key;else if(sEst===oEst){const filled=sh=>CS_DETAIL_FIELDS.filter(f=>_csStr(sh[f]).trim()).length,sFilled=filled(s),oFilled=filled(other);if(oFilled>sFilled)keepKey=dupKey,dropKey=key;else if(oFilled===sFilled&&establishedCreated){const sT=establishedCreated.get(key),oT=establishedCreated.get(dupKey);sT!=null&&oT!=null&&oT{const dv=_csStr(drop[f]);dv.length>(keep[f]||"").length&&(keep[f]=dv)}),keep.aliases=csMergeAliases({...keep,aliases:oldAliases},drop),drop.tier==="main"&&(keep.tier="main"),drop.line_count!=null&&(keep.line_count=Math.max(keep.line_count||0,drop.line_count)),(drop.inventory||[]).forEach(it=>{it&&!keep.inventory.includes(it)&&keep.inventory.length<3&&keep.inventory.push(it)}),(drop.sources||[]).forEach(src=>{src&&src.quote&&keep.sources.length<12&&!keep.sources.some(x=>x.quote===src.quote)&&keep.sources.push(src)}),map.set(keepKey,keep),map.delete(dropKey),changed=!0;break}}}function csProgressSnapshot(map,changed=[]){const changedByKey=new Map(changed.map(x=>[x.key,x.status]));return[...map.entries()].map(([key,s])=>{const filled=CS_DETAIL_FIELDS.filter(f=>_csStr(s[f]).trim()).length;return{key,name:s.name||key,alias:[s.title,s.profession].filter(Boolean).join(" \xB7 "),status:changedByKey.get(key)||"",filled,main:s.tier==="main",gender:s.gender||"",lineCount:Number(s.line_count)||0,image:s._image||""}}).sort((a,b)=>(b.status?1:0)-(a.status?1:0)||b.filled-a.filled||a.name.localeCompare(b.name))}async function csGenerateStream(body,onDelta,outerSignal,idleTimeoutMs=6e4){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/character-sheets/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify(body)});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(` - -`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&onDelta&&onDelta(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}async function csGenerate(text,cacheKey,initialRoster,opts={}){if(_cs.running)return null;if(!text)return toast("Nothing to analyse","error"),null;const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,CS_CHUNK_CHARS):[text];_cs.running=!0,_cs.cancel=!1;try{const prog=csProgress(chunks.length,opts.pageHost||null),llm_url=csLlmUrl(),model=csLlmModel();let language=csLang();if(typeof detectLang=="function"){const detected=detectLang(text);if(detected&&(!language||/^auto$/i.test(language)||language.toLowerCase()==="english"&&detected.toLowerCase()!=="english")){language=detected;const sel=$("reh-design-lang");if(sel){const o=[...sel.options].find(x=>x.value.toLowerCase()===detected.toLowerCase()||x.textContent.toLowerCase()===detected.toLowerCase());o&&(sel.value=o.value)}}}const map=new Map,lineCounts=opts.lineCounts instanceof Map?opts.lineCounts:new Map,seedSheets=Array.isArray(opts.seedSheets)?opts.seedSheets:[],establishedKeys=new Set,establishedCreated=new Map;seedSheets.forEach(seedRaw=>{const seed=csSeedSheet(seedRaw),name=String(seed.name||"").trim();if(!name)return;const lc=lineCounts.get(name.toLowerCase());lc!=null&&(seed.line_count=lc),map.set(name.toLowerCase(),seed),establishedKeys.add(name.toLowerCase());const created=seedRaw!=null&&seedRaw.created?new Date(seedRaw.created).getTime():NaN;isNaN(created)||establishedCreated.set(name.toLowerCase(),created)});const roster=csCleanRoster(initialRoster,lineCounts),targetMode=roster.length>0,knownTotal=targetMode?roster.length:null;roster.forEach(name=>{const key=name.toLowerCase();if(!map.has(key)){const blank=csBlankSheet(name),lc=lineCounts.get(key);lc!=null&&(blank.line_count=lc),map.set(key,blank)}});try{for(let i=0;iCS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,charLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${detailed} profiles with details`:`${map.size} characters found`;prog.update(i,`Passage ${i+1} / ${chunks.length}\u2026`,charLabel,csProgressSnapshot(map),[...map.values()]),prog.startPassage(chunks[i]);const maxAttempts=4;for(let attempt=1;attempt<=maxAttempts;attempt++)try{const character_sheets_prompt=typeof _appSettings!="undefined"&&_appSettings.character_sheets_prompt||CS_DEFAULT_PROMPT,body={text:chunks[i],known_characters:roster.slice(0,120),target_mode:targetMode,existing:csExistingSummary(map),language,llm_url,model,character_sheets_prompt};let data=null,sawDelta=!1;try{data=await csGenerateStream(body,delta=>{sawDelta=!0,prog.thinking(delta)}),sawDelta||prog.noStream()}catch{sawDelta||prog.noStream();const r=await fetch("/api/character-sheets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){let detail=`HTTP ${r.status}`;try{const e=await r.json();detail=e.detail||e.error||detail}catch{}throw new Error(detail)}const raw=await r.text();try{data=JSON.parse(raw)}catch{throw new Error("Invalid JSON from server (passage may be too large)")}}const incomingSheets=targetMode?(data.sheets||[]).filter(s=>roster.some(n=>n.toLowerCase()===String((s==null?void 0:s.name)||"").trim().toLowerCase())):data.sheets||[];incomingSheets.forEach(s=>{const ownName=String(s.name||"").trim().toLowerCase();["first_name","last_name","full_name"].forEach(f=>{const v=String(s[f]||"").trim().toLowerCase();v&&v!==ownName&&roster.some(n=>n.toLowerCase()===v)&&(console.warn(`[character sheets] dropped ${f}="${s[f]}" from "${s.name}" \u2014 matches a different known character`),s[f]="")})});const changed=csMerge(map,incomingSheets,{strictRoster:targetMode});changed.length&&prog.focus(changed[0].name),targetMode||((data.characters||[]).forEach(n=>{roster.some(r=>r.toLowerCase()===String(n||"").toLowerCase())||roster.push(n)}),(data.sheets||[]).forEach(s=>{csNameTokens(s).forEach(n=>{const display=[s.name,s.title,s.profession,s.aliases].filter(Boolean).join(", ").split(/[,;/|]/).map(x=>x.trim()).find(x=>x.toLowerCase()===n)||n;display&&!roster.some(r=>r.toLowerCase()===display.toLowerCase())&&roster.push(display)})}));const newDetailed=[...map.values()].filter(s=>CS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,newCharLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${newDetailed} profiles with details`:`${map.size} characters found`;prog.update(i+1,`Passage ${i+1} / ${chunks.length}\u2026`,newCharLabel,csProgressSnapshot(map,changed),[...map.values()]);break}catch(e){const msg=e.message||String(e),isRateLimit=/429|too many requests|rate.?limit/i.test(msg),isNetworkError=/failed to fetch|network\s*error|load failed|timed?\s*out|timeout|\b50[234]\b|econnreset|econnrefused|socket hang up|connection (refused|reset|closed)/i.test(msg);if((isRateLimit||isNetworkError)&&attemptsetTimeout(res,waitMs));continue}console.error("Character sheets passage",i+1,"failed:",e),toast("Passage "+(i+1)+" failed: "+msg,"error");break}}}finally{prog.done()}if(_cs.cancel)return toast("Cancelled","error"),null;targetMode||csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated,roster);const sheets=csCanonicalizeRosterNames([...map.values()],roster);return cacheKey&&(_cs.cache[cacheKey]=sheets),sheets}catch(e){return console.error("[character sheets] generation crashed:",e),toast("Character sheet generation crashed: "+(e&&e.message?e.message:String(e)),"error"),null}finally{_cs.running=!1}}function csProgress(total,hostEl=null){var _a2,_b2;const usingHost=!!hostEl;let root=usingHost?hostEl:document.getElementById("cs-progress");if(!root){if(usingHost)return null;root=document.createElement("div"),root.id="cs-progress",root.className="audiobook-overlay",document.body.appendChild(root)}if(!root.querySelector("#cs-progress-fill")){usingHost&&(root.innerHTML=""),root.innerHTML=`
-
-
- Character sheets - -
-
Analysing\u2026
- -
-
-
-
-
Select a character to preview the sheet as it fills.
-
-
- -
- Passage / Live output watching\u2026 - -
-
-
-
-
Passage
-

-                
-
-
Live output
-

-                
-
-
-
-
-
-
- Characters found - -
-
- - -
-
-
-
-
- -
`,(_a2=root.querySelector("#cs-progress-cancel"))==null||_a2.addEventListener("click",()=>{_cs.cancel=!0});const promptBtn=root.querySelector("#cs-prompt-btn"),promptPanel=root.querySelector("#cs-prompt-panel"),promptText=root.querySelector("#cs-prompt-text");promptText&&(promptText.value=typeof _appSettings!="undefined"&&_appSettings.character_sheets_prompt||CS_DEFAULT_PROMPT),promptBtn==null||promptBtn.addEventListener("click",()=>{if(!promptPanel)return;promptPanel.hidden=!promptPanel.hidden;const chevron=root.querySelector("#cs-prompt-chevron");chevron&&(chevron.className=promptPanel.hidden?"mdi mdi-chevron-down":"mdi mdi-chevron-up")});let savedCsPrompts=[];try{savedCsPrompts=JSON.parse(localStorage.getItem("ttsvc_cs_prompts")||"[]")}catch{savedCsPrompts=[]}const libSelect=root.querySelector("#cs-prompt-lib"),delBtn=root.querySelector("#cs-prompt-del"),nameInput=root.querySelector("#cs-prompt-name"),renderCsPromptLib=(selectedIdx=-1)=>{!libSelect||!delBtn||(libSelect.innerHTML=''+savedCsPrompts.map((p,i)=>``).join(""),selectedIdx>=0?(libSelect.value=selectedIdx,delBtn.style.display="inline-flex"):(libSelect.value="",delBtn.style.display="none"))};renderCsPromptLib(),libSelect==null||libSelect.addEventListener("change",()=>{const idx=parseInt(libSelect.value,10);!isNaN(idx)&&savedCsPrompts[idx]?(promptText&&(promptText.value=savedCsPrompts[idx].prompt),nameInput&&(nameInput.value=savedCsPrompts[idx].name),delBtn.style.display="inline-flex"):(nameInput&&(nameInput.value=""),delBtn.style.display="none")}),delBtn==null||delBtn.addEventListener("click",()=>{const idx=parseInt(libSelect.value,10);isNaN(idx)||confirm("Delete this saved prompt preset?")&&(savedCsPrompts.splice(idx,1),localStorage.setItem("ttsvc_cs_prompts",JSON.stringify(savedCsPrompts)),renderCsPromptLib(),toast("Prompt deleted","success"))}),(_b2=root.querySelector("#cs-prompt-save"))==null||_b2.addEventListener("click",async()=>{const val=(promptText==null?void 0:promptText.value.trim())||"";if(!val){toast("Prompt is empty","error");return}const name=(nameInput==null?void 0:nameInput.value.trim())||"Custom Prompt "+(savedCsPrompts.length+1);let targetIdx=parseInt((libSelect==null?void 0:libSelect.value)||"",10);!isNaN(targetIdx)&&savedCsPrompts[targetIdx]&&savedCsPrompts[targetIdx].name===name?savedCsPrompts[targetIdx].prompt=val:(savedCsPrompts.push({name,prompt:val}),targetIdx=savedCsPrompts.length-1),localStorage.setItem("ttsvc_cs_prompts",JSON.stringify(savedCsPrompts)),renderCsPromptLib(targetIdx),toast("Prompt preset saved","success")});let promptSaveTimer=null;promptText==null||promptText.addEventListener("input",()=>{clearTimeout(promptSaveTimer),promptSaveTimer=setTimeout(()=>{const val=promptText.value;typeof _appSettings!="undefined"&&(_appSettings.character_sheets_prompt=val),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({character_sheets_prompt:val})}).catch(()=>{})},500)})}usingHost||(root.style.display="flex");const fill=root.querySelector("#cs-progress-fill"),msg=root.querySelector("#cs-progress-msg"),chars=root.querySelector("#cs-progress-chars"),sideTitle=root.querySelector("#cs-progress-side-title"),sideDone=root.querySelector("#cs-progress-side-done"),sideCollapse=root.querySelector("#cs-progress-side-collapse"),leftPane=root.querySelector(".cs-progress-left"),outputPanel=root.querySelector("#cs-progress-output"),outputCollapse=root.querySelector("#cs-progress-output-collapse"),outputResize=root.querySelector("#cs-progress-output-resize"),preview=root.querySelector("#cs-progress-preview"),search=root.querySelector("#cs-progress-search"),passagePre=root.querySelector("#cs-progress-passage-pre"),livePre=root.querySelector("#cs-progress-live-pre"),liveStatus=root.querySelector("#cs-progress-live-status");let currentRoster=[],currentSheets=[],selectedName="",searchQuery="",rosterSort=(()=>{try{return localStorage.getItem("ttsvc_cs_side_sort")||"lines"}catch{return"lines"}})();const keyFor=v=>String(v||"").trim().toLowerCase(),findSheet=(name,sheets=currentSheets)=>(Array.isArray(sheets)?sheets:[]).find(s=>keyFor(s==null?void 0:s.name)===keyFor(name))||null;let outputCollapsed=!1,outputExpandedHeight=0;const clampOutputHeight=h=>{const max=Math.max(140,Math.round(window.innerHeight*.82));return Math.max(140,Math.min(max,Math.round(h)))},setOutputHeight=(h,persist=!0)=>{const next=clampOutputHeight(h);if(outputExpandedHeight=next,!outputCollapsed&&outputPanel&&(outputPanel.style.height=`${next}px`,outputPanel.style.maxHeight=`${next}px`,outputPanel.dataset.csHeight=String(next)),persist)try{localStorage.setItem("ttsvc_cs_output_height",String(next))}catch{}},applyOutputCollapse=collapsed=>{if(outputCollapsed=!!collapsed,outputPanel==null||outputPanel.classList.toggle("is-collapsed",outputCollapsed),leftPane==null||leftPane.classList.toggle("is-output-collapsed",outputCollapsed),outputCollapse){const icon=outputCollapse.querySelector(".mdi");icon&&(icon.className="mdi "+(outputCollapsed?"mdi-chevron-up":"mdi-chevron-down")),outputCollapse.title=outputCollapsed?"Expand passage and live output":"Collapse passage and live output"}if(outputPanel)if(outputCollapsed){if(!outputExpandedHeight){const cur=parseInt(outputPanel.dataset.csHeight||"",10)||parseInt(outputPanel.style.height||"",10)||outputPanel.getBoundingClientRect().height;cur&&(outputExpandedHeight=clampOutputHeight(cur))}const collapsedH=138;outputPanel.style.height=`${collapsedH}px`,outputPanel.style.maxHeight=`${collapsedH}px`}else{const h=outputExpandedHeight||parseInt(outputPanel.dataset.csHeight||"",10)||260;outputPanel.style.height=`${clampOutputHeight(h)}px`,outputPanel.style.maxHeight=`${clampOutputHeight(h)}px`,outputPanel.dataset.csHeight=String(clampOutputHeight(h))}try{localStorage.setItem("ttsvc_cs_output_collapsed",outputCollapsed?"1":"0")}catch{}};let sideCollapsed=!1;const applySideCollapse=collapsed=>{var _a3,_b3;if(sideCollapsed=!!collapsed,(_a3=root.querySelector("#cs-progress-layout"))==null||_a3.classList.toggle("side-collapsed",sideCollapsed),(_b3=root.querySelector("#cs-progress-side"))==null||_b3.classList.toggle("is-collapsed",sideCollapsed),sideCollapse){const icon=sideCollapse.querySelector(".mdi");icon&&(icon.className="mdi "+(sideCollapsed?"mdi-chevron-left":"mdi-chevron-right")),sideCollapse.title=sideCollapsed?"Expand character list":"Collapse character list"}try{localStorage.setItem("ttsvc_cs_side_collapsed",sideCollapsed?"1":"0")}catch{}};try{sideCollapsed=localStorage.getItem("ttsvc_cs_side_collapsed")==="1"}catch{}try{const savedOutputCollapsed=localStorage.getItem("ttsvc_cs_output_collapsed");savedOutputCollapsed!==null&&(outputCollapsed=savedOutputCollapsed==="1");const savedOutputHeight=parseInt(localStorage.getItem("ttsvc_cs_output_height")||"",10);!Number.isNaN(savedOutputHeight)&&savedOutputHeight>0&&(outputExpandedHeight=clampOutputHeight(savedOutputHeight))}catch{}if(applySideCollapse(sideCollapsed),applyOutputCollapse(outputCollapsed),sideCollapse==null||sideCollapse.addEventListener("click",()=>applySideCollapse(!sideCollapsed)),outputCollapse==null||outputCollapse.addEventListener("click",()=>applyOutputCollapse(!outputCollapsed)),outputResize){let dragging=!1,startY=0,startH=0;const onMove=e=>{dragging&&(outputCollapsed&&applyOutputCollapse(!1),setOutputHeight(startH-(e.clientY-startY)),e.preventDefault())},stopDrag=()=>{dragging=!1,document.body.classList.remove("cs-output-resizing"),document.removeEventListener("mousemove",onMove),document.removeEventListener("mouseup",stopDrag)};outputResize.addEventListener("mousedown",e=>{e.preventDefault(),outputPanel&&(outputCollapsed&&applyOutputCollapse(!1),dragging=!0,startY=e.clientY,startH=parseInt(outputPanel.dataset.csHeight||"",10)||outputPanel.getBoundingClientRect().height||260,document.body.classList.add("cs-output-resizing"),document.addEventListener("mousemove",onMove),document.addEventListener("mouseup",stopDrag))})}const renderPreview=()=>{var _a3,_b3,_c2,_d2,_e2,_f2;if(!preview)return;const sheet=findSheet(selectedName)||currentSheets[0]||null;if(!sheet){preview.innerHTML='
Select a character to preview the sheet as it fills.
';return}preview.innerHTML=csCardHtml(sheet,{voice:sheet.voice,line_count:sheet.line_count}),(_a3=preview.querySelector(".cs-card"))==null||_a3.classList.add("cs-progress-preview-card"),preview.querySelectorAll(".cs-head-btns, .cs-header-actions, .cs-sources").forEach(el=>el.remove()),preview.querySelectorAll(".cs-avatar").forEach(el=>{el.style.pointerEvents="none",el.style.cursor="default"});const book=typeof readerState!="undefined"&&readerState.title||"",ensureRecord=async()=>typeof clUpsert=="function"?await clUpsert(book,sheet):null;(_b3=preview.querySelector(".lcd-pick-voice"))==null||_b3.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _openVoicePicker=="function"&&_openVoicePicker(preview.querySelector(".lcd-voice-top"),rec,()=>renderPreview())}),(_c2=preview.querySelector(".lcd-auto-voice"))==null||_c2.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _autoAssignVoice=="function"&&(await _autoAssignVoice(rec),renderPreview())}),(_d2=preview.querySelector(".lcd-online-voice"))==null||_d2.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _charSearchOnline=="function"&&_charSearchOnline(rec)}),(_e2=preview.querySelector(".lcd-gen-voice"))==null||_e2.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _charDesignVoice=="function"&&_charDesignVoice(rec)}),(_f2=preview.querySelector(".cs-header-color input"))==null||_f2.addEventListener("input",async e=>{const rec=await ensureRecord();rec&&(sheet.color=e.target.value,rec.color=e.target.value,typeof clPut=="function"&&await clPut(rec))});const sourceBox=preview.querySelector(".cs-source-preview");preview.querySelectorAll(".cs-source-mark:not(.cs-source-mark-sample)").forEach(btn=>{btn.addEventListener("click",()=>{if(!sourceBox)return;preview.querySelectorAll(".cs-source-mark.is-active").forEach(b=>b.classList.remove("is-active")),btn.classList.add("is-active");const quote=btn.dataset.quote||"",hint=btn.dataset.lineHint||"",page=btn.dataset.page||"";sourceBox.innerHTML=`
${hint?escHtml(hint):"Source sentence"}${page?` \xB7 p.${escHtml(page)}`:""}
-
${quote?escHtml(quote):'No exact sentence was saved for this field.'}
`,sourceBox.scrollIntoView({behavior:"smooth",block:"nearest"})})})},rosterSorters={progress:(a,b)=>(b.status?1:0)-(a.status?1:0)||b.filled-a.filled||a.name.localeCompare(b.name),alpha:(a,b)=>a.name.localeCompare(b.name),lines:(a,b)=>(b.lineCount||0)-(a.lineCount||0)||a.name.localeCompare(b.name),gender:(a,b)=>(a.gender||"\uFFFF").localeCompare(b.gender||"\uFFFF")||a.name.localeCompare(b.name)},renderChars=()=>{if(!chars)return;const list=currentRoster.filter(c=>{if(!searchQuery)return!0;const name=keyFor(c.name),alias=keyFor(c.alias);return name.includes(searchQuery)||alias.includes(searchQuery)}).slice().sort(rosterSorters[rosterSort]||rosterSorters.progress);chars.innerHTML=list.length?list.map(c=>` -
- - ${c.image?``:`${escHtml((c.name||"?")[0].toUpperCase())}`} - ${escHtml(c.name||"Unknown")} - - ${c.lineCount?escHtml(String(c.lineCount))+" lines":"\u2014"} - - ${escHtml(String(c.filled||0))}/${CS_DETAIL_FIELDS.length} - -
- `).join(""):'
reading\u2026
',chars.querySelectorAll(".ab-char-item").forEach(el=>{el.addEventListener("click",()=>{selectedName=keyFor(el.dataset.name),renderPreview()})})};search==null||search.addEventListener("input",()=>{searchQuery=search.value.trim().toLowerCase(),renderChars()});const sortSel=root.querySelector("#cs-progress-sort");return sortSel&&(sortSel.value=rosterSort,sortSel.addEventListener("change",()=>{rosterSort=sortSel.value;try{localStorage.setItem("ttsvc_cs_side_sort",rosterSort)}catch{}renderChars()})),renderPreview(),{startPassage(text){passagePre&&(passagePre.textContent=text||""),livePre&&(livePre.textContent="Waiting for streamed JSON output\u2026"),liveStatus&&(liveStatus.textContent="watching\u2026",liveStatus.className="cs-progress-live-status"),outputPanel==null||outputPanel.classList.add("is-working")},thinking(delta){!livePre||!delta||(liveStatus&&(liveStatus.textContent="streaming",liveStatus.className="cs-progress-live-status is-live"),outputPanel==null||outputPanel.classList.add("is-working"),livePre.textContent=(livePre.textContent+delta).slice(-2e4),livePre.scrollTop=livePre.scrollHeight)},noStream(){liveStatus&&(liveStatus.textContent="no live output for this model",liveStatus.className="cs-progress-live-status"),livePre&&livePre.textContent==="Waiting for streamed JSON output\u2026"&&(livePre.textContent="This model doesn't stream incremental output \u2014 the full result arrives at once when each passage finishes.")},focus(name){name&&(selectedName=keyFor(name),renderPreview())},update(d,label,charLabel,roster=[],sheets=[]){if(fill&&(fill.style.width=d/total*100+"%"),msg&&label&&(msg.textContent=label),sideTitle&&(sideTitle.textContent="Characters found"),sideDone&&(sideDone.innerHTML=charLabel?` ${escHtml(charLabel)}`:""),outputPanel==null||outputPanel.classList.add("is-working"),currentRoster=Array.isArray(roster)?roster:[],currentSheets=Array.isArray(sheets)?sheets:[],!selectedName&¤tRoster.length&&(selectedName=keyFor((currentRoster.find(c=>c.status)||currentRoster[0]||{}).name)),currentSheets.length&&!findSheet(selectedName)){const changedSheet=currentRoster.find(c=>c.status&&findSheet(c.name,currentSheets));changedSheet&&(selectedName=keyFor(changedSheet.name))}renderChars(),renderPreview()},done(){outputPanel==null||outputPanel.classList.remove("is-working"),usingHost||(root.style.display="none")}}}function csAlignmentBar(score,arcDirection,arcNote){if(score==null)return"";const pct=Math.max(0,Math.min(100,score)),arcMap={"stable-good":{arrow:"\u2192",label:"Stable Good",color:"#66bb6a"},"stable-bad":{arrow:"\u2192",label:"Stable Evil",color:"#aaa"},neutral:{arrow:"\u2192",label:"Neutral",color:"#aaa"},"good-to-bad":{arrow:"\u2198",label:"Descends toward evil",color:"#ff7043"},"bad-to-good":{arrow:"\u2197",label:"Redeems toward good",color:"#66bb6a"},complex:{arrow:"\u2195",label:"Complex arc",color:"#ab47bc"}},arc=arcMap[arcDirection]||arcMap.neutral,label=pct>=70?"Good":pct<=30?"Evil":"Neutral/Ambiguous",showArrow=arcDirection&&arcDirection!=="neutral";return`
-
EvilGood
-
-
- ${showArrow?`${arc.arrow}`:""} -
-
- ${arcNote?`
${escHtml(arc.label)} \xB7 ${escHtml(arcNote)}
`:`
${escHtml(arc.label)}
`} -
`}function csWorldGroundingClause(bookProfile){const bp=bookProfile||{},bits=[bp.genre,bp.setting,bp.era].filter(Boolean);return bits.length?`WORLD AND PERIOD (authoritative, overrides any era implied above): this character belongs to ${bits.join(", ")}. Every visual choice \u2014 clothing, materials, cut and tailoring, armour, weapons, insignia, jewellery, technology level and architecture \u2014 must fit THAT world and period. A rank or occupation title (Colonel, Admiral, General, Captain) must NOT be drawn as a modern or 19th-century military uniform: no epaulettes, no braided frogging, no peaked caps, no industrial-era tailoring, no machine-made fabric, no zips or modern buttons. Use only what that period could actually make.`:""}function csBuildImagePrompt(s,bookProfile){var _a2;const llm=_csStr(s.image_prompt).trim();if(llm)return llm+" "+csWorldGroundingClause(bookProfile);const parts=[];s.gender&&parts.push(`exactly ${s.gender}`),s.race_species&&parts.push(s.race_species),s.archetype&&parts.push(s.archetype),s.physical&&parts.push(s.physical),s.clothing&&parts.push(s.clothing),s.alignment&&parts.push(s.alignment);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;parts.push(pct>=70?"benevolent expression":pct<=30?"dark and menacing presence":"ambiguous expression"),s.arc_direction==="bad-to-good"&&parts.push("redemptive aura"),s.arc_direction==="good-to-bad"&&parts.push("ominous aura, turning to darkness");const bp=bookProfile||{},settingBits=[bp.genre,bp.setting,bp.era].filter(Boolean),settingClause=settingBits.length?`This character belongs to the following book/world: ${settingBits.join(", ")}. Every visual choice \u2014 architecture, clothing materials, weaponry, ethnicity mix, technology level \u2014 must fit THAT world, not a generic modern or real-world default. `:"";return`Create a photorealistic live-action square character portrait for an original character named ${s.name}, ${parts.filter(Boolean).join(", ")}. VISUAL IDENTITY LOCK: use the exact gender, age impression, species, face shape, skin tone, hair, eyes, facial hair, distinguishing marks, build, clothing, colors, armor, jewelry, weapons, tools, and signature props established here. Keep one identical face identity anchor for future concept art and profile images. `+settingClause+`Base the setting, era, and art style strictly on the character's own described archetype and clothing above \u2014 do not default to a modern or real-world 20th/21st-century look for occupation-sounding titles (e.g. an "Admiral" or "General" in a fantasy/period setting should NOT be drawn in a contemporary military uniform); every visual choice should fit the world implied by the description, not the real one, unless the description itself is explicitly modern/contemporary. Show a head-and-shoulders portrait facing camera, with a natural in-character expression, on a subdued neutral studio background. Photorealistic cinematic fantasy-film photography: natural skin texture and pores, believable anatomy, authentic cloth/leather/metal, physically plausible lighting and shadows. Not a drawing, painting, anime, game render, plastic skin, collage, text, logo, watermark, or copyrighted character.`}function csBuildVoicePrompt(s){var _a2;if(_csStr(s.voice_design_prompt).trim())return _csStr(s.voice_design_prompt).trim();const parts=[];s.voice_pattern&&parts.push(s.voice_pattern),s.mannerisms&&parts.push(s.mannerisms),s.archetype&&parts.push(`archetype: ${s.archetype}`);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;return pct>=70?parts.push("warm, trustworthy tone"):pct<=30?parts.push("cold, threatening or sinister tone"):parts.push("neutral, measured tone"),parts.filter(Boolean).join(". ")}async function csDeepAnalysis(sheet,sourceText){const llm_url=csLlmUrl(),model=csLlmModel(),language=csLang(),modal=document.createElement("div");modal.className="audiobook-overlay",modal.innerHTML=`
-
- Deep Analysis \u2014 ${escHtml(sheet.name)} - - -
-
-
-
Running deep psychological analysis\u2026 -
-
-
`,document.body.appendChild(modal),modal.querySelector("#cs-deep-close").addEventListener("click",()=>modal.remove()),modal.addEventListener("click",e=>{e.target===modal&&modal.remove()});try{const summary=[sheet.archetype,sheet.alignment,sheet.secret,sheet.win_condition,sheet.conflict_style].filter(Boolean).join("; "),r=await fetch("/api/character-deep-analysis",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:sheet.name,role:sheet.tier,goal:sheet.win_condition,summary,text_excerpt:(sourceText||"").slice(0,8e3),language,llm_url,model})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const a=(await r.json()).analysis||{},section=(icon,title,content)=>content?`
-
${title}
-
${escHtml(String(content))}
-
`:"";modal.querySelector("#cs-deep-body").innerHTML=` -
-
- ${escHtml(sheet.name)} - ${sheet.archetype?`${escHtml(sheet.archetype)}`:""} -
- ${csAlignmentBar(sheet.moral_alignment_score,sheet.arc_direction,sheet.arc_note)} -
- ${section("mdi-heart-broken-outline","1 \u2014 Core Flaw & Desire",a.core_flaw)} - ${section("mdi-run-fast","2 \u2014 Agency & Passivity",a.agency)} - ${section("mdi-comment-quote-outline","3 \u2014 Dialogue & Voice",a.dialogue_voice)} - ${section("mdi-timeline-outline","4 \u2014 Narrative Arc",a.narrative_arc)} - ${section("mdi-infinity","5 \u2014 Paradox & Depth",a.paradox)} - `}catch(err){modal.querySelector("#cs-deep-body").innerHTML=`
Error: ${escHtml(String(err.message))}
`}}function csSourcesForField(s,field){const keys=(CS_SOURCE_FIELDS[field]||[field]).map(x=>x.toLowerCase());return(s.sources||[]).filter(src=>{const hint=_csStr(src==null?void 0:src.line_hint).toLowerCase();return hint&&keys.some(k=>hint.includes(k))})}function csSourceBadgeHtml(s,field){const all=s.sources||[],matches=csSourcesForField(s,field);return matches.length?matches.map(src=>{const idx=all.findIndex(x=>x===src),n=idx>=0?idx+1:1;return``}).join(""):""}function csField(label,value,sheet,field){if(!value)return"";const badge=sheet&&field?csSourceBadgeHtml(sheet,field):"",key=field?` data-sheet-key="${escHtml(field)}"`:"";return`
${label}
${escHtml(String(value))}${badge?` ${badge}`:""}
`}function csIdentityCell(label,value,sheet,field){if(!value)return"";const badge=sheet&&field?csSourceBadgeHtml(sheet,field):"",key=field?` data-sheet-key="${escHtml(field)}"`:"";return`
${label}
${escHtml(String(value))}${badge?` ${badge}`:""}
`}function csPromptBox(label,value,sheetKey,emptyNote,extraImage){const text=_csStr(value),has=!!text.trim(),actionBtn=has?sheetKey==="voice_design_prompt"?'':sheetKey==="image_prompt"?'':sheetKey==="concept_art_prompt"?``:"":"",imageHtml=extraImage?`
Concept art
`:"";return`
- ${escHtml(label)}${has?"":' \u2014 '+escHtml(emptyNote||"not generated yet")+""} -
- ${imageHtml} -
${escHtml(text)}
-
- - - ${actionBtn} -
-
-
`}function csAvatarHtml(s){const hue=Math.abs((s.name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0));return s._image?`
- ${escHtml(s.name)} -
-
`:`
- ${escHtml((s.name||"?")[0].toUpperCase())} -
-
`}function csIdentityTagList(s){const tags=[],add=(label,value)=>{const text=String(value||"").trim();if(!text)return;const key=text.toLowerCase();tags.some(t=>t.key===key)||tags.push({label,text,key})},firstLast=[s.first_name,s.last_name].filter(Boolean).join(" ").trim();return firstLast&&firstLast!==String(s.name||"").trim()&&add("first / last",firstLast),tags}function csNameHue(name){return Math.abs((name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0))}function csHeaderPill(label,value,icon=""){const text=String(value||"").trim();return text?`${icon?``:""}${escHtml(label)}${escHtml(text)}`:""}function csProfileHeaderHtml(s,meta={},actionsHtml=""){var _a2;const hue=csNameHue(s.name),accent2=`hsl(${(hue+38)%360}, 58%, 34%)`,primaryPills=[csHeaderPill("Occupation",s.profession,"briefcase-outline"),csHeaderPill("Archetype",s.archetype,"shape-outline"),csHeaderPill("Also known as",s.aliases,"tag-multiple-outline")].filter(Boolean).join(""),chips=[csHeaderPill("Lines",(_a2=meta.line_count)!=null?_a2:s.line_count,"format-list-numbered"),csHeaderPill("Voice",meta.voice,"account-voice"),csHeaderPill("Book",meta.book,"book-open-page-variant-outline"),csHeaderPill("Tags",meta.tags,"tag-outline"),csHeaderPill("Gender",s.gender,"human-male-female")].filter(Boolean).join(""),color=typeof clNormalizeColor=="function"?clNormalizeColor(s.color,s.name):`hsl(${hue},58%,42%)`;return`
- ${csAvatarHtml(s)} -
-
${escHtml(s.name||"Unnamed")}
- ${primaryPills?`
${primaryPills}
`:""} -
${s.tier?`${escHtml(String(s.tier).toLowerCase()==="main"?"Hauptcharakter":"Nebencharakter")}`:""}${chips?`
${chips}
`:""}
-
- - - ${actionsHtml?`
${actionsHtml}
`:""} -
`}function csDetailActionsHtml(s){return` - - - `}function csOverviewCardHtml(rec){const s=rec.sheet||{},score=s.moral_alignment_score!=null?Math.max(0,Math.min(100,parseInt(s.moral_alignment_score,10)||50)):50,scoreLabel=score>=70?"Good":score<=30?"Evil":"Neutral",voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",fullName=s.full_name||rec.name||"",tierLabel=String(s.tier||"").toLowerCase()==="main"?"Main character":"Side character",facts=[["Occupation",s.profession],["Archetype",s.archetype],["Gender",s.gender],["Lines",s.line_count],["Voice",voiceId||"Not assigned"]],hue=csNameHue(rec.name);return`
-
- ${csAvatarHtml(s.name?s:{...s,name:rec.name,_image:rec.image})} -
-
${escHtml(fullName)}
- ${escHtml(tierLabel)} -
-
-
-
-
Evil${scoreLabel} \xB7 ${score}/100Good
-
-
-
- ${facts.filter(([,v])=>String(v||"").trim()).map(([label,value])=>`
${escHtml(label)}${escHtml(String(value))}
`).join("")} -
-
-
`}function csLcdField(label,value,sheet,field){if(!value)return"";const badge=sheet&&field?csSourceBadgeHtml(sheet,field):"";return label?`
${escHtml(label)}
${escHtml(String(value))}${badge?` ${badge}`:""}
`:`
${escHtml(String(value))}${badge?` ${badge}`:""}
`}function csLcdSection(icon,label,fieldsHtml,full=!1){const inner=(fieldsHtml||[]).filter(Boolean).join("");return inner?`
${inner}
`:""}function csCardHtml(s,meta={}){const inv=(s.inventory||[]).filter(Boolean),invHtml=inv.length?`
Signature Items
${inv.map(escHtml).join(", ")}
`:"",attrs=s.attribute_high||s.attribute_low?`
Core Attributes
\u25B2 ${escHtml(s.attribute_high||"\u2014")}  \xB7  \u25BC ${escHtml(s.attribute_low||"\u2014")}
`:"",sources=(s.sources||[]).filter(x=>x&&(x.quote||x.page!=null||x.line_hint)),srcHtml=sources.length?`
Sources / Evidence
${sources.map((x,i)=>``).join("")}
`:"",laterNote=_cs.running?"generates after casting finishes":"not generated yet",promptParts=[csPromptBox("Voice Design Prompt",s.voice_design_prompt,"voice_design_prompt"),csPromptBox("Character Image Prompt",s.image_prompt,"image_prompt")].filter((_,i)=>[s.voice_design_prompt,s.image_prompt][i]);promptParts.push(csPromptBox("SillyTavern Character Prompt",s.silly_tavern_prompt,"silly_tavern_prompt",laterNote),csPromptBox("Character Concept Art Prompt",s.concept_art_prompt,"concept_art_prompt",laterNote,s.concept_art_image));const promptHtml=promptParts.length?`
${promptParts.join("")}
`:"",voiceId=meta.voice||s.voice||"";return`
- ${csProfileHeaderHtml(s,meta,csDetailActionsHtml(s))} -
-
- - ${voiceId?escHtml(voiceId):'Noch keine Stimme'} - - - - -
-
${csAlignmentBar(s.moral_alignment_score,s.arc_direction,s.arc_note)}
-
- ${(()=>{const sections=[csLcdSection("mdi-card-account-details-outline","Identit\xE4t",[csLcdField("Voller Name",s.full_name,s,"full_name"),csLcdField("Vorname",s.first_name,s,"first_name"),csLcdField("Nachname",s.last_name,s,"last_name"),csLcdField("Alter",s.age_estimate,s,"age_estimate"),csLcdField("Geschlecht",s.gender,s,"gender"),csLcdField("Rasse / Spezies",s.race_species,s,"race_species"),csLcdField("Sprachen",s.languages,s,"languages"),csLcdField("Titel",s.title,s,"title"),csLcdField("Beruf / Rolle",s.profession,s,"profession"),csLcdField("Herkunft",s.nationality_background,s,"nationality_background"),csLcdField("Sozialer Stand",s.social_class,s,"social_class"),csLcdField("Religion",s.religious_beliefs,s,"religious_beliefs"),csLcdField("Ruf",s.reputation,s,"reputation")]),csLcdSection("mdi-account-outline","Erscheinung",[csLcdField("K\xF6rperlich",s.physical,s,"physical"),csLcdField("Kleidung",s.clothing,s,"clothing")]),csLcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[csLcdField("Eigenheiten",s.mannerisms,s,"mannerisms"),csLcdField("Kommunikationsstil",s.communication_style,s,"communication_style"),csLcdField("Stimme & Sprache",s.voice_pattern,s,"voice_pattern")]),csLcdSection("mdi-book-open-outline","Geschichte",[csLcdField("Hintergrund",s.backstory,s,"backstory"),csLcdField("Motivation",s.motivation,s,"motivation"),csLcdField("\xC4ngste",s.fears,s,"fears")]),csLcdSection("mdi-sword","F\xE4higkeiten",[csLcdField("Fertigkeiten",s.skills,s,"skills"),csLcdField("Besondere F\xE4higkeiten",s.capabilities,s,"capabilities"),attrs,invHtml]),csLcdSection("mdi-shield-sword-outline","Konflikt",[csLcdField("Geheimnis / Fataler Fehler",s.secret,s,"secret"),csLcdField("Konfliktstil",s.conflict_style,s,"conflict_style"),csLcdField("Siegbedingung",s.win_condition,s,"win_condition")]),csLcdSection("mdi-account-group-outline","Beziehungen",[csLcdField("",s.relationships,s,"relationships")],!0),csLcdSection("mdi-note-text-outline","Notizen",[csLcdField("",s.notes,s,"notes")],!0)].filter(Boolean);return sections.length?sections.join(""):`
This character's profile hasn't been generated yet \u2014 it fills in as the passage containing their scenes is processed.
`})()} -
-
- ${srcHtml} - ${promptHtml} -
-
Click a i mark next to a field above to see the exact sentence it was drawn from.
-
-
`}function csToMarkdown(sheets){var _a2;const sec=t=>` -## ${t} -`;let md=`# Character Sheets -`;for(const tier of["main","supp"]){const list=sheets.filter(s=>s.tier==="main"==(tier==="main"));if(list.length){md+=sec(tier==="main"?"Main characters":"Supporting characters");for(const s of list){md+=` -### ${s.name}${s.archetype?" \u2014 "+s.archetype:""} -`,s.aliases&&(md+=`*also known as ${s.aliases}* -`),s.gender&&(md+=`- **Gender:** ${s.gender} -`),s.title&&(md+=`- **Title:** ${s.title} -`),s.profession&&(md+=`- **Occupation:** ${s.profession} -`),(s.first_name||s.last_name)&&(md+=`- **Name parts:** ${[s.first_name,s.last_name].filter(Boolean).join(" ")} -`);const mas=(_a2=s.moral_alignment_score)!=null?_a2:50,masLabel=mas>=70?"Good":mas<=30?"Evil":"Neutral";md+=`- **Moral alignment:** ${mas}/100 (${masLabel}) \u2014 Arc: ${s.arc_direction||"neutral"} -`,s.arc_note&&(md+=` *${s.arc_note}* -`);const f=(l,v)=>v?`- **${l}:** ${v} -`:"";md+=f("Physical",s.physical)+f("Clothing",s.clothing)+f("Alignment & Ethos",s.alignment)+f("Core Attributes",[s.attribute_high&&"\u25B2 "+s.attribute_high,s.attribute_low&&"\u25BC "+s.attribute_low].filter(Boolean).join(" \xB7 "))+f("Trained Skills",s.skills)+f("Capabilities",s.capabilities)+f("Backstory & Origin",s.backstory)+f("Relationships",s.relationships)+f("Motivation",s.motivation)+f("Fears",s.fears)+f("Mannerisms & Habits",s.mannerisms)+f("Voice & Speech",s.voice_pattern)+f("Voice Design Prompt",s.voice_design_prompt)+f("Image Generation Prompt",s.image_prompt)+f("SillyTavern Character Prompt",s.silly_tavern_prompt)+f("Character Concept Art Prompt",s.concept_art_prompt)+f("Signature Items",(s.inventory||[]).join(", "))+f("Dark Secret / Fatal Flaw",s.secret)+f("Conflict Style",s.conflict_style)+f("Win Condition",s.win_condition);const src=(s.sources||[]).filter(x=>x&&x.quote).map(x=>`${x.line_hint?"["+x.line_hint+"] ":""}${x.page!=null?"p."+x.page+" ":""}"${x.quote}"`).join("; ");src&&(md+=`- *Sources:* ${src} -`)}}}return md.trim()}function csWireResultInteractions(root,sheets,sourceText,book,inline=!1){var _a2,_b2;const copyBtn=root.querySelector("#cs-copy");copyBtn==null||copyBtn.addEventListener("click",()=>{var _a3;(_a3=navigator.clipboard)==null||_a3.writeText(csToMarkdown(sheets)).then(()=>toast("Copied as Markdown","success"),()=>toast("Copy failed","error"))});const closeBtn=root.querySelector("#cs-close");closeBtn==null||closeBtn.addEventListener("click",()=>{var _a3;inline?typeof navLibraryView=="function"&&navLibraryView("characters"):(_a3=document.getElementById("cs-overlay"))==null||_a3.remove()}),(_a2=root.querySelector("#cs-open-cast"))==null||_a2.addEventListener("click",()=>csGoToLibrary()),(_b2=root.querySelector("#cs-back-cast-audio"))==null||_b2.addEventListener("click",()=>{typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("cast")});const bkCtx=book||"";root.querySelectorAll(".cs-avatar").forEach(av=>{av.addEventListener("click",async e=>{e.stopPropagation();const name=av.dataset.name,s=sheets.find(x=>x.name===name);if(!s)return;const id=`${bkCtx}::${name}`.toLowerCase();let rec=typeof clGet=="function"?await clGet(id).catch(()=>null):null;if(!rec&&typeof clUpsert=="function"&&(rec=await clUpsert(bkCtx,s).catch(()=>null)),!rec||typeof _openAvatarLightbox!="function"){toast("Avatar editor is unavailable right now","error");return}_openAvatarLightbox(rec,async updatedRec=>{s._image=(updatedRec==null?void 0:updatedRec.image)||rec.image||"";const card=root.querySelector(`.cs-card[data-name="${CSS.escape(name)}"]`);if(card){const oldAv=card.querySelector(".cs-avatar");oldAv&&(oldAv.outerHTML=csAvatarHtml(s))}})})}),root.querySelectorAll(".cs-deep-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation();const sheet=sheets.find(s=>s.name===btn.dataset.name);sheet&&csDeepAnalysis(sheet,sourceText)})}),root.querySelectorAll(".cs-auto-refine-btn").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const name=String(btn.dataset.name||"").trim();if(name){if(typeof window.csForReaderSelective=="function"){await window.csForReaderSelective([name]);return}toast("Character refinement is unavailable right now","error")}})}),root.querySelectorAll(".cs-edit-btn").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const name=String(btn.dataset.name||"").trim();if(!(!name||typeof clEdit!="function"))try{const all=typeof clGetAll=="function"?await clGetAll():[],wantBook=String(book||"").trim().toLowerCase(),rec=all.find(r=>{var _a3;const recName=String((r==null?void 0:r.name)||((_a3=r==null?void 0:r.sheet)==null?void 0:_a3.name)||"").trim().toLowerCase(),recBook=String((r==null?void 0:r.book)||"").trim().toLowerCase();return recName===name.toLowerCase()&&(!wantBook||recBook===wantBook)})||all.find(r=>{var _a3;return String((r==null?void 0:r.name)||((_a3=r==null?void 0:r.sheet)==null?void 0:_a3.name)||"").trim().toLowerCase()===name.toLowerCase()});if(!rec){toast("Character record not found in library","error");return}clEdit(rec.id)}catch{toast("Could not open editor","error")}})}),root.querySelectorAll(".cs-img-btn").forEach(btn=>{btn.addEventListener("click",e=>{var _a3;e.stopPropagation();const sheet=sheets.find(s=>s.name===btn.dataset.name);sheet&&((_a3=navigator.clipboard)==null||_a3.writeText(csBuildImagePrompt(sheet)).then(()=>toast("Image prompt copied","success"),()=>toast("Copy failed","error")))})}),root.querySelectorAll(".cs-voice-btn").forEach(btn=>{btn.addEventListener("click",e=>{var _a3;e.stopPropagation();const sheet=sheets.find(s=>s.name===btn.dataset.name);sheet&&((_a3=navigator.clipboard)==null||_a3.writeText(csBuildVoicePrompt(sheet)).then(()=>toast("Voice prompt copied \u2014 paste into Design a Voice","success"),()=>toast("Copy failed","error")))})}),root.querySelectorAll(".cs-prompt-text[data-sheet-key]").forEach(el=>{el.addEventListener("input",()=>{const key=el.dataset.sheetKey,card=el.closest(".cs-card"),sheet=sheets.find(s=>s.name===(card==null?void 0:card.dataset.name));sheet&&key&&(sheet[key]=el.textContent||"")})}),root.querySelectorAll(".cs-prompt-copy").forEach(btn=>{btn.addEventListener("click",async e=>{var _a3,_b3,_c2;e.stopPropagation();const text=((_b3=(_a3=btn.closest(".lcd-prompt-body"))==null?void 0:_a3.querySelector(".cs-prompt-text"))==null?void 0:_b3.textContent.trim())||"";if(!text){toast("Nothing to copy yet \u2014 generate first","error");return}typeof copyText=="function"?await copyText(text):(_c2=navigator.clipboard)==null||_c2.writeText(text),toast("Prompt copied","success")})}),root.querySelectorAll(".cs-gen-prompt").forEach(btn=>{btn.addEventListener("click",async e=>{var _a3;e.stopPropagation();const key=btn.dataset.sheetKey,card=btn.closest(".cs-card"),name=card==null?void 0:card.dataset.name,sheet=sheets.find(s=>s.name===name);if(!sheet||!key)return;const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026';try{const sample=[sheet.physical,sheet.backstory,sheet.motivation,sheet.relationships].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""},r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:sheet.name,book:book||"",sheet,language,llm_url:target.url,model:target.model,fields:[key]})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();if(!d[key])throw new Error("Empty response \u2014 try again");sheet[key]=d[key];const textEl=(_a3=btn.closest(".lcd-prompt-body"))==null?void 0:_a3.querySelector(".cs-prompt-text");textEl&&(textEl.textContent=d[key]),typeof csSaveToLibrary=="function"&&await csSaveToLibrary(book,sheets),toast("Prompt generated","success")}catch(err){toast("Prompt generation failed: "+(err.message||err),"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}),root.querySelectorAll(".cs-prompt-act").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const card=btn.closest(".cs-card"),name=card==null?void 0:card.dataset.name,sheet=sheets.find(s=>s.name===name);if(!sheet)return;const act=btn.dataset.act;let rec=typeof clUpsert=="function"?await clUpsert(book||"",sheet).catch(()=>null):null;if(!rec){toast("Could not save this character yet \u2014 try again","error");return}if(act==="voice"){typeof _charDesignVoice=="function"&&_charDesignVoice(rec);return}if(act==="image"){const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026';try{await _charAutoGenerateImage(rec),sheet._image=rec.image;const av=card==null?void 0:card.querySelector(".cs-avatar");av&&(av.outerHTML=csAvatarHtml(sheet)),toast("Profile image generated","success")}catch(err){toast("Image generation failed: "+(err.message||err),"error")}finally{btn.disabled=!1,btn.innerHTML=orig}}if(act==="concept_art"){const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026';try{const img=await _charAutoGenerateConceptArt(rec);sheet.concept_art_image=img;const box=btn.closest(".cs-prompt-box"),body=box==null?void 0:box.querySelector(".lcd-prompt-body"),existingPreview=body==null?void 0:body.querySelector(".cs-concept-art-preview");existingPreview?existingPreview.querySelector("img").src=img:body&&body.insertAdjacentHTML("afterbegin",`
Concept art
`),btn.innerHTML=' Regenerate Concept Art',toast("Concept art generated","success")}catch(err){toast("Concept art generation failed: "+(err.message||err),"error"),btn.innerHTML=orig}finally{btn.disabled=!1}}})}),root.querySelectorAll(".cs-edit-toggle").forEach(btn=>{const card=btn.closest(".cs-card");if(!card)return;const saveTimers=new Map;btn.addEventListener("click",e=>{e.stopPropagation();const editing=card.classList.toggle("cs-editing");btn.innerHTML=editing?'':'',btn.title=editing?"Done editing":"Edit character sheet",card.querySelectorAll(".cs-field-editable[data-sheet-key]").forEach(el=>{el.contentEditable=editing?"true":"false",editing&&!el.dataset.wired&&(el.dataset.wired="1",el.addEventListener("input",()=>{const key=el.dataset.sheetKey,name=card.dataset.name,sheet=sheets.find(s=>s.name===name);!sheet||!key||(clearTimeout(saveTimers.get(key)),saveTimers.set(key,setTimeout(async()=>{sheet[key]=el.textContent.trim(),typeof csSaveToLibrary=="function"&&await csSaveToLibrary(book,[sheet])},900)))}))})})}),root.querySelectorAll(".cs-source-link, .cs-source-mark").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const pg=parseInt(btn.dataset.page,10),quote=(btn.dataset.quote||"").trim();if(!pg){toast("No page reference saved for this source","error");return}if(typeof window.readerJumpToPage!="function"){toast(`Source: page ${pg}`,"info");return}if(await window.readerJumpToPage(pg),quote&&typeof readerSearchApply=="function"){const term=quote.length>60?quote.slice(0,60):quote;readerSearchApply(term,{jump:!0});const inp=document.getElementById("reader-search");inp&&(inp.value=term)}})})}async function csShow(sheets,title,sourceText,book,hostEl=null){var _a2;const inline=!!hostEl;if((_a2=document.getElementById("cs-overlay"))==null||_a2.remove(),typeof clGetAll=="function")try{const bk=book||title||"",all=await clGetAll(),imgMap=new Map(all.filter(r=>r.image).map(r=>[r.id,r.image]));sheets.forEach(s=>{const id=`${bk}::${s.name}`.toLowerCase();imgMap.has(id)&&(s._image=imgMap.get(id))})}catch{}const bkCtx=book||title||"";let allRecs=[];try{allRecs=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(bkCtx):[]}catch{allRecs=[]}const recByName=new Map(allRecs.map(r=>[String(r.name||"").toLowerCase(),r])),recFor=s=>recByName.get(String(s.name||"").toLowerCase())||{id:"",name:s.name,sheet:s,voice:s.voice,book:bkCtx,tags:""},recsById=new Map,cardsFor=list=>list.map(s=>{const rec=recFor(s);return rec.id&&recsById.set(rec.id,rec),typeof _charCardHtml=="function"?_charCardHtml(rec,allRecs):csCardHtml(s)}).join(""),main=sheets.filter(s=>s.tier==="main"),supp=sheets.filter(s=>s.tier!=="main"),group=(label,list)=>list.length?`
${label}
`+cardsFor(list):"",ov=inline?hostEl:document.createElement("div");if(inline?ov.innerHTML=`
-
- - ${escHtml(title||"Character sheets")} - ${sheets.length} character${sheets.length!==1?"s":""} - - - - - -
-
Generated sheets are saved into the cast library. Use the buttons above to jump back to the audiobook cast or continue to the roster stage.
-
${group("Main characters",main)}${group("Supporting characters",supp)}
-
`:(ov.id="cs-overlay",ov.className="audiobook-overlay",ov.innerHTML=`
-
- - ${escHtml(title||"Character sheets")} - ${sheets.length} character${sheets.length!==1?"s":""} - - - - -
-
${group("Main characters",main)}${group("Supporting characters",supp)}
-
`,document.body.appendChild(ov),ov.addEventListener("click",e=>{e.target===ov&&ov.remove()})),csWireResultInteractions(ov,sheets,sourceText,book,inline),typeof _wireCharCards=="function"){const listEl=ov.querySelector(".cs-list")||ov;_wireCharCards(ov,recsById,allRecs,null,{container:listEl,onBack:()=>csShow(sheets,title,sourceText,book,hostEl)})}}async function csSaveToLibrary(book,sheets){if(typeof clUpsertMany=="function")try{const n=await clUpsertMany(book,sheets);n&&toast(`${n} character${n!==1?"s":""} saved to library`,"success")}catch{}}async function csAutoGenerateExternalPrompts(book,sheets){if(!Array.isArray(sheets)||!sheets.length)return;const target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""};let done=0,failed=0,repeatMsg="",repeatCount=0;toast(`Generating SillyTavern + Concept Art prompts for ${sheets.length} character${sheets.length!==1?"s":""}\u2026`,"info");for(const s of sheets)if(!(!s||!s.name))try{const sample=[s.physical,s.backstory,s.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:s.name,book,sheet:s,language,llm_url:target.url,model:target.model,fields:["silly_tavern_prompt","concept_art_prompt"]})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();d.silly_tavern_prompt&&(s.silly_tavern_prompt=d.silly_tavern_prompt),d.concept_art_prompt&&(s.concept_art_prompt=d.concept_art_prompt),done++,repeatCount=0}catch(e){failed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto external prompts]",s.name,e),msg===repeatMsg?repeatCount++:(repeatMsg=msg,repeatCount=1),repeatCount>=3)break}try{await clUpsertMany(book,sheets)}catch{}const suffix=failed?` (${failed} failed${repeatMsg?": "+repeatMsg.slice(0,160):""})`:"";if(toast(`External prompts generated for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),typeof _charAutoGenerateConceptArt!="function")return;const withPrompt=sheets.filter(s=>s&&s.name&&_libStr(s.concept_art_prompt).trim());if(!withPrompt.length)return;let imgDone=0,imgFailed=0,imgRepeatMsg="",imgRepeatCount=0;toast(`Generating concept art for ${withPrompt.length} character${withPrompt.length!==1?"s":""}\u2026`,"info");for(const s of withPrompt)try{await _charAutoGenerateConceptArt({id:clKey(book,s.name),sheet:s}),imgDone++,imgRepeatCount=0}catch(e){imgFailed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto concept art]",s.name,e),msg===imgRepeatMsg?imgRepeatCount++:(imgRepeatMsg=msg,imgRepeatCount=1),imgRepeatCount>=3)break}const imgSuffix=imgFailed?` (${imgFailed} failed${imgRepeatMsg?": "+imgRepeatMsg.slice(0,160):""})`:"";toast(`Concept art generated for ${imgDone} character${imgDone!==1?"s":""}${imgSuffix}`,imgFailed&&!imgDone?"error":"success")}function csGoToLibrary(){typeof navTo=="function"&&navTo("s-library"),typeof navLibraryView=="function"?navLibraryView("characters"):typeof libraryRender=="function"&&libraryRender("characters"),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib")}function csAttachLineCounts(sheets,counts){!counts||!counts.size||sheets.forEach(function(s){const n=counts.get(String(s.name||"").trim().toLowerCase());n!=null&&(s.line_count=n)})}async function csForReader(opts={}){var _a2;const fresh=!!opts.fresh,text=csReaderText(),book=readerState.title||"Untitled book",key="reader:"+(readerState.title||"")+":"+(typeof readerScopeIndices=="function"?readerScopeIndices().length:0);typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(!fresh&&_cs.cache[key]){const cached=csCanonicalizeRosterNames(_cs.cache[key],csKnownReaderRoster());_cs.cache[key]=cached,await csSaveToLibrary(book,cached),pageHost&&await csShow(cached,book,text,book,pageHost);return}const knownRoster=csKnownReaderRoster();await csEnsureBookContext(book,text,knownRoster,{force:fresh});let seedSheets=[];if(!fresh)try{if(typeof clGetAllByTagOrBook=="function"){const existing=await clGetAllByTagOrBook(book),stillCast=rec=>{var _a3,_b2,_c2,_d2;return!knownRoster.length||csSheetMatchesRoster({name:(rec==null?void 0:rec.name)||"",aliases:((_a3=rec==null?void 0:rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec==null?void 0:rec.sheet)==null?void 0:_d2.full_name)||""},knownRoster)};seedSheets=(existing||[]).filter(stillCast).map(rec=>{const sheet=csSeedSheet(rec),rosterName=csRosterCanonicalName(sheet,knownRoster);return rosterName&&sheet.name.toLowerCase()!==rosterName.toLowerCase()&&(sheet.aliases=csMergeAliases({name:rosterName,aliases:sheet.aliases},{name:sheet.name,aliases:""}),sheet.name=rosterName),sheet}).filter(s=>String(s.name||"").trim())}}catch{seedSheets=[]}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook,lineCounts=new Map;if((_a2=ab==null?void 0:ab.segments)!=null&&_a2.length)for(const s of ab.segments){if((s==null?void 0:s.type)!=="dialogue"||!s.speaker)continue;const k=String(s.speaker).trim().toLowerCase();lineCounts.set(k,(lineCounts.get(k)||0)+1)}const sheets=await csGenerate(text,key,knownRoster,{pageHost,seedSheets,lineCounts});if(sheets){if(!sheets.length){toast("No characters found","error");return}lineCounts.size&&csAttachLineCounts(sheets,lineCounts),await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,book,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success"),csAutoGenerateExternalPrompts(book,sheets)}}async function csForReaderSelective(selectedNames){var _a2;const wanted=new Set((selectedNames||[]).map(n=>String(n).trim().toLowerCase()));if(!wanted.size){toast("No characters selected","error");return}const text=csReaderText(),book=readerState.title||"Untitled book";typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost(),knownRoster=csKnownReaderRoster();let records=[];try{records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(book):[]}catch{records=[]}const picked=[],seen=new Set,matchesWanted=rec=>{var _a3,_b2,_c2,_d2;const tokens=csNameTokens({name:(rec==null?void 0:rec.name)||"",aliases:((_a3=rec==null?void 0:rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec==null?void 0:rec.sheet)==null?void 0:_d2.full_name)||""});for(const t of tokens){const lower=String(t||"").toLowerCase();for(const w of wanted)if(lower===w||lower.includes(w)||w.includes(lower))return!0}return!1};if((selectedNames||[]).forEach(name=>{const canonical=String(name||"").trim(),key=canonical.toLowerCase();if(!key||seen.has(key))return;let chosen=(records||[]).find(rec=>String((rec==null?void 0:rec.name)||"").trim().toLowerCase()===key)||(records||[]).find(rec=>{var _a3,_b2,_c2,_d2;return!rec||!rec.name?!1:csNameTokens({name:rec.name,aliases:((_a3=rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec.sheet)==null?void 0:_d2.full_name)||""}).some(t=>{const lower=String(t||"").toLowerCase();return lower===key||lower.includes(key)||key.includes(lower)})})||{name:canonical,sheet:csBlankSheet(canonical)};chosen.name!==canonical&&(chosen=Object.assign({},chosen,{name:canonical,sheet:Object.assign({},chosen.sheet||{},{name:canonical})})),seen.add(key),picked.push(chosen)}),picked.sort((a,b)=>{var _a3,_b2;return(((_a3=b==null?void 0:b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a==null?void 0:a.sheet)==null?void 0:_b2.line_count)||0)||String((a==null?void 0:a.name)||"").localeCompare(String((b==null?void 0:b.name)||""))}),!picked.length){toast("No matching cast characters found","error");return}const finalMap=new Map;for(const rec of picked){const targetName=String(rec.name||"").trim();if(!targetName)continue;const rosterName=targetName,needles=csRecordNeedles(rec),scanText=csEvidenceWindowText(csReaderParagraphBlocks(),needles,2,2)||text,seedSheet=csSeedSheet(rec);seedSheet.name=targetName;const sheets=await csGenerate(scanText,null,[rosterName],{pageHost,seedSheets:[seedSheet]});if(!sheets)return;const filtered2=sheets.filter(s=>wanted.has(String(s.name||"").trim().toLowerCase()));filtered2.length&&(filtered2.forEach(s=>{s.name=targetName,s._rosterNameAuthoritative=!0}),((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.line_count)!=null&&filtered2.forEach(s=>{s.line_count=rec.sheet.line_count}),csMerge(finalMap,filtered2))}const filtered=[...finalMap.values()];if(!filtered.length){toast("None of the selected characters turned up in this pass \u2014 try again or pick different ones","error");return}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;if(ab!=null&&ab.roster){const counts=new Map;ab.roster.forEach(function(info,name){counts.set(String(name).trim().toLowerCase(),info.count||0)}),csAttachLineCounts(filtered,counts)}await csSaveToLibrary(book,filtered),pageHost&&await csShow(filtered,book,text,book,pageHost),toast(filtered.length+" character"+(filtered.length!==1?"s":"")+" defined","success")}async function csForRehearser(){var _a2,_b2,_c2,_d2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Character sheets",book=((_b2=$("reh-script-title"))==null?void 0:_b2.value.trim())||"Untitled script",text=csRehearserText(),key="reh:"+title+":"+(rehState.lines||[]).length;typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(_cs.cache[key]){await csSaveToLibrary(book,_cs.cache[key]),pageHost&&await csShow(_cs.cache[key],title,text,book,pageHost);return}let seedSheets=[];try{typeof clGetAllByTagOrBook=="function"&&(seedSheets=(await clGetAllByTagOrBook(book)||[]).map(rec=>csSeedSheet(rec)).filter(s=>String(s.name||"").trim()))}catch{seedSheets=[]}const sheets=await csGenerate(text,key,null,{pageHost,seedSheets});if(sheets){if(!sheets.length){toast("No characters found","error");return}if((_d2=(_c2=window.rehState)==null?void 0:_c2.lines)!=null&&_d2.length){const counts=new Map;rehState.lines.forEach(function(l){if(l.type!=="dialog"||!l.speaker)return;const k=String(l.speaker).trim().toLowerCase();counts.set(k,(counts.get(k)||0)+1)}),csAttachLineCounts(sheets,counts)}await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,title,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success")}}window.csForReader=csForReader,window.csForReaderSelective=csForReaderSelective,window.csForRehearser=csForRehearser,(_tc=$("reader-charsheets-btn"))==null||_tc.addEventListener("click",csForReader),(_uc=$("reh-charsheets-btn"))==null||_uc.addEventListener("click",csForRehearser);const CL_EDIT_FIELDS=[["name","Name"],["aliases","Aliases / also known as"],["first_name","First name"],["last_name","Last name"],["title","Title / role"],["age_estimate","Estimated age"],["race_species","Race / species"],["languages","Languages"],["nationality_background","Nationality / background"],["social_class","Social class"],["archetype","Archetype"],["physical","Physical"],["clothing","Clothing & Appearance"],["alignment","Alignment & Ethos"],["arc_note","Arc note"],["skills","Trained Skills"],["capabilities","Capabilities"],["backstory","Backstory & Origin"],["relationships","Relationships"],["motivation","Motivation"],["fears","Fears"],["mannerisms","Mannerisms & Habits"],["communication_style","Communication style"],["reputation","Reputation"],["religious_beliefs","Religious beliefs"],["notes","Notes"],["voice_pattern","Voice & Speech"],["voice_design_prompt","Voice Design Prompt"],["image_prompt","Image Generation Prompt"],["silly_tavern_prompt","SillyTavern Character Prompt"],["concept_art_prompt","Concept Art Prompt"],["secret","Dark Secret / Fatal Flaw"],["conflict_style","Conflict Style"],["win_condition","Win Condition"]];async function clGetAll(){const r=await fetch("/api/characters");if(!r.ok)throw new Error("clGetAll failed: "+r.status);return(await r.json()).characters||[]}async function clGet(id){const r=await fetch("/api/characters/"+encodeURIComponent(id));if(r.status!==404){if(!r.ok)throw new Error("clGet failed: "+r.status);return r.json()}}async function clPut(rec){rec!=null&&rec.color&&(rec.sheet=rec.sheet||{},rec.sheet.color=clNormalizeColor(rec.color,rec.name));const r=await fetch("/api/characters/"+encodeURIComponent(rec.id),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(rec)});if(!r.ok)throw new Error("clPut failed: "+r.status);const saved=await r.json();return typeof window._rehSyncCastVoiceFromLibrary=="function"&&window._rehSyncCastVoiceFromLibrary(saved),saved}async function clDelete(id){const r=await fetch("/api/characters/"+encodeURIComponent(id),{method:"DELETE"});if(!r.ok)throw new Error("clDelete failed: "+r.status)}function clKey(book,name){return`${String(book||"").trim()}::${String(name||"").trim()}`.toLowerCase()}function clHslToHex(h,s,l){s/=100,l/=100;const k=n=>(n+h/30)%12,a=s*Math.min(l,1-l),f=n=>l-a*Math.max(-1,Math.min(k(n)-3,Math.min(9-k(n),1)));return"#"+[f(0),f(8),f(4)].map(x=>Math.round(255*x).toString(16).padStart(2,"0")).join("")}function clNameHue(name){return Math.abs((name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0))}function clNormalizeColor(color,name){const c=String(color||"").trim();return/^#[0-9a-f]{6}$/i.test(c)?c:/^#[0-9a-f]{3}$/i.test(c)?"#"+c.slice(1).split("").map(ch=>ch+ch).join(""):clHslToHex(clNameHue(name),58,43)}const CL_IDENTITY_FIELDS=["name","aliases","first_name","last_name","full_name"],CL_ALIAS_MAX_TOKENS=12,CL_ALIAS_MAX_CHARS=500,CL_ALIAS_STOPWORDS=new Set(["die","der","das","den","dem","des","ein","eine","einer","er","sie","es","ich","du","wir","ihr","the","a","an","he","she","it","they","who"]);function clSplitIdentityTokens(v,opts={}){const raw=_clStr(v),parts=raw.split(/[,;/|]|\baka\b|\baka\.\b|\balias(?:es)?\b|\bgenannt\b|\bnamens\b|\bcalled\b|\bknown as\b/i).map(x=>x.trim()).filter(Boolean).filter(x=>x.length<=80&&!/^needs?:/i.test(x)&&!/^complete$/i.test(x)).filter(x=>!CL_ALIAS_STOPWORDS.has(x.toLowerCase()));return opts.aliases&&(raw.length>CL_ALIAS_MAX_CHARS||parts.length>CL_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CL_ALIAS_MAX_TOKENS:void 0)}function clIdentityNames(recOrSheet){const s=(recOrSheet==null?void 0:recOrSheet.sheet)||recOrSheet||{},out=new Set,add=(v,opts={})=>clSplitIdentityTokens(v,opts).forEach(x=>out.add(x.toLowerCase()));return add((recOrSheet==null?void 0:recOrSheet.name)||s.name),CL_IDENTITY_FIELDS.filter(k=>k!=="name").forEach(k=>add(s[k],{aliases:k==="aliases"})),out}function clMergeAliases(existing,incoming){const names=new Map,add=v=>clSplitIdentityTokens(v,{aliases:!0}).forEach(x=>names.set(x.toLowerCase(),x));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function clSameIdentity(rec,book,sheet){if(String((rec==null?void 0:rec.book)||"").trim().toLowerCase()!==String(book||"").trim().toLowerCase())return!1;const a=clIdentityNames(rec),b=clIdentityNames(sheet);for(const n of b)if(a.has(n))return!0;return!1}function _clStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _clSanitize(sheet){const out={...sheet};return(typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name")).forEach(f=>{out[f]!=null&&(out[f]=_clStr(out[f]))}),out}function clMergeSheet(existing,incoming){const e={...existing},inc=_clSanitize(incoming),scalars=typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name"),oldAliases=e.aliases;return scalars.forEach(f=>{(inc[f]||"").length>(e[f]||"").length&&(e[f]=inc[f])}),e.aliases=clMergeAliases({...e,aliases:oldAliases},incoming),incoming.tier==="main"&&(e.tier="main"),incoming.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+incoming.moral_alignment_score)/2):incoming.moral_alignment_score),incoming.arc_direction&&incoming.arc_direction!=="neutral"&&(e.arc_direction=incoming.arc_direction),incoming.gender&&!e.gender&&(e.gender=incoming.gender),incoming.line_count!=null&&(e.line_count=incoming.line_count),e.inventory=[...existing.inventory||[]],(incoming.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),e.sources=[...existing.sources||[]],(incoming.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),e}function clMergeTags(...parts){const set=new Set;return parts.forEach(p=>String(p||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))),[...set].join(", ")}async function clUpsert(book,sheet,knownId){var _a2;const name=(sheet.name||"").trim();if(!name)return null;const rosterNameAuthoritative=sheet._rosterNameAuthoritative===!0,bk=(book||"").trim()||"Unsorted",authoritativeId=rosterNameAuthoritative?clKey(bk,name):null,aliasPrev=authoritativeId?await clGet(authoritativeId).catch(()=>null):knownId?await clGet(knownId).catch(()=>null):(await clGetAll().catch(()=>[])).find(r=>clSameIdentity(r,bk,sheet)),id=authoritativeId||(aliasPrev==null?void 0:aliasPrev.id)||knownId||clKey(bk,name),now=new Date,prev=aliasPrev||await clGet(id).catch(()=>null),sanitized=_clSanitize(sheet);delete sanitized._rosterNameAuthoritative;const merged=prev?clMergeSheet(prev.sheet||{},sheet):{...sanitized,name},canonicalName=rosterNameAuthoritative?name:(prev==null?void 0:prev.name)||name;merged.name=canonicalName;const tags=clMergeTags(prev==null?void 0:prev.tags,sheet.tags,bk),color=clNormalizeColor((prev==null?void 0:prev.color)||((_a2=prev==null?void 0:prev.sheet)==null?void 0:_a2.color)||sheet.color,canonicalName);merged.color=color;const rec={id,book:bk,name:canonicalName,tags,sheet:merged,color,analysis:(prev==null?void 0:prev.analysis)||null,voice:sheet.voice||(prev==null?void 0:prev.voice)||null,image:sheet.image||(prev==null?void 0:prev.image)||null,created:(prev==null?void 0:prev.created)||now,updated:now};return await clPut(rec),rec}async function clSetImage(id,dataUrl){const rec=await clGet(id).catch(()=>null);if(rec)return rec.image=dataUrl||null,rec.updated=new Date,await clPut(rec),rec}window.clSetImage=clSetImage;async function clGetAllByTagOrBook(title){const key=String(title||"").trim().toLowerCase();return key?(await clGetAll().catch(()=>[])).filter(r=>String(r.book||"").trim().toLowerCase()===key?!0:String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===key)):[]}async function clUpsertMany(book,sheets){let n=0;for(const s of sheets||[])await clUpsert(book,s)&&n++;return n}(async function(){try{if((await clGetAll()).length>0)return;const idbRecs=await _clIdbGetAll().catch(()=>[]);if(!idbRecs.length)return;const r=await fetch("/api/characters/migrate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(idbRecs)});if(r.ok){const d=await r.json();console.log(`[characters-library] migrated ${d.imported} records from IndexedDB \u2192 SQLite`)}}catch(e){console.warn("[characters-library] migration skipped:",e)}})();function _clIdbGetAll(){return new Promise((resolve,reject)=>{const req=indexedDB.open("character-library",1);req.onerror=()=>resolve([]),req.onsuccess=e=>{const db=e.target.result;if(!db.objectStoreNames.contains("characters")){db.close(),resolve([]);return}const all=db.transaction("characters","readonly").objectStore("characters").getAll();all.onsuccess=ev=>{db.close(),resolve(ev.target.result||[])},all.onerror=()=>{db.close(),resolve([])}}})}let _clRecords=[];async function clRender(){if(!document.getElementById("cl-grid"))return;const filterEl=document.getElementById("cl-book-filter"),searchEl=document.getElementById("cl-search");filterEl&&!filterEl.dataset.bound&&(filterEl.dataset.bound="1",filterEl.addEventListener("change",clApplyFilter)),searchEl&&!searchEl.dataset.bound&&(searchEl.dataset.bound="1",searchEl.addEventListener("input",clApplyFilter));try{_clRecords=await clGetAll()}catch{_clRecords=[]}const filter=document.getElementById("cl-book-filter"),prods=clAllProductions();if(filter){const cur=filter.value;filter.innerHTML=``+prods.map(b=>``).join(""),cur&&prods.includes(cur)&&(filter.value=cur)}clApplyFilter()}function clAllProductions(){const set=new Set;return _clRecords.forEach(r=>{r.book&&set.add(r.book),String(r.tags||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))}),[...set].sort((a,b)=>a.localeCompare(b))}function clApplyFilter(){var _a2,_b2;const grid=document.getElementById("cl-grid");if(!grid)return;const book=((_a2=document.getElementById("cl-book-filter"))==null?void 0:_a2.value)||"",q=(((_b2=document.getElementById("cl-search"))==null?void 0:_b2.value)||"").trim().toLowerCase();let recs=_clRecords.slice();if(book){const bk=book.toLowerCase();recs=recs.filter(r=>(r.book||"").toLowerCase()===bk||String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===bk))}if(q&&(recs=recs.filter(r=>{var _a3,_b3,_c2,_d2,_e2;return(r.name||"").toLowerCase().includes(q)||(((_a3=r.sheet)==null?void 0:_a3.aliases)||"").toLowerCase().includes(q)||(((_b3=r.sheet)==null?void 0:_b3.first_name)||"").toLowerCase().includes(q)||(((_c2=r.sheet)==null?void 0:_c2.last_name)||"").toLowerCase().includes(q)||(((_d2=r.sheet)==null?void 0:_d2.title)||"").toLowerCase().includes(q)||(((_e2=r.sheet)==null?void 0:_e2.archetype)||"").toLowerCase().includes(q)||(r.tags||"").toLowerCase().includes(q)||(r.book||"").toLowerCase().includes(q)})),!recs.length){grid.innerHTML=`
- -

${_clRecords.length?"No characters match your filter.":"No characters yet."}

-

Run Character sheets from Read Aloud or the Script Rehearser to populate your library.

-
`;return}const byBook=new Map;for(const r of recs)byBook.has(r.book)||byBook.set(r.book,[]),byBook.get(r.book).push(r);const groups=[...byBook.keys()].sort((a,b)=>a.localeCompare(b));grid.innerHTML=groups.map(bk=>{const list=byBook.get(bk).sort((a,b)=>{var _a3,_b3;return(((_a3=a.sheet)==null?void 0:_a3.tier)==="main"?0:1)-(((_b3=b.sheet)==null?void 0:_b3.tier)==="main"?0:1)||(a.name||"").localeCompare(b.name||"")});return`
-
${escHtml(bk)} - ${list.length}
-
${list.map(clCardHtml).join("")}
-
`}).join("")}function clCardHtml(rec){return`
-
- - -
- ${typeof csOverviewCardHtml=="function"?csOverviewCardHtml(rec):csCardHtml(rec.sheet)} -
`}function clEdit(id){var _a2;const rec=_clRecords.find(r=>r.id===id);if(!rec)return;const s=rec.sheet||{},modal=document.createElement("div");modal.className="audiobook-overlay";const rows=CL_EDIT_FIELDS.map(([k,label])=>{const multiline=!["name","aliases","archetype"].includes(k),val=escHtml(String(s[k]||""));return``}).join("");modal.innerHTML=`
-
- Edit \u2014 ${escHtml(rec.name)} - - - -
-
- - - - - ${rows} -
-
`,document.body.appendChild(modal);const close=()=>modal.remove();modal.querySelector("#cl-edit-cancel").addEventListener("click",close),modal.addEventListener("click",e=>{e.target===modal&&close()}),modal.querySelector("#cl-edit-save").addEventListener("click",async()=>{const patch={};modal.querySelectorAll("[data-field]").forEach(el=>{patch[el.dataset.field]=el.value});const newName=(patch.name||"").trim()||rec.name;let mas=parseInt(patch.moral_alignment_score,10);mas=isNaN(mas)?50:Math.max(0,Math.min(100,mas));const newSheet={...s,...patch,name:newName,moral_alignment_score:mas},tagsEl=modal.querySelector('[data-rec-field="tags"]'),newTags=tagsEl?clMergeTags(tagsEl.value,rec.book):rec.tags||rec.book,colorEl=modal.querySelector('[data-rec-field="color"]'),color=clNormalizeColor((colorEl==null?void 0:colorEl.value)||rec.color||s.color,newName),newId=clKey(rec.book,newName);newSheet.color=color,rec.sheet=newSheet,rec.name=newName,rec.tags=newTags,rec.color=color,rec.updated=new Date;try{newId!==rec.id&&(await clDelete(rec.id),rec.id=newId),await clPut(rec),toast("Character saved","success"),close(),clRender()}catch(e){toast("Save failed: "+(e.message||e),"error")}})}async function clDeleteRec(id){const rec=_clRecords.find(r=>r.id===id);if(rec&&confirm(`Delete "${rec.name}" from ${rec.book}?`))try{await clDelete(id),toast("Deleted","success"),clRender()}catch(e){toast("Delete failed: "+(e.message||e),"error")}}document.addEventListener("click",e=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l2,_m2,_n2;const edit=(_b2=(_a2=e.target).closest)==null?void 0:_b2.call(_a2,".cl-edit");if(edit){e.stopPropagation(),clEdit(edit.dataset.id);return}const del=(_d2=(_c2=e.target).closest)==null?void 0:_d2.call(_c2,".cl-delete");if(del){e.stopPropagation(),clDeleteRec(del.dataset.id);return}const deep=(_f2=(_e2=e.target).closest)==null?void 0:_f2.call(_e2,".cl-card-wrap .cs-deep-btn");if(deep){e.stopPropagation();const rec=_clRecords.find(r=>{var _a3;return r.id===((_a3=deep.closest(".cl-card-wrap"))==null?void 0:_a3.dataset.id)});rec&&typeof csDeepAnalysis=="function"&&csDeepAnalysis(rec.sheet,"")}const img=(_h2=(_g2=e.target).closest)==null?void 0:_h2.call(_g2,".cl-card-wrap .cs-img-btn");if(img){e.stopPropagation();const rec=_clRecords.find(r=>{var _a3;return r.id===((_a3=img.closest(".cl-card-wrap"))==null?void 0:_a3.dataset.id)});rec&&typeof csBuildImagePrompt=="function"&&((_i2=navigator.clipboard)==null||_i2.writeText(csBuildImagePrompt(rec.sheet)).then(()=>toast("Image prompt copied","success"),()=>toast("Copy failed","error")));return}const voice=(_k2=(_j2=e.target).closest)==null?void 0:_k2.call(_j2,".cl-card-wrap .cs-voice-btn");if(voice){e.stopPropagation();const rec=_clRecords.find(r=>{var _a3;return r.id===((_a3=voice.closest(".cl-card-wrap"))==null?void 0:_a3.dataset.id)});rec&&typeof csBuildVoicePrompt=="function"&&((_l2=navigator.clipboard)==null||_l2.writeText(csBuildVoicePrompt(rec.sheet)).then(()=>toast("Voice prompt copied","success"),()=>toast("Copy failed","error")));return}const src=(_n2=(_m2=e.target).closest)==null?void 0:_n2.call(_m2,".cl-card-wrap .cs-source-link, .cl-card-wrap .cs-source-mark");if(src){e.stopPropagation();const pg=parseInt(src.dataset.page,10);pg&&typeof window.readerJumpToPage=="function"?window.readerJumpToPage(pg):pg&&toast(`Source: page ${pg}`,"info")}}),window.clRender=clRender,window.clUpsertMany=clUpsertMany,window.clUpsert=clUpsert,window.clGetAll=clGetAll,window.clPut=clPut,window.clGetAllByTagOrBook=clGetAllByTagOrBook;function stReadPngText(buf){const dv=new DataView(buf),sig=[137,80,78,71,13,10,26,10];for(let i=0;i<8;i++)if(dv.getUint8(i)!==sig[i])throw new Error("Not a PNG file");let off=8;const out={};for(;off+8<=dv.byteLength;){const len=dv.getUint32(off);off+=4;let type="";for(let i=0;i<4;i++)type+=String.fromCharCode(dv.getUint8(off+i));if(off+=4,type==="tEXt"){const bytes=new Uint8Array(buf,off,len);let sep=bytes.indexOf(0);sep===-1&&(sep=len);let keyword="";for(let i=0;im*1.3?"female":m>f*1.3?"male":""}function stToSheet(data){const tags=(Array.isArray(data.tags)?data.tags:String(data.tags||"").split(",")).map(t=>String(t||"").trim()).filter(Boolean).join(", "),ext=data.extensions||{};return{name:(data.name||data.char_name||"Unnamed").trim(),archetype:(data.creator_notes||"").split(` -`)[0].slice(0,60)||"",backstory:data.description||data.char_persona||"",mannerisms:data.personality||"",arc_note:data.scenario||"",voice_pattern:data.mes_example||data.example_dialogue||data.first_mes||data.char_greeting||"",gender:ext.gender||stGuessGender((data.description||"")+" "+(data.personality||"")),note:data.creator_notes||"",tags,voice:ext.tts_voice||null,tier:"supporting",st_card:data}}function stFromRecord(rec){const sh=rec.sheet||{},prev=sh.st_card||{},descParts=[sh.physical,sh.clothing,sh.backstory,sh.alignment].map(x=>String(x||"").trim()).filter(Boolean),persona=[sh.archetype,sh.mannerisms,sh.motivation,sh.fears].map(x=>String(x||"").trim()).filter(Boolean).join(` -`);return{spec:"chara_card_v2",spec_version:"2.0",data:{name:rec.name,description:prev.description||descParts.join(` - -`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'
').join("")}async function libraryRenderBooks(){const list=document.getElementById("lib-books-list");if(!list)return;list.innerHTML=_libSkeleton(4);let all=[];try{const r=await fetch(LIB_READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='

No books yet.

Open Read Aloud, import a PDF or text, and save it to your library.

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const total=rec.sentenceCount||0,synthPct=total?Math.round((rec.synthCount||0)/total*100):0,readPct=total?Math.round((rec.idx||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"",cov=libBookCover(rec.title||"Untitled"),coverUrl=LIB_READER_API+"/"+rec.id+"/cover?t="+new Date(rec.updated||Date.now()).getTime(),bg=rec.hasCover?`style="background-image:linear-gradient(to bottom,rgba(0,0,0,.3),rgba(0,0,0,.8)),url('`+coverUrl+`');background-size:cover;background-position:center;color:#fff"`:'style="--bk1:'+cov.c1+";--bk2:"+cov.c2+'"';return'
'+escHtml(rec.title||"Untitled")+'
'+total+" sentences"+(rec.pageCount?" \xB7 "+rec.pageCount+" pg":"")+'
'+readPct+"% read \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-book").forEach(function(el){const id=el.dataset.id,title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||(window._readerStartView="main",typeof navTo=="function"&&navTo("s-reader"),typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(id))});const reh=el.querySelector(".lib-act-rehearse");reh&&reh.addEventListener("click",function(e){e.stopPropagation(),productionOpenInRehearser(title)});const del=el.querySelector(".lib-act-del-book");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete book?","Audio files will be removed.",async function(){try{if(!(await fetch(LIB_READER_API+"/"+id,{method:"DELETE"})).ok)throw new Error("delete failed");toast("Book deleted","success"),libraryRenderBooks()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function libraryRenderPlays(){const list=document.getElementById("lib-plays-list");if(!list)return;list.innerHTML=_libSkeleton(3);let all=[];try{all=typeof rehDbGetAll=="function"?await rehDbGetAll():[]}catch{all=[]}if(!all.length){list.innerHTML='

No theater plays yet.

Open Script Rehearsal \u2192 Import / Export to add a script, or cast a book as an audiobook.

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const speakers=Object.keys(rec.cast||{}),total=typeof parseScript=="function"?parseScript(rec.script||"").filter(function(l){return l.type==="dialog"}).length:0,pct=total?Math.round((rec.lineIndex||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"\u2014",cov=libBookCover(rec.title||"Untitled"),avatars=speakers.slice(0,5).map(function(sp){return''+(sp[0]||"?").toUpperCase()+""}).join("");return'
'+escHtml(rec.title||"Untitled")+'
'+avatars+"
"+total+" lines \xB7 "+speakers.length+' cast
'+pct+"% \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-play").forEach(function(el){const id=parseInt(el.dataset.id,10),title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||openPlayInRehearser(id)});const ra=el.querySelector(".lib-act-readaloud");ra&&ra.addEventListener("click",function(e){e.stopPropagation(),productionOpenInReader(title)});const del=el.querySelector(".lib-act-del-play");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete rehearsal?","This cannot be undone.",async function(){try{typeof rehDbDelete=="function"&&await rehDbDelete(id),toast("Rehearsal deleted","success"),libraryRenderPlays()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function openPlayInRehearser(id){try{const rec=typeof rehDbGetById=="function"?await rehDbGetById(id):null;rec&&typeof loadRecord=="function"?loadRecord(rec):toast("Rehearsal not found","error")}catch{toast("Could not open rehearsal","error")}}async function productionOpenInRehearser(title){const key=prodKey(title);try{const match=(typeof rehDbGetAll=="function"?await rehDbGetAll():[]).find(function(p){return prodKey(p.title)===key});if(match){openPlayInRehearser(match.id);return}}catch{}let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&book.kind!=="pdf")try{const sr=await fetch(LIB_READER_API+"/"+book.id+"/source"),text=sr.ok?await sr.text():"";if(text&&typeof audiobookOpenInRehearser=="function"){audiobookOpenInRehearser(text,title,[]);return}}catch{}if(book&&book.kind==="pdf"){typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(book.id),toast('Open this PDF book, then use "Cast as audiobook" to build a rehearsal',"info");return}toast("No source to rehearse for this title yet","error")}async function productionOpenInReader(title){const key=prodKey(title);let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&typeof readerOpenLibraryDoc=="function"){readerOpenLibraryDoc(book.id);return}typeof navTo=="function"&&navTo("s-reader"),toast("No audiobook for this title yet \u2014 import its source in Read Aloud","info")}async function castForProduction(title){const out={};if(typeof clGetAllByTagOrBook!="function")return out;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{recs=[]}return recs.forEach(function(r){const name=(r.name||"").trim();if(!name)return;const voice=r.voice&&r.voice.id?r.voice.id:typeof r.voice=="string"?r.voice:"";out[name.toLowerCase()]={name,voice:voice||"",gender:r.sheet&&r.sheet.gender||"",soul:r.sheet&&(r.sheet.voice_pattern||r.sheet.motivation)||"",tags:r.tags||""}}),out}window.castForProduction=castForProduction;async function castWriteBack(title,castMap){if(!title||!castMap||typeof clGetAllByTagOrBook!="function"||typeof clPut!="function")return;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{return}if(!recs.length)return;const byName={};recs.forEach(function(r){byName[(r.name||"").trim().toLowerCase()]=r});let n=0;for(const sp of Object.keys(castMap)){if(String(sp).includes("NARRATOR"))continue;const voice=(castMap[sp]||{}).voice;if(!voice||voice==="me")continue;const rec=byName[String(sp).trim().toLowerCase()];if(!(!rec||(rec.voice&&rec.voice.id?rec.voice.id:typeof rec.voice=="string"?rec.voice:"")===voice)){rec.voice={id:voice},rec.updated=new Date;try{await clPut(rec),n++}catch{}}}return n}window.castWriteBack=castWriteBack;function libBookCover(title){let h=0;const s=String(title||"Untitled");for(let i=0;i'+heading+'
'+sub+'
',o.addEventListener("click",function(e){e.stopPropagation()}),o.querySelector("[data-lib-cancel]").addEventListener("click",function(e){e.stopPropagation(),o.remove()}),o.querySelector("[data-lib-ok]").addEventListener("click",async function(e){e.stopPropagation(),o.innerHTML='',await onConfirm()}),cardEl.appendChild(o)}window.libraryRenderBooks=libraryRenderBooks,window.libraryRenderPlays=libraryRenderPlays,window.productionOpenInRehearser=productionOpenInRehearser,window.productionOpenInReader=productionOpenInReader,window.prodKey=prodKey;const _CHAR_PROFILE_COMPLETENESS_FIELDS=["gender","age_estimate","race_species","languages","nationality_background","social_class","profession","title","archetype","physical","clothing","alignment","arc_direction","arc_note","attribute_high","attribute_low","skills","capabilities","backstory","relationships","motivation","fears","mannerisms","communication_style","voice_pattern","secret","conflict_style","win_condition","reputation","religious_beliefs","notes","inventory"];window._CHAR_PROFILE_COMPLETENESS_FIELDS=_CHAR_PROFILE_COMPLETENESS_FIELDS;const _CHAR_TABLE_COLUMN_DEFS=[["select","Select"],["avatar","Avatar"],["name","Name"],["gender","Gender"],["age","Age"],["lines","Lines"],["language","Language"],["alignment","Good / Evil"],["profile","Profile"],["voice","Voice"],["rating","Rating"],["prompt","Voice Design Prompt"],["notes","Notes"],["tags","Tags"],["book","Book / Script"],["export","Export"]];function _charTableHiddenColumns(){try{const saved=JSON.parse(localStorage.getItem("ttsvc_libchars_columns")||"{}");return saved&&typeof saved=="object"?saved:{}}catch{return{}}}function _applyCharTableColumns(table,hidden){table&&_CHAR_TABLE_COLUMN_DEFS.forEach(function(def,index){var _a2;const isHidden=hidden[def[0]]===!1;table.querySelectorAll("tr").forEach(function(row){const cell=row.children[index];cell&&cell.classList.toggle("lib-chars-col-hidden",isHidden)});const col=(_a2=table.querySelector("colgroup"))==null?void 0:_a2.children[index];col&&col.classList.toggle("lib-chars-col-hidden",isHidden)})}function _charProfileCompleteness(sheet){const sh=sheet||{};return{filled:_CHAR_PROFILE_COMPLETENESS_FIELDS.filter(function(key){return _libStr(sh[key]).trim().length>0}).length,total:_CHAR_PROFILE_COMPLETENESS_FIELDS.length}}let _libCharsRenderSeq=0;async function _libReviewVisualPairs(button,container){if(!button)return;button.disabled=!0;const oldLabel=button.innerHTML;button.innerHTML=' Reviewing\u2026';try{const response=await fetch("/api/characters/visual-pair-review"),payload=await response.json();if(!response.ok)throw new Error(payload.detail||"Visual-pair review failed");let panel=container.querySelector("#lib-chars-visual-review");panel||(panel=document.createElement("div"),panel.id="lib-chars-visual-review",panel.className="lib-chars-visual-review",container.insertBefore(panel,container.firstChild)),!payload.issues||!payload.issues.length?(panel.innerHTML=' Visual pairs passed the metadata and asset-reuse checks ('+payload.checked+" characters reviewed).",typeof toast=="function"&&toast("Visual pair review passed","success")):(panel.innerHTML=' Review '+payload.issues.length+" character pair"+(payload.issues.length===1?"":"s")+": "+payload.issues.map(function(item){return''+escHtml(item.name||item.id)+" \u2014 "+escHtml(item.issues.join(", "))+""}).join(""),typeof toast=="function"&&toast(payload.issues.length+" visual pair(s) need review","warning"))}catch(error){typeof toast=="function"&&toast(error.message||"Visual-pair review failed","error")}finally{button.disabled=!1,button.innerHTML=oldLabel}}async function libraryRenderCharacters(){var _a2;const container=document.getElementById("lib-chars-list");if(!container)return;const renderSeq=++_libCharsRenderSeq;if((!Array.isArray(window._voices)||!window._voices.length)&&typeof loadVoiceLibrary=="function"){try{await loadVoiceLibrary()}catch(e){console.warn("[characters] voice library load failed",e)}if(renderSeq!==_libCharsRenderSeq)return}let all=[];try{all=typeof clGetAll=="function"?await clGetAll():[]}catch(e){console.warn("[characters] load failed",e),typeof toast=="function"&&toast("Failed to load characters \u2014 keeping the current view","error");return}if(renderSeq!==_libCharsRenderSeq)return;const mainEl=document.getElementById("main-content"),savedScrollTop=!window._libCharsScrollToBook&&mainEl?mainEl.scrollTop:null;container.innerHTML='
Loading characters\u2026
';const byId=new Map(all.map(function(rec){return[rec.id,rec]}));if(!all.length){container.innerHTML='

No characters yet.

Open a book in Read Aloud, cast it as an audiobook, then click Cast Characters to generate character sheets \u2014 or import an existing cast from SillyTavern.

',container.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop);return}const byBook={};all.forEach(function(rec){const bk=rec.book||"Unsorted";byBook[bk]||(byBook[bk]=[]),byBook[bk].push(rec)}),Object.keys(byBook).forEach(function(bk){if(!byBook[bk].some(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"})){const narrRec={id:clKey(bk,"Narrator"),book:bk,name:"Narrator",tags:bk,voice:null,image:null,sheet:{}};byId.set(narrRec.id,narrRec),byBook[bk].unshift(narrRec)}}),container.innerHTML="";const viewMode=localStorage.getItem("ttsvc_libchars_view")==="table"?"table":"cards";let returnToReader=!1;try{returnToReader=sessionStorage.getItem("ttsvc_cast_return")==="reader"}catch{}const SORT_OPTIONS=[["tier","Rolle (Haupt zuerst)"],["alpha","Alphabet"],["lines","Anzahl Zeilen"],["gender","Geschlecht"],["voice","Stimme zugewiesen"],["rating","Stimmenbewertung"],["profile","Profil vollst\xE4ndig"]],sortMode=SORT_OPTIONS.some(function(o){return o[0]===localStorage.getItem("ttsvc_libchars_sort")})?localStorage.getItem("ttsvc_libchars_sort"):"tier",bar=document.createElement("div");bar.className="lib-chars-toolbar",bar.innerHTML=(returnToReader?'':"")+'
',bar.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),bar.querySelector("#lib-chars-review-visual").addEventListener("click",function(){_libReviewVisualPairs(this,container)}),(_a2=bar.querySelector("#lib-chars-back-reader"))==null||_a2.addEventListener("click",function(){try{sessionStorage.removeItem("ttsvc_cast_return")}catch{}typeof navTo=="function"&&navTo("s-reader")}),bar.querySelector("#lib-chars-sort-sel").addEventListener("change",function(){localStorage.setItem("ttsvc_libchars_sort",this.value),libraryRenderCharacters()}),bar.querySelectorAll(".lib-chars-view-toggle button").forEach(function(btn){btn.addEventListener("click",function(){localStorage.setItem("ttsvc_libchars_view",btn.dataset.view),libraryRenderCharacters()})}),container.appendChild(bar);const _charSortCmp={tier:function(a,b){var _a3,_b2,_c2,_d2;const tierOrder={main:0,supporting:1,minor:2},ta=(_b2=tierOrder[String(((_a3=a.sheet)==null?void 0:_a3.tier)||"minor").toLowerCase()])!=null?_b2:2,tb=(_d2=tierOrder[String(((_c2=b.sheet)==null?void 0:_c2.tier)||"minor").toLowerCase()])!=null?_d2:2;return ta-tb||(a.name||"").localeCompare(b.name||"")},alpha:function(a,b){return(a.name||"").localeCompare(b.name||"")},lines:function(a,b){var _a3,_b2;return(((_a3=b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)||(a.name||"").localeCompare(b.name||"")},gender:function(a,b){var _a3,_b2;const ga=String(((_a3=a.sheet)==null?void 0:_a3.gender)||"zzz"),gb=String(((_b2=b.sheet)==null?void 0:_b2.gender)||"zzz");return ga.localeCompare(gb)||(a.name||"").localeCompare(b.name||"")},voice:function(a,b){return(b.voice?1:0)-(a.voice?1:0)||(a.name||"").localeCompare(b.name||"")},rating:function(a,b){const idOf=function(r){return r.voice?typeof r.voice=="object"?r.voice.id:r.voice:""},ratingOf=function(r){var _a3;return Number(((_a3=(window._voices||[]).find(function(v){return v.id===idOf(r)}))==null?void 0:_a3.rating)||0)};return ratingOf(b)-ratingOf(a)||(a.name||"").localeCompare(b.name||"")},profile:function(a,b){const filledA=_charProfileCompleteness(a.sheet).filled;return _charProfileCompleteness(b.sheet).filled-filledA||(a.name||"").localeCompare(b.name||"")},age:function(a,b){return _charAgeSortVal(a.sheet)-_charAgeSortVal(b.sheet)||(a.name||"").localeCompare(b.name||"")},language:function(a,b){return _charLangLabel(a).localeCompare(_charLangLabel(b))||(a.name||"").localeCompare(b.name||"")},align:function(a,b){var _a3,_b2,_c2,_d2;return((_b2=(_a3=b.sheet)==null?void 0:_a3.moral_alignment_score)!=null?_b2:-1)-((_d2=(_c2=a.sheet)==null?void 0:_c2.moral_alignment_score)!=null?_d2:-1)||(a.name||"").localeCompare(b.name||"")}},sortDir=localStorage.getItem("ttsvc_libchars_sort_dir")==="desc"?"desc":"asc",productions=document.createDocumentFragment();if(Object.keys(byBook).sort().forEach(function(book){var _a3;const chars=byBook[book].sort(_charSortCmp[sortMode]||_charSortCmp.tier);sortDir==="desc"&&chars.reverse();const narrIdx=chars.findIndex(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"});narrIdx>0&&chars.unshift(chars.splice(narrIdx,1)[0]);const pageKey="ttsvc_libchars_page::"+book,pageSize=typeof window.ttvcIsLowMemoryDevice=="function"&&window.ttvcIsLowMemoryDevice()?25:50,requestedPage=Math.max(1,parseInt(localStorage.getItem(pageKey)||"1",10)||1),visibleCount=Math.min(chars.length,requestedPage*pageSize),visibleChars=chars.slice(0,visibleCount),cov=libBookCover(book),prod=document.createElement("div");prod.className="lib-chars-production",prod.dataset.book=book;const collapseKey="ttsvc_libchars_collapsed::"+book;let isCollapsed=localStorage.getItem(collapseKey)==="1";window._libCharsScrollToBook&&(isCollapsed=book!==window._libCharsScrollToBook),isCollapsed&&prod.classList.add("lib-chars-production-collapsed"),prod.innerHTML='
'+escHtml(book)+'
`+(viewMode==="table"?_charsTableHtml(visibleChars,sortMode,sortDir):'
'+visibleChars.map(function(rec){return _charCardHtml(rec,chars)}).join("")+"
")+(visibleCountShow more characters ('+visibleCount+" / "+chars.length+")":"")+"
",(_a3=prod.querySelector(".lib-chars-load-more"))==null||_a3.addEventListener("click",function(){localStorage.setItem(pageKey,String(requestedPage+1)),libraryRenderCharacters()}),prod.querySelector(".lib-chars-prod-collapse-btn").addEventListener("click",function(e){e.stopPropagation();const collapsed=prod.classList.toggle("lib-chars-production-collapsed");localStorage.setItem(collapseKey,collapsed?"1":"0")}),prod.querySelector(".lib-chars-prod-head").addEventListener("click",function(e){e.target.closest("button, select, input, a")||prod.querySelector(".lib-chars-prod-collapse-btn").click()}),prod.querySelector(".lib-chars-bookctx-btn").addEventListener("click",function(){_editBookProfile(book)}),prod.querySelector(".lib-chars-casting-btn").addEventListener("click",function(){typeof navTo=="function"&&navTo("s-reader")}),prod.querySelector(".lib-chars-cast-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book),toast("Open the book in Read Aloud then click Cast Characters","info")}),prod.querySelector(".lib-chars-reh-btn").addEventListener("click",function(){typeof productionOpenInRehearser=="function"&&productionOpenInRehearser(book)}),prod.querySelector(".lib-chars-read-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book)}),prod.querySelector(".lib-chars-imp-btn").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog(book,function(){libraryRenderCharacters()})}),prod.querySelectorAll("[data-sort-key]").forEach(function(th){th.addEventListener("click",function(){const key=th.dataset.sortKey,nextDir=sortMode===key&&sortDir==="asc"?"desc":"asc";localStorage.setItem("ttsvc_libchars_sort",key),localStorage.setItem("ttsvc_libchars_sort_dir",nextDir),libraryRenderCharacters()})});const columnWrap=prod.querySelector(".lib-chars-column-chooser"),columnButton=columnWrap==null?void 0:columnWrap.querySelector(".lib-chars-column-button"),columnMenu=columnWrap==null?void 0:columnWrap.querySelector(".lib-chars-column-menu"),table=prod.querySelector(".lib-chars-tbl");if(columnWrap&&columnButton&&columnMenu&&table){const hiddenColumns=_charTableHiddenColumns();columnMenu.querySelectorAll("input[data-column-key]").forEach(function(input){input.checked=hiddenColumns[input.dataset.columnKey]!==!1,input.addEventListener("change",function(){hiddenColumns[input.dataset.columnKey]=input.checked,localStorage.setItem("ttsvc_libchars_columns",JSON.stringify(hiddenColumns)),_applyCharTableColumns(table,hiddenColumns)})}),_applyCharTableColumns(table,hiddenColumns),columnButton.addEventListener("click",function(e){e.stopPropagation(),columnMenu.classList.toggle("is-open")}),columnMenu.addEventListener("click",function(e){e.stopPropagation()})}const selectAllBtn=prod.querySelector(".lib-chars-select-all-btn"),bulkBtn=prod.querySelector(".lib-chars-bulk-voice-btn"),bulkCount=prod.querySelector(".lib-chars-bulk-count"),designBtn=prod.querySelector(".lib-chars-bulk-design-btn"),designCount=prod.querySelector(".lib-chars-bulk-count-design"),imageBtn=prod.querySelector(".lib-chars-bulk-image-btn"),imageCount=prod.querySelector(".lib-chars-bulk-count-image"),conceptBtn=prod.querySelector(".lib-chars-bulk-concept-btn"),conceptCount=prod.querySelector(".lib-chars-bulk-count-concept"),consistentBtn=prod.querySelector(".lib-chars-bulk-consistent-btn"),consistentCount=prod.querySelector(".lib-chars-bulk-count-consistent"),visualStyleSel=prod.querySelector(".lib-chars-visual-style"),imageProviderSel=prod.querySelector(".lib-chars-image-provider");imageProviderSel&&(imageProviderSel.value=typeof _appSettings!="undefined"&&_appSettings.image_gen_provider||"");const deleteBtn=prod.querySelector(".lib-chars-bulk-delete-btn"),deleteCount=prod.querySelector(".lib-chars-bulk-count-delete"),tblSelectAllCb=prod.querySelector(".lib-chars-tbl-select-all-cb"),syncTblSelectAllCb=function(){if(!tblSelectAllCb)return;const boxes=[...prod.querySelectorAll(".lib-char-select-cb")],checkedN=boxes.filter(function(cb){return cb.checked}).length;tblSelectAllCb.checked=boxes.length>0&&checkedN===boxes.length,tblSelectAllCb.indeterminate=checkedN>0&&checkedN0&&boxes.every(function(cb){return cb.checked});boxes.forEach(function(cb){cb.checked=!allChecked}),refreshBulkBtn()}),tblSelectAllCb&&tblSelectAllCb.addEventListener("change",function(){[...prod.querySelectorAll(".lib-char-select-cb")].forEach(function(cb){cb.checked=tblSelectAllCb.checked}),refreshBulkBtn()}),syncTblSelectAllCb();const runBulk=async function(btn,ids,verb,fn){btn.disabled=!0;const orig=btn.innerHTML;let done=0,failed=0,lastErrMsg="",repeatErrMsg="",repeatCount=0,aborted=!1;for(const id of ids){const rec=byId.get(id);if(rec){btn.innerHTML=' '+verb+" "+(done+failed+1)+" / "+ids.length+"\u2026";try{await fn(rec),done++,repeatCount=0}catch(e){if(failed++,lastErrMsg=e&&e.message?e.message:String(e),console.error("[bulk "+verb+"]",rec.name,e),lastErrMsg===repeatErrMsg?repeatCount++:(repeatErrMsg=lastErrMsg,repeatCount=1),repeatCount>=3){aborted=!0;break}}}}btn.innerHTML=orig;const remaining=ids.length-done-failed,suffix=failed?` (${failed} failed${aborted&&remaining?`, ${remaining} skipped`:""}${lastErrMsg?": "+lastErrMsg.slice(0,200):""})`:"";toast(`${verb} finished for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),await _flushPendingTtsRestart(),typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary({refresh:!0}).catch(()=>{}),libraryRenderCharacters()};bulkBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(bulkBtn,ids,"Assigning",_autoAssignVoice)}),designBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(designBtn,ids,"Designing",_charAutoDesignVoice)}),imageBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});if(!ids.length)return;const provider=imageProviderSel?imageProviderSel.value:"";runBulk(imageBtn,ids,"Generating images",function(rec){return _charAutoGenerateImage(rec,provider)})}),conceptBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});if(!ids.length)return;const provider=imageProviderSel?imageProviderSel.value:"";runBulk(conceptBtn,ids,"Generating concept art",function(rec){return _charAutoGenerateConceptArt(rec,provider)})}),consistentBtn==null||consistentBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(consistentBtn,ids,"Generating consistent images",function(rec){return _charAutoGenerateConsistentImages(rec,visualStyleSel?visualStyleSel.value:"cinematic")})});const fixLangBtn=prod.querySelector(".lib-chars-fix-lang-btn");fixLangBtn==null||fixLangBtn.addEventListener("click",function(){const _bookLangCounts={};chars.forEach(function(r){const l=_charLang(r);l&&(_bookLangCounts[l]=(_bookLangCounts[l]||0)+1)});let _bookLang="",_bookLangBest=0;Object.keys(_bookLangCounts).forEach(function(l){_bookLangCounts[l]>_bookLangBest&&(_bookLang=l,_bookLangBest=_bookLangCounts[l])});const bookCode=_bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[_bookLang]:null,mismatched=chars.filter(function(rec){if(!rec.voice||!bookCode)return!1;const voiceId=typeof rec.voice=="object"?rec.voice.id:rec.voice;return _voiceLangCode(voiceId)!==bookCode});if(!mismatched.length){toast("No language-mismatched voices found in this production","info");return}runBulk(fixLangBtn,mismatched.map(function(r){return r.id}),"Redesigning",function(rec){return _charAutoDesignVoice(rec,!0)})}),deleteBtn.addEventListener("click",async function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});!ids.length||!await confirmDialog(`Delete ${ids.length} character${ids.length!==1?"s":""} from the library? This cannot be undone \u2014 use it to clear out stale/corrupted entries before a fresh recast.`,{title:"Delete characters?",okLabel:"Delete",danger:!0})||runBulk(deleteBtn,ids,"Deleting",function(rec){return clDelete(rec.id)})}),_wireCharCards(prod,byId,chars),prod.querySelectorAll(".lib-chars-tbl-rating-star").forEach(function(star){star.addEventListener("click",async function(e){e.stopPropagation();const voiceId=star.dataset.voiceId,voice=(window._voices||[]).find(function(v){return v.id===voiceId});if(!voice||typeof saveMeta!="function")return;const value=Number(star.dataset.rating||0);voice.rating=value===Number(voice.rating||0)?0:value,prod.querySelectorAll(".lib-chars-tbl-rating-star").forEach(function(btn){btn.dataset.voiceId===voiceId&&btn.classList.toggle("is-on",Number(btn.dataset.rating)<=voice.rating)});try{await saveMeta(voiceId,{rating:voice.rating})}catch(err){toast("Could not save voice rating: "+(err.message||err),"error")}})}),prod.querySelectorAll(".lib-chars-tbl-voice-note").forEach(function(input){let timer=null;input.addEventListener("click",function(e){e.stopPropagation()}),input.addEventListener("keydown",function(e){e.stopPropagation()}),input.addEventListener("input",function(){clearTimeout(timer),timer=setTimeout(async function(){const voiceId=input.dataset.voiceId,voice=(window._voices||[]).find(function(v){return v.id===voiceId});if(!(!voice||typeof saveMeta!="function")){voice.note=input.value;try{await saveMeta(voiceId,{note:voice.note})}catch(err){toast("Could not save voice note: "+(err.message||err),"error")}}},600)})}),productions.appendChild(prod)}),container.appendChild(productions),window._libCharsScrollToBook){const target=window._libCharsScrollToBook;window._libCharsScrollToBook=null;const prodEl=[...container.querySelectorAll(".lib-chars-production")].find(function(p){return p.dataset.book===target});prodEl&&(prodEl.scrollIntoView({behavior:"smooth",block:"start"}),prodEl.classList.add("lib-chars-production-highlight"),setTimeout(function(){prodEl.classList.remove("lib-chars-production-highlight")},2200))}else savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop);if(window._libResumeCharacterId){const resumeId=window._libResumeCharacterId;window._libResumeCharacterId=null;const resumeRec=byId.get(resumeId);resumeRec&&setTimeout(function(){_charDetailPage(resumeRec,all,{fromLibrary:!1})},0)}}function _charHue(name){return Math.abs((name||"?").split("").reduce(function(h,c){return(h*31+c.charCodeAt(0))%360},0))}function _charAlignHtml(sh){const score=sh.moral_alignment_score;if(score==null)return"";const pct=Math.max(0,Math.min(100,score)),arc=sh.arc_direction||"neutral",arrowMap={"good-to-bad":{ch:"\u2198",color:"#ff7043",tip:"Arc: Descends toward evil"},"bad-to-good":{ch:"\u2197",color:"#66bb6a",tip:"Arc: Redeems toward good"},complex:{ch:"\u2195",color:"#ab47bc",tip:"Arc: Complex / unpredictable"},"stable-good":{ch:"\u2192",color:"#66bb6a",tip:"Arc: Stable good"},"stable-bad":{ch:"\u2192",color:"#888",tip:"Arc: Stable evil"},neutral:{ch:"\u2192",color:"#aaa",tip:"Arc: Neutral"}},a=arrowMap[arc]||arrowMap.neutral;return'
\u25CF
\u25CF'+a.ch+"
"}function _libStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _charAgeLabel(sh){return _libStr((sh==null?void 0:sh.age_estimate)||"").trim()}function _charAgeSortVal(sh){const m=_charAgeLabel(sh).match(/\d+/);return m?parseInt(m[0],10):9999}function _charLangLabel(rec){const sh=(rec==null?void 0:rec.sheet)||{},voiceLang=rec!=null&&rec.voice&&typeof rec.voice=="object"&&rec.voice.language||"";return _libStr(sh.languages||voiceLang).trim()}function _charGenderLabel(sh){const gender=String((sh==null?void 0:sh.gender)||"").trim();return gender?gender.charAt(0).toUpperCase()+gender.slice(1):""}function _charRelsHtml(rec,allChars){var _a2;if(!allChars||allChars.length<2)return"";const relText=_libStr((_a2=rec.sheet)==null?void 0:_a2.relationships).toLowerCase();if(!relText)return"";const hits=allChars.filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,5);return hits.length?'
'+hits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":""}let _avatarHoverEl=null;function _showAvatarHoverPreview(rec,anchorEl){if(!rec.image)return;_avatarHoverEl||(_avatarHoverEl=document.createElement("div"),_avatarHoverEl.className="lib-avatar-hover-preview",_avatarHoverEl.innerHTML="",document.body.appendChild(_avatarHoverEl)),_avatarHoverEl.querySelector("img").src=rec.image;const rect=anchorEl.getBoundingClientRect(),size=512;let left=rect.right+12;left+size>window.innerWidth&&(left=rect.left-size-12);let top=rect.top+rect.height/2-size/2;top=Math.max(8,Math.min(top,window.innerHeight-size-8)),_avatarHoverEl.style.left=Math.max(8,left)+"px",_avatarHoverEl.style.top=top+"px",_avatarHoverEl.hidden=!1}function _hideAvatarHoverPreview(){_avatarHoverEl&&(_avatarHoverEl.hidden=!0)}function _wireCharCards(root,recsById,allRecs,onChange,detailOpts){const refresh=onChange||libraryRenderCharacters;root.querySelectorAll(".lib-char-card").forEach(function(card){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;const charId=card.dataset.charId,rec=recsById.get?recsById.get(charId):recsById[charId];if(rec){if(card.addEventListener("click",function(e){e.target.closest("button, .lib-char-avatar, .lib-voice-picker-popup")||_charDetailPage(rec,allRecs,detailOpts)}),(_a2=card.querySelector(".lib-char-avatar"))==null||_a2.addEventListener("click",function(e){e.stopPropagation(),_openAvatarLightbox(rec,refresh)}),rec.image){const avatarEl=card.querySelector(".lib-char-avatar");avatarEl==null||avatarEl.addEventListener("mouseenter",function(){_showAvatarHoverPreview(rec,avatarEl)}),avatarEl==null||avatarEl.addEventListener("mouseleave",_hideAvatarHoverPreview)}(_b2=card.querySelector(".lib-char-voice-pill"))==null||_b2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_c2=card.querySelector(".lib-char-pick-voice"))==null||_c2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_d2=card.querySelector(".lib-char-auto-voice"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),refresh()}),(_e2=card.querySelector(".lib-char-redesign-voice"))==null||_e2.addEventListener("click",async function(e){e.stopPropagation();const btn=e.currentTarget;btn.disabled=!0;try{await _charAutoDesignVoice(rec,!0),_schedulePendingTtsRestart(),refresh()}catch(err){toast("Voice design failed: "+(err.message||err),"error")}finally{btn.disabled=!1}}),(_f2=card.querySelector(".lib-char-remove-voice"))==null||_f2.addEventListener("click",async function(e){e.stopPropagation(),await clPut(Object.assign({},rec,{voice:"",updated:new Date})),rec.voice="",_syncVoicePictureFromChar(rec),toast("Voice removed from "+rec.name,"success"),refresh()}),(_g2=card.querySelector(".lib-char-export"))==null||_g2.addEventListener("click",function(e){e.stopPropagation(),typeof stExportRecord=="function"&&stExportRecord(rec)}),(_h2=card.querySelector(".lib-char-online-voice"))==null||_h2.addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),(_i2=card.querySelector(".lib-char-gen-voice"))==null||_i2.addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),(_j2=card.querySelector(".lib-char-voice-play"))==null||_j2.addEventListener("click",function(e){e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)})}})}let _libVoicePreviewEl=null,_libVoicePreviewBtn=null;function _libStopVoicePreview(){if(_libVoicePreviewEl&&(_libVoicePreviewEl.pause(),_libVoicePreviewEl.src=""),_libVoicePreviewBtn){_libVoicePreviewBtn.classList.remove("playing","loading");const icon=_libVoicePreviewBtn.querySelector(".mdi");icon&&(icon.className="mdi mdi-play")}_libVoicePreviewBtn=null}async function _libPreviewCharVoice(rec,btn){const voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"";if(!voiceId){toast("No voice assigned yet","error");return}if(_libVoicePreviewBtn===btn&&_libVoicePreviewEl&&!_libVoicePreviewEl.paused){_libStopVoicePreview();return}_libStopVoicePreview();const icon=btn.querySelector(".mdi"),v=(window._voices||[]).find(x=>x.id===voiceId);if(!v||!v.path||typeof voiceFileUrl!="function"){toast("Voice file not found","error");return}btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{_libVoicePreviewEl||(_libVoicePreviewEl=new Audio,_libVoicePreviewEl.addEventListener("ended",_libStopVoicePreview)),_libVoicePreviewEl.src=voiceFileUrl(v),await _libVoicePreviewEl.play(),btn.classList.remove("loading"),_libVoicePreviewBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _libGeneratedVoiceFileName(rec){const voiceId=rec!=null&&rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"";if(!voiceId)return"";const voice=(window._voices||[]).find(v=>v&&v.id===voiceId);return voice!=null&&voice.path&&String(voice.path).split(/[\\/]/).pop().trim()||""}function _voiceExists(voiceId){if(!voiceId)return!0;const voices=window._voices||[];return voices.length?voices.some(function(v){return v.id===voiceId}):!0}function _charCardHtml(rec,allChars){const sh=rec.sheet||{},hue=_charHue(rec.name),hue2=(hue+40)%360,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",snippet=_libStr(sh.mannerisms||sh.voice_pattern||sh.motivation||sh.backstory||"").slice(0,190),roleLine=_libStr(sh.profession||sh.archetype).trim(),ageLabel=_charAgeLabel(sh),langLabel=_charLangLabel(rec),genderLabel=_charGenderLabel(sh),bookLabel=_libStr(rec.book||""),lineLabel=sh.line_count!=null?String(sh.line_count)+" Zeilen":"",tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),tagsHtml=tagList.length?'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+"
":"",hasPhoto=!!rec.image,bannerStyle=hasPhoto?'style="background-image:linear-gradient(180deg, rgba(0,0,0,.05) 0%, rgba(0,0,0,.72) 100%), url("'+rec.image+'"); background-size:cover; background-position:center;"':'style="--ch1:hsl('+hue+",52%,35%);--ch2:hsl("+hue2+',56%,26%)"',avatarInner=hasPhoto?'':escHtml((rec.name||"?")[0].toUpperCase()),stat=function(label,value,icon){return value?'
'+escHtml(label)+''+escHtml(value)+"
":""},metaChips=[];return bookLabel&&metaChips.push(' '+escHtml(bookLabel)+""),'
'+avatarInner+'
'+(voiceId?'':"")+'
'+escHtml(rec.name)+''+tierBadge+"
"+(roleLine?'
'+escHtml(roleLine)+"
":"")+(_libStr(sh.title)?'
Titel: '+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
aka '+escHtml(_libStr(sh.aliases))+"
":"")+'
'+metaChips.join("")+'
'+stat("Occupation",_libStr(sh.profession),"mdi-briefcase-outline")+stat("Archetype",_libStr(sh.archetype),"mdi-shape-outline")+stat("Gender",genderLabel,"mdi-gender-male-female")+stat("Age",ageLabel,"mdi-cake-variant")+stat("Lines",lineLabel,"mdi-format-list-numbered")+"
"+_charAlignHtml(sh)+"
"}function _charsTableHtml(chars,sortMode,sortDir){const arrow=function(key){return sortMode===key?' ':""},th=function(key,label,title){return'"+label+arrow(key)+""},rows=chars.map(function(rec){const sh=rec.sheet||{},hue=_charHue(rec.name),voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMeta=voiceId?(window._voices||[]).find(function(v){return v.id===voiceId}):null,voiceMissing=!!voiceId&&!_voiceExists(voiceId),voiceRating=Math.max(0,Math.min(5,Number((voiceMeta==null?void 0:voiceMeta.rating)||0))),rawVoiceNote=_libStr(voiceMeta==null?void 0:voiceMeta.note),voiceNote=/^voice design\s*:/i.test(rawVoiceNote)?"":rawVoiceNote,voicePrompt=_libStr((voiceMeta==null?void 0:voiceMeta.voice_design_prompt)||sh.voice_design_prompt),stars=voiceId?'
'+[1,2,3,4,5].map(function(n){return''}).join("")+"
":'\u2014',voiceLang=_charLangLabel(rec),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":gender?"mdi-gender-non-binary":"",genderLabel=_charGenderLabel(sh),score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),avatarInner=rec.image?''+escHtml(rec.name)+'':escHtml((rec.name||"?")[0].toUpperCase()),occupation=_libStr(sh.profession),ageLabel=_charAgeLabel(sh),bookLabel=_libStr(rec.book||""),aliasLabel=_libStr(sh.aliases),archetype=_libStr(sh.archetype),titleLabel=_libStr(sh.title),tableMeta=[aliasLabel?"aka "+aliasLabel:"",occupation?"Occupation: "+occupation:"",titleLabel?"Title: "+titleLabel:"",archetype?"Archetype: "+archetype:""].filter(Boolean).join(" \xB7 "),profile=_charProfileCompleteness(sh),profileClass=profile.filled<=8?"is-sparse":profile.filled<=20?"is-partial":"is-strong";return'
'+avatarInner+'
'+tierBadge+escHtml(rec.name)+"
"+(tableMeta?'
'+escHtml(tableMeta)+"
":"")+(bookLabel?'
'+escHtml(bookLabel)+"
":"")+""+(genderLabel?escHtml(genderLabel):'\u2014')+""+(ageLabel?escHtml(ageLabel):'\u2014')+""+(sh.line_count!=null?sh.line_count:'\u2014')+""+(voiceLang?escHtml(voiceLang):'\u2014')+""+(pct!=null?'
':'\u2014')+''+profile.filled+" / "+profile.total+'
'+(voiceId?'"+(voiceMissing?' ':"")+escHtml(voiceId)+"":'Keine Stimme')+'
'+(voiceId?'':"")+''+(voiceId?'':"")+'
'+stars+''+(voicePrompt?escHtml(voicePrompt):'\u2014')+''+(voiceId?'":'\u2014')+'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+'
'+(bookLabel?escHtml(bookLabel):'\u2014')+''}).join("");return'
'+_CHAR_TABLE_COLUMN_DEFS.map(function(def){return'"}).join("")+"
"+'
'+th("alpha","Name")+th("gender","Geschlecht")+th("age","Alter","Estimated age")+th("lines","Zeilen","Anzahl Zeilen")+th("language","Sprache")+th("align","Gut/B\xF6se","Moralische Gesinnung")+th("profile","Profil (gef\xFCllt)","Sort by filled profile fields; the second number is always 32")+th("voice","Stimme")+th("rating","Rating","Rate the assigned voice")+''+rows+"
Voice Design PromptNotesTagsBook / Script
"}function _lcdSourcesHtml(sources){const list=Array.isArray(sources)?sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[];return list.length?'
'+list.map(function(s){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":""}function _lcdField(label,value,multiline){const v=_libStr(value);return v?'
'+label+'
'+escHtml(v)+"
":""}function _lcdSection(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdSectionFull(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdPromptBox(label,value,sheetKey){const has=!!(value&&String(value).trim()),isVoicePrompt=sheetKey==="voice_design_prompt",saveAction='';return'
"+escHtml(label)+(isVoicePrompt?' Edit the description to improve the voice':"")+(has||isVoicePrompt?"":' \u2014 not generated yet')+'
'+escHtml(value||"")+'
'+saveAction+'
"}function _lcdFieldEdit(label,value,sheetKey,sourceIdxs){const v=_libStr(value),links=(sourceIdxs||[]).map(function(idx){return''+(sourceIdxs.indexOf(idx)+1)+""}).join("");return'
'+(label||links?'
'+escHtml(label)+(links?' '+links+"":"")+"
":"")+'
'+escHtml(v)+"
"}function _jumpToReaderPage(pageNum){typeof navTo=="function"&&navTo("s-reader"),setTimeout(function(){var _a2,_b2;const pages=(_a2=window.readerState)==null?void 0:_a2.pages;if(pages&&pages.length>=pageNum){const pg=pages[pageNum-1];if(pg!=null&&pg.pageDiv){pg.pageDiv.scrollIntoView({behavior:"smooth",block:"start"});return}}const sentences=(_b2=window.readerState)==null?void 0:_b2.sentences;if(sentences&&sentences.length){const target0=pageNum-1,idx=sentences.findIndex(function(s){return(s.words||[]).some(function(w){var _a3,_b3;return((_b3=(_a3=w.page)!=null?_a3:w.para)!=null?_b3:0)>=target0})});if(idx>=0&&typeof readerJumpTo=="function"){readerJumpTo(idx);return}}toast('\xD6ffne das Buch in \u201EVorlesen" und klicke nochmal auf die Quelle',"info")},300)}function _charLineCount(c){if(window.rehState&&rehState.lines&&rehState.lines.length){const key=String(c.name||"").toUpperCase().trim(),live=rehState.lines.filter(function(l){return l.type==="dialog"&&String(l.speaker||"").toUpperCase().trim()===key}).length;if(live)return live}return Number(c.sheet&&c.sheet.line_count)||0}async function _charDetailPage(rec,allChars,opts){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l2,_m2,_n2;typeof window.studioRememberCharacter=="function"&&window.studioRememberCharacter(rec.id),opts=opts||{},window._libDetailOpts=opts;const container=opts.container||document.getElementById("lib-chars-list");if(!container)return;const goBack=typeof opts.onBack=="function"?opts.onBack:libraryRenderCharacters;window._libDetailRec=rec;const baseSheet=rec.sheet||{},characterForms=Array.isArray(baseSheet.forms)?baseSheet.forms:[],activeFormId=String(baseSheet.active_form_id||""),activeForm=characterForms.find(function(form){return String((form==null?void 0:form.id)||"")===activeFormId})||null,sh=Object.assign({},baseSheet,activeForm||{}),formSwitcherHtml=characterForms.length?'":"",bookProfile=typeof _getBookProfile=="function"?await _getBookProfile(rec.book||""):{},hue=_charHue(rec.name),hue2=(hue+40)%360,tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":tier==="minor"?"Nebenfigur":"",gender=_libStr(sh.gender),genderIcon=gender.toLowerCase().startsWith("f")?"mdi-gender-female":gender.toLowerCase().startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,arcMap={"good-to-bad":{ch:"\u2198",label:"Entwicklung zum B\xF6sen",color:"#ff7043"},"bad-to-good":{ch:"\u2197",label:"Wandel zum Guten",color:"#66bb6a"},complex:{ch:"\u2195",label:"Komplex / unvorhersehbar",color:"#ab47bc"},"stable-good":{ch:"\u2192",label:"Stabil gut",color:"#66bb6a"},"stable-bad":{ch:"\u2192",label:"Stabil b\xF6se",color:"#888"},neutral:{ch:"\u2192",label:"Neutral / stabil",color:"#aaa"}},arcInfo=arcMap[sh.arc_direction||"neutral"]||arcMap.neutral,identityFingerprint=["age","age_estimate","hair","eyes","physical","build","skin_tone","race_species","gender","marks","scars","nationality_background","clothing","era","profession"].map(function(key){return key+":"+_libStr(sh[key])}).join("|");let identityHash=0;for(let i=0;i>>0).toString(16),avatarImage=sh.image||rec.image,avatarHtml='
'+(avatarImage?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
")+"
",visualPairNeedsReview=!!(avatarImage&&sh.concept_art_image&&(sh.consistent_prompt_version!=="v3"||!sh.consistent_identity_fingerprint||sh.consistent_identity_fingerprint!==identityFingerprintShort||sh.identity_consistent===!1)),conceptArtHtml='
"+(visualPairNeedsReview?'
Dieses Bildpaar stammt aus einer \xE4lteren Prompt-Version und sollte f\xFCr konsistente Identit\xE4t neu erzeugt werden.
':"")+(sh.concept_art_image?'
Concept art \u2014 '+escHtml(rec.name)+'
':'
'+(_libStr(sh.concept_art_prompt).trim()?"Kein Konzeptbild":"Kein Konzeptbild-Prompt \u2014 erst unten bei Generation Prompts erzeugen")+"
")+"
",bookContextHtml='
Book context
'+escHtml([bookProfile.genre,bookProfile.setting,bookProfile.era,bookProfile.language].filter(Boolean).join(" \xB7 ")||"Not set")+'
',alignHtml=pct!=null?'
B\xF6seGut'+pct+'/100
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",promptsHtml='
'+_lcdPromptBox("Voice Design Prompt",sh.voice_design_prompt,"voice_design_prompt")+function(){const fileName=_libGeneratedVoiceFileName(rec);return fileName?'
Voice file: '+escHtml(fileName)+'
':'
'}()+_lcdPromptBox("Character Image Prompt",sh.image_prompt,"image_prompt")+_lcdPromptBox("SillyTavern Character Prompt",sh.silly_tavern_prompt,"silly_tavern_prompt")+_lcdPromptBox("Concept Art Prompt",sh.concept_art_prompt,"concept_art_prompt")+"
",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",sourcesList=Array.isArray(sh.sources)?sh.sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[],sourcesByField={};sourcesList.forEach(function(s,idx){const key=_libStr(s.line_hint||s.hint||"").trim().toLowerCase();key&&(sourcesByField[key]=sourcesByField[key]||[]).push(idx)});const sourcesHtml=sourcesList.length?'
'+sourcesList.map(function(s,idx){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":"",sidebarByName=new Map;(allChars||[]).filter(function(c){return String(c.book||"").trim()===String(rec.book||"").trim()}).forEach(function(c){const key=String(c.name||"").trim().toLocaleLowerCase();if(!key)return;const old=sidebarByName.get(key);(!old||c.id===rec.id||_charLineCount(c)>_charLineCount(old))&&sidebarByName.set(key,c)}),sidebarByName.has(String(rec.name||"").trim().toLocaleLowerCase())||sidebarByName.set(String(rec.name||"").trim().toLocaleLowerCase(),rec);const sidebarHtml=[...sidebarByName.values()].sort(function(a,b){return _charLineCount(b)-_charLineCount(a)}).map(function(c){const h=_charHue(c.name),count=_charLineCount(c),gender2=_libStr(c.sheet&&c.sheet.gender).toLocaleLowerCase();return'
'+escHtml((c.name||"?")[0].toUpperCase())+''+escHtml(c.name)+""+(count?''+count+"":"")+"
"}).join("");container.innerHTML="";const pg=document.createElement("div");pg.className="lib-char-page",pg.style.gridColumn="1 / -1",pg.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+'
'+escHtml(_libStr(sh.aliases))+'
'+escHtml(_libStr(sh.archetype))+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+"
"+formSwitcherHtml+'
'+bookContextHtml+conceptArtHtml+'
'+(voiceId?'':"")+''+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdFieldEdit("Voller Name",sh.full_name,"full_name",sourcesByField.full_name),_lcdFieldEdit("Vorname",sh.first_name,"first_name",sourcesByField.first_name),_lcdFieldEdit("Nachname",sh.last_name,"last_name",sourcesByField.last_name),_lcdFieldEdit("Alter",sh.age_estimate,"age_estimate",sourcesByField.age_estimate),_lcdFieldEdit("Geschlecht",sh.gender,"gender",sourcesByField.gender),_lcdFieldEdit("Rasse / Spezies",sh.race_species,"race_species",sourcesByField.race_species),_lcdFieldEdit("Sprachen",sh.languages,"languages",sourcesByField.languages),_lcdFieldEdit("Titel",sh.title,"title",sourcesByField.title),_lcdFieldEdit("Beruf / Rolle",sh.profession,"profession",sourcesByField.profession),_lcdFieldEdit("Auch bekannt als",sh.aliases,"aliases",sourcesByField.aliases)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdFieldEdit("K\xF6rperlich",sh.physical,"physical",sourcesByField.physical),_lcdFieldEdit("Kleidung & Aussehen",sh.clothing,"clothing",sourcesByField.clothing)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdFieldEdit("Eigenheiten & Verhalten",sh.mannerisms,"mannerisms",sourcesByField.mannerisms),_lcdFieldEdit("Stimme & Sprache",sh.voice_pattern,"voice_pattern",sourcesByField.voice_pattern)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdFieldEdit("Hintergrund & Herkunft",sh.backstory,"backstory",sourcesByField.backstory),_lcdFieldEdit("Nationalit\xE4t / Herkunft",sh.nationality_background,"nationality_background",sourcesByField.nationality_background),_lcdFieldEdit("Soziale Klasse",sh.social_class,"social_class",sourcesByField.social_class),_lcdFieldEdit("Motivation",sh.motivation,"motivation",sourcesByField.motivation),_lcdFieldEdit("\xC4ngste",sh.fears,"fears",sourcesByField.fears)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdFieldEdit("Fertigkeiten",sh.skills,"skills",sourcesByField.skills),_lcdFieldEdit("Besondere F\xE4higkeiten",sh.capabilities,"capabilities",sourcesByField.capabilities),_lcdFieldEdit("St\xE4rkstes Attribut",sh.attribute_high,"attribute_high"),_lcdFieldEdit("Schw\xE4chstes Attribut",sh.attribute_low,"attribute_low")])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdFieldEdit("",sh.relationships,"relationships",sourcesByField.relationships),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdFieldEdit("Konfliktstil",sh.conflict_style,"conflict_style",sourcesByField.conflict_style),_lcdFieldEdit("Siegbedingung",sh.win_condition,"win_condition",sourcesByField.win_condition)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdFieldEdit("Dunkles Geheimnis / fataler Fehler",sh.secret,"secret"),_lcdFieldEdit("Charakterentwicklung",sh.arc_note,"arc_note")])+promptsHtml+"
"+sourcesHtml+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+'
Charaktere \xB7 '+escHtml(rec.book||"")+'
'+sidebarHtml+"
",container.appendChild(pg);const sidebar=pg.querySelector(".lib-cpg-sidebar"),sidebarSearch=pg.querySelector(".lib-cpg-sidebar-search"),sidebarSort=pg.querySelector(".lib-cpg-sidebar-sort");function refreshCharacterSidebar(){const query=String((sidebarSearch==null?void 0:sidebarSearch.value)||"").trim().toLocaleLowerCase(),mode=(sidebarSort==null?void 0:sidebarSort.value)||"lines",items=Array.from((sidebar==null?void 0:sidebar.querySelectorAll(".lib-cpg-sidebar-item"))||[]);items.forEach(function(item){item.hidden=!!query&&!String(item.dataset.charName||"").toLocaleLowerCase().includes(query)}),items.sort(function(a,b){return mode==="name"?String(a.dataset.charName||"").localeCompare(String(b.dataset.charName||""),void 0,{sensitivity:"base"}):mode==="gender"?String(a.dataset.charGender||"").localeCompare(String(b.dataset.charGender||""),void 0,{sensitivity:"base"})||String(a.dataset.charName||"").localeCompare(String(b.dataset.charName||""),void 0,{sensitivity:"base"}):(parseInt(b.dataset.charLines||"0",10)||0)-(parseInt(a.dataset.charLines||"0",10)||0)||String(a.dataset.charName||"").localeCompare(String(b.dataset.charName||""),void 0,{sensitivity:"base"})}),items.forEach(function(item){sidebar.appendChild(item)})}sidebarSearch==null||sidebarSearch.addEventListener("input",refreshCharacterSidebar),sidebarSort==null||sidebarSort.addEventListener("change",refreshCharacterSidebar),(_a2=pg.querySelector(".lcd-form-switcher select"))==null||_a2.addEventListener("change",async function(){baseSheet.active_form_id=this.value,rec.sheet=baseSheet,rec.updated=new Date,typeof clPut=="function"&&await clPut(rec);const all=await clGetAll().catch(()=>allChars),updated=all.find(function(item){return item.id===rec.id})||rec;_charDetailPage(updated,all.filter(function(item){return item.book===rec.book}),opts)}),pg.querySelector(".lib-cpg-back").addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),Promise.resolve(goBack()).catch(function(err){console.error("[character detail] back navigation failed",err),toast("Character list could not be opened","error")})}),(_b2=pg.querySelector(".lcd-edit-book-context"))==null||_b2.addEventListener("click",function(){_editBookProfile(rec.book||"")}),(_c2=pg.querySelector(".lcd-avatar-upload"))==null||_c2.addEventListener("click",function(){_openAvatarLightbox(rec,function(){_charDetailPage(rec,allChars,opts)})}),(_d2=pg.querySelector(".lcd-conceptart-gen"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation();const btn=this;if(!_libStr(sh.concept_art_prompt).trim()){toast("No Concept Art Prompt yet \u2014 generate that first in Generation Prompts below","error");return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='';try{await _charAutoGenerateConceptArt(rec),toast("Concept art generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Concept art generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}}),(_e2=pg.querySelector(".lcd-consistent-images-gen"))==null||_e2.addEventListener("click",async function(e){var _a3;e.stopPropagation();const btn=this;if(!_libStr(sh.image_prompt).trim()||!_libStr(sh.concept_art_prompt).trim()){toast("Generate both the Character Image Prompt and Character Reference Prompt first","error");return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026';try{const style=((_a3=pg.querySelector(".lcd-visual-style"))==null?void 0:_a3.value)||"cinematic";await _charAutoGenerateConsistentImages(rec,style),toast("Matching profile and reference images generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Consistent image generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}}),(_f2=pg.querySelector(".lcd-visual-style"))==null||_f2.addEventListener("change",async function(){rec.sheet||(rec.sheet={}),rec.sheet.visual_style=this.value==="concept"?"concept":"cinematic",rec.updated=new Date,typeof clPut=="function"&&await clPut(rec)}),(_g2=pg.querySelector(".lcd-conceptart-img, .lcd-conceptart-empty"))==null||_g2.addEventListener("click",function(){_openConceptArtLightbox(rec,function(){_charDetailPage(rec,allChars,opts)})}),pg.querySelectorAll(".lib-cpg-sidebar-item").forEach(function(item){item.addEventListener("click",async function(){const target=(allChars||[]).find(function(c){return c.id===item.dataset.charId});target&&_charDetailPage(target,allChars,opts)})}),pg.querySelectorAll(".lcd-source-clickable").forEach(function(item){item.addEventListener("click",function(){const n=parseInt(item.dataset.page,10);isNaN(n)||_jumpToReaderPage(n)})}),(_h2=pg.querySelector(".lcd-pick-voice"))==null||_h2.addEventListener("click",async function(){_openVoicePicker(pg.querySelector(".lcd-voice-top"),rec,async function(){const all=await clGetAll().catch(()=>allChars),up=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(up,all.filter(function(r){return r.book===rec.book}),opts)})}),(_i2=pg.querySelector(".lcd-auto-voice"))==null||_i2.addEventListener("click",async function(){await _autoAssignVoice(rec);const all=await clGetAll().catch(()=>allChars),up=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(up,all.filter(function(r){return r.book===rec.book}),opts)}),(_j2=pg.querySelector(".lcd-online-voice"))==null||_j2.addEventListener("click",function(){_charSearchOnline(rec)}),(_k2=pg.querySelector(".lcd-gen-voice"))==null||_k2.addEventListener("click",function(){_charDesignVoiceInline(rec)}),(_l2=pg.querySelector(".lcd-clone-voice"))==null||_l2.addEventListener("click",function(){_charCloneVoice(rec)}),(_m2=pg.querySelector(".lcd-preview-voice"))==null||_m2.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)}),(_n2=pg.querySelector(".lcd-generated-voice-play"))==null||_n2.addEventListener("click",function(e){e.preventDefault(),e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)}),pg.querySelectorAll(".lcd-prompt-copy").forEach(function(btn){btn.addEventListener("click",async function(){var _a3;const box=btn.closest(".lcd-prompt-body"),text=((_a3=box==null?void 0:box.querySelector(".lcd-prompt-text"))==null?void 0:_a3.textContent.trim())||"";if(!text){toast("Nothing to copy yet \u2014 click Generate first","error");return}typeof copyText=="function"&&await copyText(text),toast("Prompt copied","success")})}),pg.querySelectorAll(".lcd-save-prompt").forEach(function(btn){btn.addEventListener("click",async function(){var _a3,_b3,_c3;const text=((_b3=(_a3=btn.closest(".lcd-prompt-body"))==null?void 0:_a3.querySelector(".lcd-prompt-text"))==null?void 0:_b3.textContent.trim())||"";if(!text){toast("Prompt is empty","error");return}const sheetKey=btn.dataset.sheetKey||"voice_design_prompt",original=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Saving\u2026';try{if(rec.sheet||(rec.sheet={}),rec.sheet[sheetKey]=text,rec.sheet[sheetKey+"_user_edited"]=!0,rec.updated=new Date,typeof clPut=="function"&&await clPut(rec),sheetKey==="voice_design_prompt"){const key="vcf-design-presets",presets=JSON.parse(localStorage.getItem(key)||"{}"),base=String(rec.name||"Voice Design").trim()||"Voice Design";let name=base,n=2;for(;presets[name]&&presets[name].description!==text;)name=base+" "+n++;presets[name]={description:text,sample_text:"",language:_charLang(rec)||"Auto",gender:_libStr((_c3=rec.sheet)==null?void 0:_c3.gender)||"N"},localStorage.setItem(key,JSON.stringify(presets)),typeof syncDesignPresetsToServer=="function"&&syncDesignPresetsToServer()}toast("Prompt saved and character updated","success")}catch(err){toast("Could not save prompt: "+(err.message||err),"error")}finally{btn.disabled=!1,btn.innerHTML=original}})}),pg.querySelectorAll(".lcd-regenerate-voice").forEach(function(btn){btn.addEventListener("click",async function(){var _a3,_b3;const prompt=((_a3=pg.querySelector('.lcd-prompt-box[data-sheet-key="voice_design_prompt"] .lcd-prompt-text'))==null?void 0:_a3.textContent.trim())||((_b3=pg.querySelector(".lcd-voice-prompt-box .lcd-prompt-text"))==null?void 0:_b3.textContent.trim())||"";if(!prompt){toast("Voice design prompt is empty","error");return}const generate=async function(){const original=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating Voice\u2026';try{rec.sheet||(rec.sheet={}),rec.sheet.voice_design_prompt=prompt,rec.updated=new Date,typeof clPut=="function"&&await clPut(rec),await _charAutoDesignVoice(rec,!0,prompt),_schedulePendingTtsRestart(),toast("New voice generated for "+rec.name,"success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Voice generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=original}};if(!rec.voice){await generate();return}const confirmRow=pg.querySelector(".lcd-voice-replace-confirm");if(!confirmRow)return;confirmRow.hidden=!1,confirmRow.scrollIntoView({behavior:"smooth",block:"nearest"});const cancel=confirmRow.querySelector(".lcd-voice-replace-cancel"),replace=confirmRow.querySelector(".lcd-voice-replace-ok");cancel.onclick=function(){confirmRow.hidden=!0},replace.onclick=async function(){confirmRow.hidden=!0,replace.disabled=!0;try{await generate()}finally{replace.disabled=!1}}})}),pg.querySelectorAll(".lcd-gen-prompt").forEach(function(btn){btn.addEventListener("click",async function(e){var _a3;e.preventDefault();const key=btn.dataset.sheetKey,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026';try{const sh2=rec.sheet||{},sample=[sh2.physical,sh2.backstory,sh2.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""},r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:rec.name,book:rec.book||"",book_profile:typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},sheet:sh2,visual_style:((_a3=pg.querySelector(".lcd-visual-style"))==null?void 0:_a3.value)||sh2.visual_style||"cinematic",language,llm_url:target.url,model:target.model,fields:[key]})});if(!r.ok)throw new Error((await r.json().catch(function(){return{}})).detail||r.statusText);const d=await r.json();if(!d[key])throw new Error("Empty response \u2014 try again");rec.sheet||(rec.sheet={}),rec.sheet[key]=d[key],rec.sheet[key+"_user_edited"]=!1,rec.updated=new Date,typeof clPut=="function"&&await clPut(rec),toast("Prompt generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Prompt generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}})});const slider=pg.querySelector(".lcd-align-slider"),sliderVal=pg.querySelector(".lcd-align-slider-val"),arcEl=pg.querySelector(".lcd-align-arc");slider&&slider.addEventListener("input",async function(){const val=parseInt(slider.value,10);sliderVal&&(sliderVal.textContent=val+"/100"),arcEl&&(arcEl.textContent=arcInfo.ch+" "+arcInfo.label+(val>=70?" \xB7 Rechtschaffen ("+val+"/100)":val<=30?" \xB7 B\xF6se ("+val+"/100)":" \xB7 Moralisch ambivalent ("+val+"/100)")),arcEl&&(arcEl.style.color=arcInfo.color),rec.sheet.moral_alignment_score=val,rec.updated=new Date,typeof clPut=="function"&&await clPut(rec)});const _saveTimers=new Map;function _schedSave(key,value,isRecKey){clearTimeout(_saveTimers.get(key)),_saveTimers.set(key,setTimeout(async function(){isRecKey?rec[key]=value:(rec.sheet||(rec.sheet={}),rec.sheet[key]=value),rec.updated=new Date,typeof clPut=="function"&&await clPut(rec)},900))}pg.querySelectorAll("[contenteditable][data-sheet-key]").forEach(function(el){el.addEventListener("input",function(){_schedSave(el.dataset.sheetKey,el.textContent.trim(),!1)})}),pg.querySelectorAll("[contenteditable][data-rec-key]").forEach(function(el){el.addEventListener("input",function(){_schedSave(el.dataset.recKey,el.textContent.trim(),!0)})})}window._charDetailPage=_charDetailPage;function _charDetailModal(rec,allChars){const sh=rec.sheet||{},cov=libBookCover(rec.name),hue=_charHue(rec.name),tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":tier==="minor"?"Nebenfigur":"",gender=_libStr(sh.gender),genderIcon=gender.toLowerCase().startsWith("f")?"mdi-gender-female":gender.toLowerCase().startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,arc=sh.arc_direction||"neutral",arcMap={"good-to-bad":{ch:"\u2198",label:"Entwicklung zum B\xF6sen",color:"#ff7043"},"bad-to-good":{ch:"\u2197",label:"Wandel zum Guten",color:"#66bb6a"},complex:{ch:"\u2195",label:"Komplex / unvorhersehbar",color:"#ab47bc"},"stable-good":{ch:"\u2192",label:"Stabil gut",color:"#66bb6a"},"stable-bad":{ch:"\u2192",label:"Stabil b\xF6se",color:"#888"},neutral:{ch:"\u2192",label:"Neutral / stabil",color:"#aaa"}},arcInfo=arcMap[arc]||arcMap.neutral,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",avatarHtml=rec.image?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
",alignHtml=pct!=null?'
B\xF6se
Gut
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.arc_note)?'
'+escHtml(_libStr(sh.arc_note))+"
":"")+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",ov=document.createElement("div");ov.className="lib-char-detail-ov",ov.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
auch bekannt als '+escHtml(_libStr(sh.aliases))+"
":"")+(_libStr(sh.archetype)?'
'+escHtml(_libStr(sh.archetype))+"
":"")+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdField("Voller Name",sh.full_name,!0),_lcdField("Vorname",sh.first_name,!0),_lcdField("Nachname",sh.last_name,!0),_lcdField("Alter",sh.age_estimate,!0),_lcdField("Geschlecht",sh.gender,!0),_lcdField("Rasse / Spezies",sh.race_species,!0),_lcdField("Sprachen",sh.languages,!0),_lcdField("Titel",sh.title,!0),_lcdField("Beruf / Rolle",sh.profession,!0),_lcdField("Auch bekannt als",sh.aliases,!0)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdField("K\xF6rperlich",sh.physical,!0),_lcdField("Kleidung & Aussehen",sh.clothing,!0)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdField("Eigenheiten & Verhalten",sh.mannerisms,!0),_lcdField("Stimme & Sprache",sh.voice_pattern,!0)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdField("Hintergrund & Herkunft",sh.backstory,!0),_lcdField("Nationalit\xE4t / Herkunft",sh.nationality_background,!0),_lcdField("Soziale Klasse",sh.social_class,!0),_lcdField("Motivation",sh.motivation,!0),_lcdField("\xC4ngste",sh.fears,!0)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdField("Fertigkeiten",sh.skills,!0),_lcdField("Besondere F\xE4higkeiten",sh.capabilities,!0),_lcdField("St\xE4rkstes Attribut",sh.attribute_high,!1),_lcdField("Schw\xE4chstes Attribut",sh.attribute_low,!1)])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdField("",sh.relationships,!0),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdField("Konfliktstil",sh.conflict_style,!0),_lcdField("Siegbedingung",sh.win_condition,!0)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdField("Dunkles Geheimnis / fataler Fehler",sh.secret,!0),_lcdField("Charakterentwicklung",sh.arc_note,!0)])+"
"+_lcdSourcesHtml(sh.sources)+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+"
",document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector(".lcd-close-btn").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector(".lcd-edit-btn").addEventListener("click",function(){close(),typeof clEdit=="function"&&clEdit(rec.id)}),ov.querySelector(".lcd-pick-voice").addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){close(),libraryRenderCharacters()})}),ov.querySelector(".lcd-auto-voice").addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),close(),libraryRenderCharacters()}),ov.querySelector(".lcd-online-voice").addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),ov.querySelector(".lcd-gen-voice").addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),ov.querySelector(".lcd-avatar").addEventListener("click",function(){const inp=document.createElement("input");inp.type="file",inp.accept="image/*",inp.onchange=async function(){const file=inp.files[0];if(!file)return;const fr=new FileReader;fr.onload=async function(ev){typeof clSetImage=="function"&&await clSetImage(rec.id,ev.target.result),toast("Profile picture saved","success"),close(),libraryRenderCharacters()},fr.readAsDataURL(file)},inp.click()})}window._charDetailModal=_charDetailModal;function _openAvatarLightbox(rec,onSaved){var _a2;(_a2=document.getElementById("avatar-lightbox"))==null||_a2.remove();const sh=rec.sheet||{},currentPrompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,_getBookProfileSync(rec.book)):""),ov=document.createElement("div");ov.id="avatar-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Profilbild
'+(rec.image?''+escHtml(rec.name)+'':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
")+'
',document.body.appendChild(ov),ov.addEventListener("click",function(e){e.target===ov&&ov.remove()}),ov.querySelector("#alb-close").addEventListener("click",function(){ov.remove()}),ov.querySelector("#alb-upload-btn").addEventListener("click",function(){ov.querySelector("#alb-file-input").click()}),ov.querySelector("#alb-online-focus-btn").addEventListener("click",function(){ov.querySelector("#alb-url-input").focus(),ov.querySelector("#alb-url-input").scrollIntoView({behavior:"smooth",block:"center"})});const setPreview=function(src){ov.querySelector(".alb-preview").innerHTML=''+escHtml(rec.name)+''},setStatus=function(msg,cls){const el=ov.querySelector("#alb-status");el.textContent=msg||"",el.className="llm-active-status"+(cls?" "+cls:"")},commitImage=async function(dataUri){typeof clSetImage=="function"&&await clSetImage(rec.id,dataUri),rec.image=dataUri,setPreview(dataUri),document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),toast("Profilbild gespeichert","success"),_syncVoicePictureFromChar(rec),typeof onSaved=="function"&&onSaved()};ov.querySelector("#alb-file-input").addEventListener("change",function(){const file=this.files[0];if(!file)return;const fr=new FileReader;fr.onload=function(ev){commitImage(ev.target.result)},fr.readAsDataURL(file)}),ov.querySelector("#alb-url-btn").addEventListener("click",async function(){const url=ov.querySelector("#alb-url-input").value.trim();if(!url){toast("Bild-URL eingeben","error");return}const btn=this;btn.disabled=!0,setStatus("Wird heruntergeladen\u2026");try{const r=await fetch("/api/character-image-from-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),setStatus("\u2713 Heruntergeladen","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Download fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1}}),ov.querySelector("#alb-gen-btn").addEventListener("click",async function(){const prompt=ov.querySelector("#alb-prompt").value.trim();if(!prompt){toast("Prompt eingeben","error");return}const provider=ov.querySelector("#alb-provider").value,btn=this,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generiere\u2026',setStatus("Generiere\u2026 (kann bei lokalen Modellen etwas dauern)");try{const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),rec.sheet||(rec.sheet={}),rec.sheet.image_prompt!==prompt&&(rec.sheet.image_prompt=prompt,typeof clUpsert=="function"&&await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name}),rec.id)),setStatus("\u2713 Generiert","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Generierung fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}window._openAvatarLightbox=_openAvatarLightbox;async function _syncVoicePictureFromChar(rec){if(!rec||!rec.image||!rec.voice)return;const voiceId=typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice||"");if(voiceId)try{const existing=(window._voices||[]).find(function(v){return v.id===voiceId});if(existing&&existing.has_picture)return;const blob=await(await fetch(rec.image)).blob(),fd=new FormData;fd.append("voice_id",voiceId),fd.append("file",blob,"character.jpg"),await fetch("/api/voice/picture",{method:"POST",body:fd})}catch(e){console.warn("[voice picture sync]",e)}}window._syncVoicePictureFromChar=_syncVoicePictureFromChar;function _openVoicePicker(cardEl,rec,onDone){var _a2;document.querySelectorAll(".lib-voice-picker-popup").forEach(function(p){p.remove()});let voices=window._voices||[];const gender=String(((_a2=rec.sheet)==null?void 0:_a2.gender)||"").toLowerCase(),genderMatch=gender.startsWith("f")?"f":gender.startsWith("m")?"m":"",popup=document.createElement("div");popup.className="lib-voice-picker-popup",popup.innerHTML='
',popup.querySelector(".lib-vp-design-btn").addEventListener("click",function(){popup.remove(),typeof _charDesignVoiceInline=="function"&&_charDesignVoiceInline(rec)});function renderList(filter){let list=voices.filter(function(v){return v.enabled!==!1});if(filter){const f=filter.toLowerCase();list=list.filter(function(v){return(v.id||"").toLowerCase().includes(f)||(v.name||"").toLowerCase().includes(f)})}else genderMatch&&(list=list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg.startsWith(genderMatch)||!vg}).concat(list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg&&!vg.startsWith(genderMatch)})));const ul=popup.querySelector(".lib-vp-list");ul.innerHTML=list.slice(0,500).map(function(v){return'
'+escHtml(v.id||v.name||"")+(v.gender?' \xB7 '+escHtml(v.gender)+"":"")+"
"}).join("")+(list.length===0?'
No voices found
':""),ul.querySelectorAll(".lib-vp-item").forEach(function(item){item.addEventListener("click",async function(){const vid=item.dataset.vid;await clPut(Object.assign({},rec,{voice:vid,updated:new Date})),rec.voice=vid,_syncVoicePictureFromChar(rec),popup.remove(),onDone()})})}renderList(""),popup.querySelector(".lib-vp-input").addEventListener("input",function(e){renderList(e.target.value)}),voices.length===0&&typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().then(function(){popup.isConnected&&(voices=window._voices||[],renderList(popup.querySelector(".lib-vp-input").value||""))}).catch(function(){}),document.body.appendChild(popup);const rect=cardEl.getBoundingClientRect(),popupWidth=260;popup.style.position="fixed",popup.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-popupWidth-8))+"px",popup.style.width=popupWidth+"px";const spaceBelow=window.innerHeight-rect.bottom;spaceBelow>300||spaceBelow>rect.top?popup.style.top=rect.bottom+4+"px":popup.style.bottom=window.innerHeight-rect.top+4+"px",setTimeout(function(){function close(e){popup.contains(e.target)||(popup.remove(),document.removeEventListener("click",close))}document.addEventListener("click",close)},0),popup.querySelector(".lib-vp-input").focus()}async function _findVoiceFromSameCharacterElsewhere(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey)return null;let all=[];try{all=await clGetAll()}catch{return null}const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null,match=all.find(function(r){if(r.id===rec.id||!r.voice||String(r.name||"").trim().toLowerCase()!==nameKey)return!1;const vId=typeof r.voice=="object"?r.voice.id:r.voice;return!(!_voiceExists(vId)||bookCode&&_voiceLangCode(vId)!==bookCode)});return match?{voiceId:typeof match.voice=="object"?match.voice.id:match.voice,book:match.book}:null}function _voiceLangCode(voiceId){const m=/^([A-Za-z]{2,3})_/.exec(String(voiceId||""));return m?m[1].toUpperCase():null}async function _findVoiceByCharacterName(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey||nameKey.length<3)return null;const hits=(window._voices||[]).filter(function(v){return v.enabled!==!1}).filter(function(v){return String(v.id||v.name||"").toLowerCase().includes(nameKey)});if(!hits.length)return null;const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null;if(bookCode){const langHits=hits.filter(function(v){return _voiceLangCode(v.id)===bookCode});return langHits.length?langHits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]:null}return hits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]}async function _autoAssignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}const named=await _findVoiceByCharacterName(rec);if(named){await clPut(Object.assign({},rec,{voice:named.id,updated:new Date})),rec.voice=named.id,_syncVoicePictureFromChar(rec),toast(named.id+" \u2192 "+rec.name+" (matching voice already in the library)","success");return}if(typeof _charAutoDesignVoice=="function"){await _charAutoDesignVoice(rec);return}toast("No matching voice found","error")}function _charLang(rec){const sh=rec.sheet||{},text=[sh.backstory,sh.voice_pattern,sh.mannerisms,sh.relationships,sh.motivation,sh.archetype].filter(Boolean).join(" ");return typeof detectLang=="function"?detectLang(text):""}const _bookProfileCache=new Map;async function _getBookProfile(book){const key=String(book||"").trim();if(!key)return{};if(_bookProfileCache.has(key))return _bookProfileCache.get(key);let profile={};try{const r=await fetch("/api/book-profile?book="+encodeURIComponent(key));r.ok&&(profile=await r.json())}catch(e){console.warn("[book profile]",e)}return _bookProfileCache.set(key,profile),profile}function _getBookProfileSync(book){return _bookProfileCache.get(String(book||"").trim())||{}}async function _saveBookProfile(book,profile){const key=String(book||"").trim(),r=await fetch("/api/book-profile",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(Object.assign({book:key,confirmed:!0},profile))});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json();return _bookProfileCache.set(key,d.profile||profile),d.profile}const _bookLangCache=new Map;async function _resolveBookLang(rec){const book=rec.book||"",profile=await _getBookProfile(book);if(profile&&profile.language)return profile.language;const direct=_charLang(rec);if(direct)return direct;if(_bookLangCache.has(book))return _bookLangCache.get(book);let lang="";try{const siblings=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(book):[],counts={};siblings.forEach(function(s){const l=_charLang(s);l&&(counts[l]=(counts[l]||0)+1)});let best="",bestN=0;Object.keys(counts).forEach(function(l){counts[l]>bestN&&(best=l,bestN=counts[l])}),lang=best}catch(e){console.warn("[book lang]",e)}return _bookLangCache.set(book,lang),lang}const _VOICE_TEXTURE_POOL=["a warm, breathy timbre","a bright, clear timbre","a low, husky timbre","a crisp, silvery timbre","a soft, velvety timbre","a slightly nasal, reedy timbre","a rich, resonant timbre","a light, airy timbre"],_VOICE_PACE_POOL=["an unhurried, deliberate pace","a quick, energetic pace","a measured, even pace","a pace that quickens when excited or nervous"];function _hashPick(str,pool){let h=0;for(let i=0;i>>0;return pool[h%pool.length]}function _voiceAudibleSpeciesCue(value){const v=String(value||"").toLowerCase();return/\b(ork|orc|orcs)\b/.test(v)?"Orc: chest-led, gravel-edged resonance; clipped consonants, forceful breath, fully intelligible.":/\b(vampir|vampire)\b/.test(v)?"Vampire: cool velvety timbre, precise diction, restrained predatory calm.":/\b(elf|elv|elfe)\b/.test(v)?"Elf: clear airy resonance, precise musical diction, adult and grounded.":/\b(d[aä]mon|demon|teufel|devil)\b/.test(v)?"Demon: dark resonant timbre with a subtle otherworldly undertone; fully intelligible.":/\b(zwerg|dwarf)\b/.test(v)?"Dwarf: compact earthy resonance, sturdy cadence, deliberate articulation.":/\b(fee|fae|geist|spirit|untot|undead)\b/.test(v)?"Otherworldly: unusual natural resonance, measured breath, emotionally believable.":""}function _voiceAudibleRoleCue(sheet){const v=[sheet.profession,sheet.title,sheet.archetype,sheet.social_class,sheet.capabilities].filter(Boolean).join(" ").toLowerCase();return/krieger|soldat|ritter|wache|kämpfer|warrior|soldier|knight|guard|general|anführer|commander/.test(v)?"Warrior/commander: grounded projection, disciplined breath, concise command cadence.":/schmied|blacksmith|handwerk|artisan|craft/.test(v)?"Craftsperson: practical work-worn texture, measured pace, plain-spoken certainty.":/magier|zauber|hexe|hexer|wizard|mage|sorcer|druid/.test(v)?"Magic-user: measured phrasing, precise diction, contained intensity.":/priester|inquis|mönch|monk|priest|cleric|prophet/.test(v)?"Religious authority: formal diction, calm conviction, weighted pauses.":/könig|königin|baron|graf|adel|noble|lord|lady|herrscher|herrscherin/.test(v)?"High-status authority: polished diction, composed pace, effortless command.":/dieb|spion|assassin|schurke|rogue|thief/.test(v)?"Covert operator: quiet confidence, economical phrasing, alert timing.":/gelehrt|scholar|lehrer|teacher|arzt|heiler|healer/.test(v)?"Learned professional: articulate, patient pace, observant and assured.":""}function _voiceIdentityPrompt(rec,sourcePrompt,langName){const sh=rec.sheet||{},rawGender=String(sh.gender||_genderFromGenericName(rec.name)||"").toLowerCase(),gender=rawGender.startsWith("f")?"female":rawGender.startsWith("m")?"male":rawGender.startsWith("non")?"androgynous/nonbinary":"",hardGender=gender?"Adult "+gender+" voice, clearly "+(gender==="female"?"feminine and never masculine-coded":gender==="male"?"masculine and never feminine-coded":"androgynous")+".":"",age=String(sh.age_estimate||"").trim(),ageCue=age?"Age: "+age.slice(0,36)+".":"",lang=String(langName||"").trim(),languageCue=lang?lang.toLowerCase()==="english"?"Neutral international English.":"Native "+lang+" delivery, never American-accented.":"",species=_voiceAudibleSpeciesCue([sh.race_species,sh.archetype,sh.profession,sh.aliases].filter(Boolean).join(" ")),role=_voiceAudibleRoleCue(sh),prefix=[hardGender,ageCue,languageCue,species,role,"Distinct natural audiobook voice; no imitation."].filter(Boolean).join(" "),base=String(sourcePrompt||"").replace(/\s+/g," ").trim(),room=Math.max(0,480-prefix.length-(prefix&&base?1:0));return(prefix+(base?" "+base.slice(0,room):"")).slice(0,480)}function _buildVoicePrompt(rec,profile,langName){const sh=rec.sheet||{},g=String(sh.gender||"").toLowerCase(),genderWord=g.startsWith("f")?"female":g.startsWith("m")?"male":"",bits=[],lang=String(langName||"").trim();lang&&lang.toLowerCase()!=="english"?bits.push("Speak with an authentic native "+lang+" accent \u2014 not American-accented, not an English speaker doing "+lang+"."):lang&&bits.push("English with a neutral British or international accent, explicitly not American/US-accented.");const settingBits=[profile&&profile.genre,profile&&profile.setting,profile&&profile.era].filter(Boolean);return settingBits.length&&bits.push("Setting: "+settingBits.join(", ")+"."),bits.push("A "+(genderWord?genderWord+" ":"")+"voice"+(sh.archetype?" for "+sh.archetype.toLowerCase():"")+","),bits.push("with "+_hashPick(rec.name||rec.id||"",_VOICE_TEXTURE_POOL)+" and "+_hashPick((rec.name||rec.id||"")+"_pace",_VOICE_PACE_POOL)+"."),sh.voice_pattern&&bits.push(sh.voice_pattern),sh.mannerisms&&bits.push("Mannerisms: "+sh.mannerisms),sh.physical&&bits.push(sh.physical),sh.alignment&&bits.push("Disposition: "+sh.alignment),bits.join(" ").slice(0,600)}function _selectLoose(sel,val){if(!sel||!val)return;const v=String(val).toLowerCase(),opt=[...sel.options].find(function(o){const ov=o.value.toLowerCase(),ot=o.textContent.toLowerCase();return ov===v||ot===v||ov.startsWith(v)||ot.startsWith(v)||v.startsWith(ov)});opt&&(sel.value=opt.value,sel.dispatchEvent(new Event("change")))}function _charSearchOnline(rec){typeof navTo=="function"&&navTo("s-studio");const lang=_charLang(rec);setTimeout(function(){const fishTab=document.querySelector('#gvo-tabs .gvo-tab[data-src="fish"]');fishTab&&fishTab.click(),setTimeout(function(){const langSel=document.getElementById("fa-lang");langSel&&_selectLoose(langSel,lang);const search=document.getElementById("fa-search");search&&(search.value=rec.name,search.dispatchEvent(new KeyboardEvent("keydown",{key:"Enter",bubbles:!0})))},120)},120),toast("Searching online voices for "+rec.name+(lang?" ("+lang+")":""),"info")}function _editBookProfile(book,afterSave){_getBookProfile(book).then(function(profile){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Book context \u2014 '+escHtml(book)+`

Used in every voice design (and image) prompt for this book, so a fantasy story doesn't end up with 1920s-general portraits or English voices in a German book just because one character's own sheet was too sparse to tell.

',document.body.appendChild(ov);const close=function(result){ov.remove(),typeof afterSave=="function"&&afterSave(result||null)};ov.querySelector("#bctx-cancel").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#bctx-save").addEventListener("click",async function(){const btn=this;btn.disabled=!0;try{const saved=await _saveBookProfile(book,{genre:ov.querySelector("#bctx-genre").value,setting:ov.querySelector("#bctx-setting").value,era:ov.querySelector("#bctx-era").value,language:ov.querySelector("#bctx-lang").value});toast("Book context saved for "+book,"success"),close(saved)}catch(e){toast("Failed to save: "+(e.message||e),"error"),btn.disabled=!1}})})}window.openBookContextEditor=function(book){return new Promise(function(resolve){_editBookProfile(book,resolve)})};function _confirmVoiceReuse(rec,reuse){return new Promise(function(resolve){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Existing voice found for '+escHtml(rec.name)+'

"'+escHtml(reuse.voiceId)+'" is already used for '+escHtml(rec.name)+' in "'+escHtml(reuse.book)+'". Reuse it for series consistency, or design a brand-new voice just for this book?

',document.body.appendChild(ov);let audioEl=null,audioObjectUrl=null;ov.querySelector("#cvr-play").addEventListener("click",async function(e){const btn=e.currentTarget,icon=btn.querySelector(".mdi");if(audioEl&&!audioEl.paused){audioEl.pause(),icon.className="mdi mdi-play";return}btn.disabled=!0,icon.className="mdi mdi-loading mdi-spin";try{const langHint=typeof _resolveBookLang=="function"?await _resolveBookLang(rec).catch(function(){return""}):"",text=typeof _charSampleTextFor=="function"&&_charSampleTextFor(rec,langHint)||"Hallo, ich bin "+rec.name+".",rv=(window._voices||[]).find(x=>x.id===reuse.voiceId),rBackend=rv&&!rv.has_ref?"voice_design":"voice_clone",blob=await fetchTtsPreviewBlob(reuse.voiceId,text,"wav","",rBackend);audioEl||(audioEl=new Audio,audioEl.addEventListener("ended",function(){icon.className="mdi mdi-play",audioObjectUrl&&(URL.revokeObjectURL(audioObjectUrl),audioObjectUrl=null)})),audioObjectUrl&&URL.revokeObjectURL(audioObjectUrl),audioObjectUrl=URL.createObjectURL(blob),audioEl.src=audioObjectUrl,await audioEl.play(),icon.className="mdi mdi-pause"}catch(err){toast("Could not play sample: "+(err.message||err),"error"),icon.className="mdi mdi-play"}finally{btn.disabled=!1}});const cleanup=function(result){audioEl&&audioEl.pause(),audioObjectUrl&&(URL.revokeObjectURL(audioObjectUrl),audioObjectUrl=null),ov.remove(),resolve(result)};ov.querySelector("#cvr-cancel").addEventListener("click",function(){cleanup("cancel")}),ov.querySelector("#cvr-use").addEventListener("click",function(){cleanup("use")}),ov.querySelector("#cvr-new").addEventListener("click",function(){cleanup("new")}),ov.addEventListener("click",function(e){e.target===ov&&cleanup("cancel")})})}async function _charDesignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){const choice=await _confirmVoiceReuse(rec,reuse);if(choice==="cancel")return;if(choice==="use"){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}}typeof navTo=="function"&&navTo("s-design");const sh=rec.sheet||{},lang=_charLang(rec),savedPrompt=_libStr(sh.voice_design_prompt).trim();setTimeout(function(){_selectLoose(document.getElementById("design-gender"),sh.gender),_selectLoose(document.getElementById("design-language"),lang);const instruct=document.getElementById("design-instruct");instruct&&(instruct.value=_voiceIdentityPrompt(rec,savedPrompt||_buildVoicePrompt(rec,null,lang),lang));const nm=document.getElementById("design-preset-name");nm&&(nm.value=rec.name)},140),toast("Voice design prepared for "+rec.name+(lang?" \xB7 "+lang:""),"info")}function _charDesignVoiceInline(rec,promptOverride){const sh=rec.sheet||{},lang=_charLang(rec),savedPrompt=_libStr(promptOverride||sh.voice_design_prompt).trim(),instruct=savedPrompt||_voiceIdentityPrompt(rec,_buildVoicePrompt(rec,null,lang),lang),ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='
Voice design prompt \u2014 '+escHtml(rec.name)+'

Edit the description, then generate a new voice from it. This replaces '+(rec.voice?"the current voice":"this character\u2019s voice")+'.

',document.body.appendChild(ov);const close=function(){ov.remove()};ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#cdi-cancel").addEventListener("click",close);let promptSaveTimer=null;const saveInlinePrompt=async function(text){if(text=String(text||"").trim(),!!text&&(rec.sheet||(rec.sheet={}),rec.sheet.voice_design_prompt=text,rec.updated=new Date,typeof clPut=="function")){const saved=await clPut(rec);saved&&saved.sheet&&(rec.sheet=saved.sheet)}},promptInput=ov.querySelector("#cdi-instruct");promptInput.addEventListener("input",function(){clearTimeout(promptSaveTimer),promptSaveTimer=setTimeout(function(){saveInlinePrompt(promptInput.value).catch(function(err){console.warn("[character prompt] autosave failed",err)})},600)}),ov.querySelector("#cdi-save").addEventListener("click",async function(e){const btn=e.currentTarget,original=btn.innerHTML;clearTimeout(promptSaveTimer),btn.disabled=!0,btn.innerHTML=' Saving\u2026';try{await saveInlinePrompt(promptInput.value),toast("Voice design prompt saved","success")}catch(err){toast("Prompt save failed: "+(err.message||err),"error")}finally{btn.disabled=!1,btn.innerHTML=original}}),ov.querySelector("#cdi-rewrite").addEventListener("click",async function(e){const btn=e.currentTarget,original=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Rewriting\u2026';try{const target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""},sample=[sh.physical,sh.backstory,sh.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:rec.name,book:rec.book||"",book_profile:typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},sheet:sh,language,llm_url:target.url,model:target.model,fields:["voice_design_prompt"]})});if(!r.ok)throw new Error((await r.json().catch(function(){return{}})).detail||r.statusText);const d=await r.json();if(!d.voice_design_prompt)throw new Error("The rewrite returned an empty prompt");ov.querySelector("#cdi-instruct").value=d.voice_design_prompt,await saveInlinePrompt(d.voice_design_prompt),toast("Voice design prompt rewritten \u2014 review it, then generate the voice","success")}catch(err){toast("Prompt rewrite failed: "+(err.message||err),"error")}finally{btn.disabled=!1,btn.innerHTML=original}}),ov.querySelector("#cdi-generate").addEventListener("click",async function(e){const btn=e.currentTarget,text=ov.querySelector("#cdi-instruct").value.trim();if(!text){toast("Prompt is empty","error");return}clearTimeout(promptSaveTimer),btn.disabled=!0;const icon=btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-loading mdi-spin");try{await saveInlinePrompt(text),await _charAutoDesignVoice(rec,!0,text),_schedulePendingTtsRestart(),rec.sheet||(rec.sheet={}),rec.sheet.voice_design_prompt=text,close(),toast("New voice designed for "+rec.name,"success");const detailPage=document.querySelector(".lib-char-page"),detailOpts=window._libDetailOpts;if(detailPage&&detailOpts&&typeof _charDetailPage=="function"){const all=typeof clGetAll=="function"?await clGetAll().catch(function(){return[]}):[],updated=all.find(function(r){return r.id===rec.id})||rec;_charDetailPage(updated,all.filter(function(r){return r.book===updated.book}),detailOpts)}else typeof libraryRenderCharacters=="function"&&await libraryRenderCharacters();typeof _libRefreshDetailModal=="function"&&_libRefreshDetailModal(rec)}catch(err){toast("Voice design failed: "+(err.message||err),"error"),btn.disabled=!1,icon&&(icon.className="mdi mdi-creation")}})}function _charCloneVoice(rec){typeof navTo=="function"&&navTo("s-clone"),setTimeout(function(){const nm=document.getElementById("clone-your-name");nm&&(nm.value=rec.name)},140),toast("Clone a Voice prepared for "+rec.name+" \u2014 pick a mic take, file, or YouTube URL","info")}const _DESIGN_SAMPLE_FALLBACK={German:"Ich habe lange auf diesen Moment gewartet, und jetzt, da er da ist, wei\xDF ich genau, was zu tun ist.",English:"I have waited a long time for this moment, and now that it is here, I know exactly what to do."};function _charRealLine(rec){const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook,nameLower=String(rec.name||"").trim().toLowerCase(),line=(ab&&ab.segments||[]).find(function(s){return s&&s.type==="dialogue"&&String(s.speaker||"").trim().toLowerCase()===nameLower&&s.text&&s.text.trim().length>=20&&s.text.trim().length<=200});if(line)return line.text.trim();const quotes=(Array.isArray(rec.sheet&&rec.sheet.sources)?rec.sheet.sources:[]).map(function(s){return s&&s.quote?String(s.quote).trim():""}).filter(function(q){return q.length>=20&&q.length<=240}),spoken=quotes.find(function(q){return/[""„"]/.test(q)});return spoken||(quotes.length?quotes[0]:null)}function _charSampleTextFor(rec,langNameHint){const lang=_charLang(rec)||langNameHint||"English",greeting=lang==="German"?`Hallo, ich bin ${rec.name}.`:`Hello, I am ${rec.name}.`,line=_charRealLine(rec);return line?`${greeting} ${line}`:_DESIGN_SAMPLE_FALLBACK[lang]||_DESIGN_SAMPLE_FALLBACK.English}const _GENERIC_NAME_GENDER={frau:"female",dame:"female",junge_frau:"female",m\u00E4dchen:"female",maedchen:"female",mann:"male",herr:"male",junge:"male",knabe:"male"};function _genderFromGenericName(name){const key=String(name||"").trim().toLowerCase().replace(/\s+/g,"_");return _GENERIC_NAME_GENDER[key]||""}let _voiceRestartPending=!1;async function _flushPendingTtsRestart(){if(_voiceRestartPending){_voiceRestartPending=!1;try{const r=await fetch("/api/tts/restart",{method:"POST"});r.ok?toast("TTS backend restarted to pick up the newly designed voice(s)","success"):console.warn("[tts restart] failed:",r.status)}catch(e){console.warn("[tts restart]",e)}}}let _voiceRestartDebounceTimer=null;function _schedulePendingTtsRestart(){clearTimeout(_voiceRestartDebounceTimer),_voiceRestartDebounceTimer=setTimeout(_flushPendingTtsRestart,4e3)}async function _fetchRetryingNetworkErrors(url,opts,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch(url,opts)}catch(e){if(i===tries)throw e;await new Promise(function(r){setTimeout(r,2500*i)})}}function _designBenchmarkWpmBad(b){if(!b||!b.ok||!b.audio_sec||!b.text)return!1;const wpm=String(b.text).trim().split(/\s+/).length/(b.audio_sec/60);return wpm<80||wpm>400}async function _charAutoDesignVoice(rec,force,instructOverride){const reuse=force||instructOverride?null:await _findVoiceFromSameCharacterElsewhere(rec);if(reuse&&reuse.voiceId!==rec.voice){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec);return}const sh=rec.sheet||{},langName=await _resolveBookLang(rec)||"English",instruct=instructOverride?_libStr(instructOverride).trim():_voiceIdentityPrompt(rec,_libStr(sh.voice_design_prompt).trim()||_buildVoicePrompt(rec,await _getBookProfile(rec.book),langName),langName);if(!instruct.trim())throw new Error("No character description to design a voice from yet");instructOverride&&(sh.voice_design_prompt=instruct);const langCode=typeof DESIGN_LANG_CODE!="undefined"&&DESIGN_LANG_CODE[langName]||"EN",genderWord=String(sh.gender||"").toLowerCase()||_genderFromGenericName(rec.name),genderLetter=genderWord.startsWith("f")?"F":genderWord.startsWith("m")?"M":"N",sampleText=_charSampleTextFor(rec,langName),dialogue=typeof isDialogueDesign=="function"?isDialogueDesign(instruct,sampleText,null):!1,baseName=typeof designSafeName=="function"?designSafeName(rec.name):(typeof _umlautSafe=="function"?_umlautSafe(rec.name||"VoiceDesign"):String(rec.name||"VoiceDesign")).replace(/[^A-Za-z0-9]+/g,"_"),voiceId=(langCode+"_"+genderLetter+"_"+baseName).slice(0,96),maxAttempts=3;let saved=null;for(let attempt=1;attempt<=maxAttempts;attempt++){const r1=await _fetchRetryingNetworkErrors("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({instruct,sample_text:sampleText,language:langName,gender:genderLetter,dialogue})});if(!r1.ok){const e=await r1.json().catch(function(){return{}});throw new Error(e.detail||r1.statusText)}const designed=await r1.json(),tryId=(voiceId+"__try"+attempt).slice(0,96),r2=await _fetchRetryingNetworkErrors("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designed.id,voice_id:tryId,transcript:sampleText})});if(!r2.ok){const e=await r2.json().catch(function(){return{}});throw new Error(e.detail||r2.statusText)}await r2.json();let bad=!1;if(typeof runVoiceBenchmark=="function")try{const d=await runVoiceBenchmark(tryId,{text:sampleText}),hit=(d&&d.voices||[]).find(function(x){return x.voice_id===tryId}),b=hit&&hit.benchmark;!b||!b.ok&&/connection (refused|reset|aborted)|max retries exceeded|newconnectionerror|econnrefused|timed? ?out/i.test(String(b.error||""))?console.warn("[voice design] benchmark unreachable, accepting unverified:",b&&b.error):bad=!!(b.clipped||_designBenchmarkWpmBad(b))}catch(e){console.warn("[voice benchmark]",e)}if(!bad&&typeof _voiceRoundtripCheck=="function")try{const rt=await _voiceRoundtripCheck(tryId,sampleText,"voice_design");rt.score<.5&&(bad=!0,console.warn("[voice design] STT roundtrip mismatch (score "+rt.score.toFixed(2)+'): said "'+rt.transcript+'"'))}catch(e){console.warn("[voice design roundtrip]",e)}if(!bad){const r3=await _fetchRetryingNetworkErrors("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designed.id,voice_id:voiceId,transcript:sampleText})});if(!r3.ok){const e=await r3.json().catch(function(){return{}});throw new Error(e.detail||r3.statusText)}saved=await r3.json(),saved.needs_tts_restart&&(_voiceRestartPending=!0),await fetch("/api/voice/"+encodeURIComponent(tryId),{method:"DELETE"}).catch(function(){});break}if(await fetch("/api/voice/"+encodeURIComponent(tryId),{method:"DELETE"}).catch(function(){}),attempt===maxAttempts)throw new Error('Voice design for "'+voiceId+'" produced broken audio after '+maxAttempts+" attempts \u2014 left the previous voice in place, try again later")}return typeof saveMeta=="function"&&await saveMeta(saved.voice_id,{gender:genderLetter,flag:typeof LANG_FLAG_DEFAULT!="undefined"?LANG_FLAG_DEFAULT[langCode]:void 0,origin:"designed",group:rec.book||void 0,tag:rec.book||void 0,transcript:sampleText,voice_design_prompt:instruct}).catch(function(){}),rec.voice=saved.voice_id,await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name,voice:saved.voice_id}),rec.id),_syncVoicePictureFromChar(rec),saved.voice_id}function _libActiveCharacterForm(rec){const sheet=rec&&rec.sheet&&typeof rec.sheet=="object"?rec.sheet:{},forms=Array.isArray(sheet.forms)?sheet.forms:[],activeId=String(sheet.active_form_id||"").trim();return activeId&&forms.find(function(form){return String((form==null?void 0:form.id)||"")===activeId})||null}async function _libSaveCharacterVisual(rec,fields){const target=(typeof clGet=="function"?await clGet(rec.id).catch(function(){return null}):null)||rec,form=_libActiveCharacterForm(target);return form?(target.sheet=Object.assign({},target.sheet||{}),target.sheet.forms=Array.isArray(target.sheet.forms)?target.sheet.forms.map(function(item){return String((item==null?void 0:item.id)||"")===String(form.id||"")?Object.assign({},item,fields):item}):[]):Object.assign(target,fields),target.updated=new Date,typeof clPut=="function"&&await clPut(target),target!==rec&&(rec.image=target.image,rec.sheet=target.sheet),target}async function _charAutoGenerateImage(rec,provider){const form=_libActiveCharacterForm(rec),sh=form?Object.assign({},rec.sheet||{},form):rec.sheet||{},hasExplicitPrompt=!!_libStr(sh.image_prompt).trim();if(!hasExplicitPrompt&&[sh.archetype,sh.physical,sh.clothing].filter(Boolean).join(" ").trim().length<20)throw new Error("Not enough character detail to generate a meaningful portrait \u2014 skipped instead of using a generic placeholder");const bookProfile=typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},prompt=hasExplicitPrompt?_libStr(sh.image_prompt).trim():typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,bookProfile):"";if(!prompt)throw new Error("No image prompt to work from yet");const body={prompt,image_kind:"portrait"};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json();if(form){await _libSaveCharacterVisual(rec,{image:d.image});const updatedForm=_libActiveCharacterForm(rec);updatedForm&&(updatedForm.image=d.image)}else typeof clSetImage=="function"&&await clSetImage(rec.id,d.image),rec.image=d.image;return document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),_syncVoicePictureFromChar(rec),d.image}const _CIJ_STEPS=[{key:"identity",label:"Identity portrait"},{key:"render",label:"Turnaround, scene & props"}];function _cijShowPanel(name){var _a2;(_a2=document.getElementById("cij-progress-panel"))==null||_a2.remove();const panel=document.createElement("div");panel.id="cij-progress-panel",panel.className="cij-progress-panel";const stepsHtml=_CIJ_STEPS.map(function(s){return'
'+escHtml(s.label)+"
"}).join("");panel.innerHTML='
Generating images \u2014 '+escHtml(name||"Character")+'
'+stepsHtml+'
Starting\u2026
Elapsed 0:00
',document.body.appendChild(panel),panel.querySelector(".cij-progress-close").addEventListener("click",function(){panel.remove()});const cancelButton=panel.querySelector(".cij-progress-cancel");return cancelButton.addEventListener("click",async function(){cancelButton.disabled=!0,cancelButton.textContent="Cancelling\u2026";try{if(!(await fetch("/api/character-generate-consistent-images/cancel/"+encodeURIComponent(panel.dataset.jobId),{method:"POST"})).ok)throw new Error("Cancellation request failed")}catch(error){cancelButton.disabled=!1,cancelButton.textContent="Cancel",toast(error.message||"Could not cancel generation","error")}}),{update:function(stage,detail,elapsed,warn){const p=document.getElementById("cij-progress-panel");if(!p)return;p.querySelector(".cij-progress-detail").textContent=detail||"Working\u2026",p.querySelector(".cij-progress-elapsed").textContent="Elapsed "+_cijFmtElapsed(elapsed)+(warn?" \u2014 no update in a while, it may be stuck":""),p.classList.toggle("cij-progress-warn",!!warn);const activeIndex=_CIJ_STEPS.findIndex(function(s){return s.key===stage});p.querySelectorAll(".cij-step").forEach(function(el,i){const icon=el.querySelector(".cij-step-icon");el.classList.remove("is-done","is-active","is-pending"),activeIndex<0||i60;if(panel.update(status2.stage,status2.detail,status2.elapsed,warn),status2.done){if(status2.error)throw panel.done(!1),new Error(status2.error);return panel.done(!0),status2.result}}}async function _charAutoGenerateConsistentImages(rec,visualStyle){const form=_libActiveCharacterForm(rec),sh=form?Object.assign({},rec.sheet||{},form):rec.sheet||{},profilePrompt=_libStr(sh.image_prompt).trim(),referencePrompt=_libStr(sh.concept_art_prompt).trim();if(!profilePrompt)throw new Error("No Character Image Prompt \u2014 generate it first");if(!referencePrompt)throw new Error("No Character Reference Prompt \u2014 generate it first");const r=await fetch("/api/character-generate-consistent-images",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({profile_prompt:profilePrompt,reference_prompt:referencePrompt,name:rec.name||"",book:rec.book||"",sheet:sh,visual_style:visualStyle||"cinematic",book_profile:typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},identity_image:"",reuse_identity:!1})});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const{job_id}=await r.json(),panel=_cijShowPanel(rec.name);panel.setJobId(job_id);const d=await _cijPollJob(job_id,panel);if(!d.profile||!d.reference)throw new Error("ComfyUI returned an incomplete image pair");const visualFields={image:d.profile,concept_art_image:d.reference,consistent_prompt_version:d.prompt_version||"v3",consistent_identity_fingerprint:d.identity_fingerprint||"",profile_asset_digest:d.profile_asset_digest||"",reference_asset_digest:d.reference_asset_digest||"",identity_consistent:d.identity_consistent!==!1};if(form){await _libSaveCharacterVisual(rec,visualFields);const updatedForm=_libActiveCharacterForm(rec);updatedForm&&Object.assign(updatedForm,visualFields)}else typeof clSetImage=="function"&&await clSetImage(rec.id,d.profile),Object.assign(rec,{image:d.profile}),rec.sheet=Object.assign({},rec.sheet||{},{concept_art_image:d.reference,consistent_prompt_version:visualFields.consistent_prompt_version,consistent_identity_fingerprint:visualFields.consistent_identity_fingerprint,identity_consistent:visualFields.identity_consistent});return document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),d.profile}async function _charAutoGenerateConceptArt(rec,provider){const form=_libActiveCharacterForm(rec),sh=form?Object.assign({},rec.sheet||{},form):rec.sheet||{},prompt=_libStr(sh.concept_art_prompt).trim();if(!prompt)throw new Error("No concept art prompt to work from yet \u2014 generate the prompt first");const body={prompt,image_kind:"concept_art"};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(function(){return{}});throw new Error(e.detail||r.statusText)}const d=await r.json();return await _libSaveCharacterVisual(rec,{concept_art_image:d.image}),form?form.concept_art_image=d.image:rec.sheet=Object.assign({},rec.sheet||{},{concept_art_image:d.image}),d.image}function _openConceptArtLightbox(rec,onSaved){var _a2;(_a2=document.getElementById("conceptart-lightbox"))==null||_a2.remove();const activeForm=_libActiveCharacterForm(rec),sh=activeForm?Object.assign({},rec.sheet||{},activeForm):rec.sheet||{},prompt=_libStr(sh.concept_art_prompt).trim(),ov=document.createElement("div");ov.id="conceptart-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Konzeptbild
'+(sh.concept_art_image?'Concept art \u2014 '+escHtml(rec.name)+'':'
No concept art yet
')+'
',document.body.appendChild(ov);const close=function(){ov.remove()},status2=function(msg,cls){const el=ov.querySelector("#calb-status");el.textContent=msg||"",el.className="llm-active-status"+(cls?" "+cls:"")},setPreview=function(src){ov.querySelector(".conceptart-editor-preview").innerHTML='Concept art \u2014 '+escHtml(rec.name)+''},commit=async function(dataUri){await _libSaveCharacterVisual(rec,{concept_art_image:dataUri}),activeForm?activeForm.concept_art_image=dataUri:rec.sheet=Object.assign({},rec.sheet||{},{concept_art_image:dataUri}),setPreview(dataUri),toast("Concept art saved","success"),typeof onSaved=="function"&&onSaved()};ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#calb-close").addEventListener("click",close),ov.querySelector("#calb-upload").addEventListener("click",function(){ov.querySelector("#calb-file").click()}),ov.querySelector("#calb-online").addEventListener("click",function(){ov.querySelector("#calb-url").focus(),ov.querySelector("#calb-url").scrollIntoView({behavior:"smooth",block:"center"})}),ov.querySelector("#calb-file").addEventListener("change",function(){const file=this.files[0];if(!file)return;const fr=new FileReader;fr.onload=function(e){commit(e.target.result).catch(function(err){toast("Upload failed: "+err.message,"error")})},fr.readAsDataURL(file)}),ov.querySelector("#calb-url-btn").addEventListener("click",async function(){const url=ov.querySelector("#calb-url").value.trim();if(!url){toast("Enter an image URL","error");return}const btn=this;btn.disabled=!0,status2("Downloading\u2026");try{const r=await fetch("/api/character-image-from-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commit(d.image),status2("\u2713 Downloaded","ok")}catch(e){status2("Failed","err"),toast("Download failed: "+e.message,"error")}finally{btn.disabled=!1}}),ov.querySelector("#calb-regenerate").addEventListener("click",async function(){const nextPrompt=ov.querySelector("#calb-prompt").value.trim();if(!nextPrompt){toast("Generate or enter a concept-art prompt first","error");return}const btn=this,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generating\u2026',status2("Generating\u2026");try{sh.concept_art_prompt=nextPrompt;const provider=ov.querySelector("#calb-provider").value,img=await _charAutoGenerateConceptArt(rec,provider);await commit(img),status2("\u2713 Generated","ok")}catch(e){status2("Failed","err"),toast("Generation failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}window._charSearchOnline=_charSearchOnline,window._charDesignVoice=_charDesignVoice,window._charAutoDesignVoice=_charAutoDesignVoice,window._charAutoGenerateImage=_charAutoGenerateImage,window._charAutoGenerateConceptArt=_charAutoGenerateConceptArt,window._openConceptArtLightbox=_openConceptArtLightbox,window.libraryRenderCharacters=libraryRenderCharacters;const STUDIO_RESUME_KEY="ttsvc_studio_resume";let _stuActive=1;function _stuReadResume(){try{return JSON.parse(localStorage.getItem(STUDIO_RESUME_KEY)||"{}")||{}}catch{return{}}}function _stuSaveResume(phase){var _a2,_b2,_c2;try{const previous=_stuReadResume();localStorage.setItem(STUDIO_RESUME_KEY,JSON.stringify({phase,bookId:((_a2=window._audiobook)==null?void 0:_a2.bookId)||((_b2=window.readerState)==null?void 0:_b2.savedId)||previous.bookId||"",bookTitle:((_c2=window.readerState)==null?void 0:_c2.title)||previous.bookTitle||"",characterId:previous.characterId||"",savedAt:Date.now()}))}catch{}}window.studioRememberBook=function(bookId,title){try{const previous=_stuReadResume();localStorage.setItem(STUDIO_RESUME_KEY,JSON.stringify({phase:_stuActive||previous.phase||1,bookId:bookId||previous.bookId||"",bookTitle:title||previous.bookTitle||"",characterId:previous.characterId||"",savedAt:Date.now()}))}catch{}},window.studioRememberCharacter=function(characterId){var _a2,_b2;try{const previous=_stuReadResume();localStorage.setItem(STUDIO_RESUME_KEY,JSON.stringify({phase:previous.phase||_stuActive||3,bookId:previous.bookId||((_a2=window._audiobook)==null?void 0:_a2.bookId)||"",bookTitle:previous.bookTitle||((_b2=window.readerState)==null?void 0:_b2.title)||"",characterId:characterId||"",savedAt:Date.now()}))}catch{}};const _stuHomes=new Map;function _stuBorrow(id,slotId){const el=document.getElementById(id),slot=document.getElementById(slotId);!el||!slot||(_stuHomes.has(id)||_stuHomes.set(id,{parent:el.parentNode,next:el.nextSibling}),slot.appendChild(el))}function _stuReturnAll(){_stuIsActive=!1,typeof _stuRestoreCastFoot=="function"&&_stuRestoreCastFoot(),_stuHomes.forEach(function(home,id){const el=document.getElementById(id);el&&home.parent&&home.parent.insertBefore(el,home.next)}),_stuHomes.clear()}window._stuReturnAll=_stuReturnAll;let _stuIsActive=!1,_stuAllowNextNav=!1;const _STU_BORROWED_FROM={"s-reader":1,"s-library":1,"s-rehearser":1};let _stuNavGuardInstalled=!1;function _stuInstallNavGuardOnce(){if(_stuNavGuardInstalled)return;_stuNavGuardInstalled=!0;const realNavTo=window.navTo;window.navTo=function(id){if(!(_stuIsActive&&!_stuAllowNextNav&&_STU_BORROWED_FROM[id]))return _stuAllowNextNav=!1,realNavTo(id)};const realShowReaderView=window.showReaderView;let _stuInShowReaderView=!1;window.showReaderView=function(view){const result=typeof realShowReaderView=="function"?realShowReaderView(view):void 0;if(_stuIsActive&&!_stuInShowReaderView&&(view==="cast"||view==="chars")){const target=view==="chars"?"sheets":"identify";if(_stuCastView!==target){_stuInShowReaderView=!0;try{_stuShowCastView(target)}finally{_stuInShowReaderView=!1}}}return result},document.querySelectorAll('[data-nav-section="s-reader"], #nav-reader-tree, [data-nav-section="s-library"], #nav-library-tree, [data-nav-section="s-rehearser"], #nav-rehearser-tree').forEach(function(el){el.addEventListener("click",function(){_stuAllowNextNav=!0},!0)})}function _stuCallSuppressingNav(fn){return fn()}function _stuEnterPhase(n){if(n===1)_stuBorrow("reader-main-view","stu-source-slot"),typeof window.readerOnShow=="function"&&_stuCallSuppressingNav(window.readerOnShow),_stuBorrow("lib-books-list","stu-books-slot"),_stuCallSuppressingNav(function(){typeof window.libraryRenderBooks=="function"&&window.libraryRenderBooks()});else if(n===2)_stuShowCastView(_stuCastView);else if(n===3)_stuBorrow("lib-chars-list","stu-voices-slot"),(async()=>{let title=null;if(window._audiobook&&window._audiobook.bookId)try{const r=await fetch("/api/reader/docs/"+encodeURIComponent(window._audiobook.bookId));r.ok&&(title=(await r.json()).title||null)}catch{}window._libCharsScrollToBook=title||window.readerState&&readerState.title||null,_stuCallSuppressingNav(function(){typeof window.libraryRender=="function"&&window.libraryRender("characters")})})();else if(n===4){_stuBorrow("reh-phase-3","stu-stage-slot"),_stuBorrow("reh-cast-list","stu-mecast-slot"),typeof _abInitPauseUI=="function"&&_abInitPauseUI(),_stuCallSuppressingNav(async function(){if(window.rehState&&rehState.lines&&rehState.lines.length){typeof buildScriptPage=="function"&&buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine();return}if(!(window._audiobook&&window._audiobook.segments&&window._audiobook.segments.length)&&typeof _abLoadDraftServer=="function"&&typeof _abBookId=="function"){const bookId=_abBookId(),draft=bookId?await _abLoadDraftServer(bookId):null;draft&&(_audiobook.segments=draft.segments||[],_audiobook.roster=draft.roster||[],_audiobook.pageMarks=draft.pageMarks||[],_audiobook.rehId=draft.rehId||_audiobook.rehId||null)}if(typeof window.audiobookOpenCurrentInRehearser=="function"&&(window._audiobook&&window._audiobook.segments||[]).length)return window.audiobookOpenCurrentInRehearser()});const rp3=document.getElementById("reh-phase-3");rp3&&(rp3.hidden=!1),_stuSyncModeToggle()}}function _stuSyncModeToggle(){const cb=document.getElementById("stu-mode-audiobook"),details=document.getElementById("stu-mecast-details");!cb||!window.rehState||(cb.checked=!rehState.skipDescriptions,details&&(details.hidden=cb.checked))}(_vc=document.getElementById("stu-mode-audiobook"))==null||_vc.addEventListener("change",function(){const audiobookMode=this.checked;if(window.rehState){rehState.skipDescriptions=!audiobookMode;const t=document.getElementById("reh-skip-desc-toggle");t&&(t.checked=rehState.skipDescriptions),audiobookMode&&!rehState.narratorVoice&&typeof toast=="function"&&toast("No narrator voice assigned yet \u2014 assign one in the Voices phase so narration actually plays","error")}const details=document.getElementById("stu-mecast-details");details&&(details.hidden=audiobookMode)});let _stuCastView="identify";function _stuShowCastView(view){_stuCastView=view,document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(t){t.classList.toggle("active",t.dataset.stuCastView===view)});const identifySlot=document.getElementById("stu-cast-slot"),sheetsSlot=document.getElementById("stu-castchars-slot");if(identifySlot&&(identifySlot.hidden=view!=="identify"),sheetsSlot&&(sheetsSlot.hidden=view!=="sheets"),view==="identify")_stuBorrow("reader-audiobook-panel","stu-cast-slot"),_stuCallSuppressingNav(function(){if(typeof window.audiobookOpenCastView=="function")return window.audiobookOpenCastView();typeof window.showReaderView=="function"&&window.showReaderView("cast")}),_stuRelocateCastFoot();else if(view==="sheets"){_stuBorrow("reader-charsheets-panel","stu-castchars-slot");const panel=document.getElementById("reader-charsheets-panel");if(panel&&!panel.innerHTML.trim()){panel.innerHTML='
Character sheets

Optional \u2014 let the AI fill out full character profiles (appearance, backstory, voice notes) for reference. Skip this if you just want to cast voices quickly.

';const goBtn=document.getElementById("stu-goto-cast-menu");goBtn&&goBtn.addEventListener("click",function(){typeof window.csForReader=="function"&&window.csForReader()})}}}document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(tab){tab.addEventListener("click",function(){_stuShowCastView(tab.dataset.stuCastView)})}),(_wc=document.getElementById("stu-book-context-btn"))==null||_wc.addEventListener("click",async function(){var _a2;const bookTitle=((_a2=window.readerState)==null?void 0:_a2.title)||"";!bookTitle||typeof window.openBookContextEditor!="function"||await window.openBookContextEditor(bookTitle)}),(_xc=document.getElementById("stu-change-book-btn"))==null||_xc.addEventListener("click",function(){showStudioPhase(1)}),(_yc=document.getElementById("stu-add-book-btn"))==null||_yc.addEventListener("click",function(){showStudioPhase(1);const dz=document.getElementById("reader-dropzone");dz&&(dz.hidden=!1);const importArea=document.getElementById("reader-import-area");importArea&&(importArea.hidden=!1);const card=document.getElementById("reader-config-card");if(card){const body=card.querySelector(".card-col-body");body&&body.hidden&&(body.hidden=!1,card.classList.remove("card-col-closed")),card.scrollIntoView({behavior:"smooth",block:"start"})}});let _stuCastFootObserver=null,_stuCastFootHealthTimer=null;function _stuCastIsRunning(){return typeof _audiobook!="undefined"&&!!_audiobook.running}function _stuSyncCastFallbackControls(tabs,innerTabs,hasActionFooter){const running=_stuCastIsRunning();tabs.hidden=!1,tabs.style.display="flex",innerTabs.forEach(function(tab){const hideFallback=running||hasActionFooter;tab.hidden=hideFallback,tab.style.display=hideFallback?"none":""})}function _stuRelocateCastFoot(){_stuTryRelocateCastFoot(),!_stuCastFootObserver&&(_stuCastFootObserver=new MutationObserver(function(){_stuTryRelocateCastFoot()}),_stuCastFootObserver.observe(document.body,{childList:!0,subtree:!0}),[0,100,300,800,1500].forEach(ms=>setTimeout(_stuTryRelocateCastFoot,ms)),_stuCastFootHealthTimer=setInterval(function(){_stuIsActive&&_stuActive===2&&_stuTryRelocateCastFoot()},500))}function _stuTryRelocateCastFoot(){const slot=document.getElementById("stu-cast-slot"),tabs=document.getElementById("stu-cast-inner-tabs");if(!tabs)return;const freshFoot=slot&&slot.querySelector("#ab-cv-foot")||document.querySelector("#ab-cv-foot"),innerTabs=Array.from(tabs.querySelectorAll(":scope > .stu-inner-tab")),footerHasActions=freshFoot&&freshFoot.querySelector("#ab-cv-menu-identify, #ab-cv-menu-cast, #ab-cv-menu-viewcast");if(_stuSyncCastFallbackControls(tabs,innerTabs,!!footerHasActions),!freshFoot||!footerHasActions)return;if(freshFoot.parentElement===tabs){freshFoot.hidden=!1,freshFoot.style.display="flex";return}tabs.querySelectorAll("#ab-cv-foot").forEach(function(stale){stale.remove()}),tabs.hidden=!1,tabs.style.display="flex",freshFoot.style.borderTop="none",freshFoot.style.padding="0",freshFoot.style.justifyContent="flex-start",freshFoot.hidden=!1,freshFoot.style.display="flex",tabs.appendChild(freshFoot);const openReh=freshFoot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!0)}function _stuRestoreCastFoot(){_stuCastFootObserver&&(_stuCastFootObserver.disconnect(),_stuCastFootObserver=null),_stuCastFootHealthTimer&&(clearInterval(_stuCastFootHealthTimer),_stuCastFootHealthTimer=null);const tabs=document.getElementById("stu-cast-inner-tabs"),panel=document.getElementById("reader-audiobook-panel"),foot=tabs?tabs.querySelector("#ab-cv-foot"):null;if(foot&&panel){foot.style.borderTop="",foot.style.padding="",foot.style.justifyContent="";const openReh=foot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!1),panel.appendChild(foot)}document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!1})}function showStudioPhase(n){_stuActive=n,_stuSaveResume(n);for(let i=1;i<=4;i++){const el=document.getElementById("stu-phase-"+i);el&&(el.hidden=i!==n)}document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.classList.toggle("active",parseInt(tab.dataset.stuPhase,10)===n)}),document.querySelectorAll("#nav-caststudio-tree [data-stu-phase]").forEach(function(item){item.classList.toggle("is-active",parseInt(item.dataset.stuPhase,10)===n)});const prevBtn=document.getElementById("stu-phase-prev"),nextBtn=document.getElementById("stu-phase-next");prevBtn&&(prevBtn.disabled=n<=1),nextBtn&&(nextBtn.disabled=n>=4),typeof _stuEnterPhase=="function"&&_stuEnterPhase(n)}window.showStudioPhase=showStudioPhase,document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.addEventListener("click",function(){showStudioPhase(parseInt(tab.dataset.stuPhase,10))})}),(_zc=document.getElementById("stu-phase-prev"))==null||_zc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_Ac=document.getElementById("stu-phase-next"))==null||_Ac.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){var _a2,_b2,_c2;_stuInstallNavGuardOnce(),_stuIsActive=!0;const resume=_stuReadResume();resume.bookId&&window._audiobook&&(window._audiobook.bookId=resume.bookId),resume.phase>=1&&resume.phase<=4&&(_stuActive=resume.phase,resume.characterId&&(window._libResumeCharacterId=resume.characterId));const resumeBookId=resume.bookId,hasCurrentBook=!!((_b2=(_a2=window.readerState)==null?void 0:_a2.sentences)!=null&&_b2.length&&((_c2=window.readerState)!=null&&_c2.savedId));if(resumeBookId&&!hasCurrentBook&&typeof window.readerOpenLibraryDoc=="function"){showStudioPhase(_stuActive),window.readerOpenLibraryDoc(resumeBookId).then(function(){showStudioPhase(_stuActive)});return}showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow,function(){const root=window.TTVC=window.TTVC||{};root.audiobook=root.audiobook||{},root.rehearser=root.rehearser||{},root.jobs=root.jobs||{},[["audiobook","fetchWithTimeout","audiobookFetchWithTimeout"],["audiobook","attributeStream","audiobookAttributeStream"],["audiobook","timeoutSeconds","audiobookTimeoutSeconds"],["rehearser","renderLibrary","renderLibraryList"],["rehearser","showPhase","showPhase"]].forEach(([namespace,publicName,legacyName])=>{typeof window[legacyName]!="undefined"&&root[namespace][publicName]==null&&(root[namespace][publicName]=window[legacyName])}),typeof rehState!="undefined"&&root.rehearser.state==null&&(root.rehearser.state=rehState)}(); +`);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}(_da=$("stt-tts-file"))==null||_da.addEventListener("change",async()=>{const input=$("stt-tts-file");input.files&&input.files.length&&await sttTtsUploadFile(input.files[0]),input.value=""}),(_ea=$("stt-tts-refresh-stt-btn"))==null||_ea.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}}),(_fa=$("stt-tts-stt-backend"))==null||_fa.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}(_ga=$("stt-tts-rec-start"))==null||_ga.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")}}),(_ha=$("stt-tts-rec-stop"))==null||_ha.addEventListener("click",()=>{sttTtsRecorder&&sttTtsRecorder.state!=="inactive"&&sttTtsRecorder.stop()}),(_ia=$("stt-tts-transcribe-btn"))==null||_ia.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}}),(_ja=$("stt-tts-copy-preview-btn"))==null||_ja.addEventListener("click",()=>{const text=$("stt-tts-text").value.trim();text&&($("preview-text-area").value=text,switchTab("generation"),toast("Copied transcription to TTS Generation","success"))}),(_ka=$("stt-tts-backend-select"))==null||_ka.addEventListener("change",()=>{$("stt-tts-voice-select").innerHTML='',sttTtsOutputBlob=null,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0,updateBackendHelp()}),(_la=$("stt-tts-fetch-voices-btn"))==null||_la.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}}),(_ma=$("stt-tts-generate-btn"))==null||_ma.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}}),(_na=$("stt-tts-save-mp3-btn"))==null||_na.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}}),(_oa=$("stt-tts-save-wav-btn"))==null||_oa.addEventListener("click",()=>{sttTtsOutputBlob&&sttTtsDownload(sttTtsOutputBlob,($("stt-tts-voice-select").value||"stt_tts")+"_stt_tts.wav")}),function(){const root=window.TTVC=window.TTVC||{};root.audiobook=root.audiobook||{},root.rehearser=root.rehearser||{},root.jobs=root.jobs||{},[["audiobook","fetchWithTimeout","audiobookFetchWithTimeout"],["audiobook","attributeStream","audiobookAttributeStream"],["audiobook","timeoutSeconds","audiobookTimeoutSeconds"],["rehearser","renderLibrary","renderLibraryList"],["rehearser","showPhase","showPhase"]].forEach(([namespace,publicName,legacyName])=>{typeof window[legacyName]!="undefined"&&root[namespace][publicName]==null&&(root[namespace][publicName]=window[legacyName])}),typeof rehState!="undefined"&&root.rehearser.state==null&&(root.rehearser.state=rehState)}(); diff --git a/static/loader.js b/static/loader.js index 671001f..f157086 100644 --- a/static/loader.js +++ b/static/loader.js @@ -127,24 +127,51 @@ // C — feature modules: always try the bundle first (one request instead of 23). // Falls back to individual files if the bundle is missing or fails to load. var _featureFiles = []; + var _lazyFeatureGroups = {}; try { var _manifestResponse = await fetch('/static/module-manifest.json?v=' + _appVersion, { cache: 'no-store' }); if (!_manifestResponse.ok) throw new Error('module manifest ' + _manifestResponse.status); var _manifest = await _manifestResponse.json(); - if (!Array.isArray(_manifest) || !_manifest.length) throw new Error('empty module manifest'); - _featureFiles = _manifest.map(function (name) { return '/static/js/' + name + '.js'; }); + if (!Array.isArray(_manifest.core) || !_manifest.core.length) throw new Error('empty core module manifest'); + _featureFiles = _manifest.core.map(function (name) { return '/static/js/' + name + '.js'; }); + _lazyFeatureGroups = _manifest.lazy && typeof _manifest.lazy === 'object' ? _manifest.lazy : {}; } catch (_) { console.error('Unable to load the feature module manifest'); } var _bundleOk = false; try { await _load('/static/dist/main.min.js'); _bundleOk = true; } catch (_) { _bundleOk = false; } - if (!_bundleOk) await _loadBatch(_featureFiles); + var _loadedFeatureModules = new Set(); + if (_bundleOk) _featureFiles.forEach(function (src) { _loadedFeatureModules.add(src); }); + if (!_bundleOk) { + await _loadBatch(_featureFiles); + _featureFiles.forEach(function (src) { _loadedFeatureModules.add(src); }); + } + + // Public section loader used by nav.js. Modules are loaded in manifest order, + // exactly once, and a failed group leaves the existing section visible rather + // than turning navigation into a blank screen. + window.ensureFeatureModules = function (sectionId) { + var names = Array.isArray(_lazyFeatureGroups[sectionId]) ? _lazyFeatureGroups[sectionId] : []; + var pending = names.map(function (name) { return '/static/js/' + name + '.js'; }) + .filter(function (src) { return !_loadedFeatureModules.has(src); }); + if (!pending.length) return Promise.resolve(); + pending.forEach(function (src) { _loadedFeatureModules.add(src); }); + return _loadBatch(pending).catch(function (error) { + pending.forEach(function (src) { _loadedFeatureModules.delete(src); }); + console.error('Unable to load section modules for ' + sectionId, error); + throw error; + }); + }; // D — navigation (must run before init/reveal so routing is interactive before // async settings or voice-library refreshes can occupy the backend). try { await _load('/static/nav.js'); } catch (_) {} + // A deep link or restored section must have its feature code before init and + // before the first visible section is activated. + try { if (window.ensureFeatureModules) await window.ensureFeatureModules(_activeSection); } catch (_) {} + // E — init (needs feature modules and nav globals to be defined) try { await _load('/static/js/init.js'); } catch (_) {} diff --git a/static/module-manifest.json b/static/module-manifest.json index 71c6ab8..f6b2feb 100644 --- a/static/module-manifest.json +++ b/static/module-manifest.json @@ -1,6 +1,12 @@ -[ - "voice-picker", "benchmark-voice-picker", "voice-inspector", "seed-finder", "voice-sources", "fishaudio-browser", "voice-cache", "jobs", - "integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", - "rehearser-parse", "rehearser", "reader", "audiobook", "audiobook/network", "character-sheets", - "characters-library", "sillytavern", "library", "character-columns", "library-characters", "studio", "namespace" -] +{ + "core": [ + "voice-picker", "benchmark-voice-picker", "voice-inspector", "seed-finder", "voice-sources", "fishaudio-browser", "voice-cache", "jobs", + "integrations", "routing", "voice-clone", "voice-library", "tts-preview", "generation", "benchmark", "stt", "namespace" + ], + "lazy": { + "s-rehearser": ["rehearser-parse", "rehearser"], + "s-reader": ["rehearser-parse", "rehearser", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets"], + "s-library": ["rehearser-parse", "rehearser", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"], + "s-caststudio": ["rehearser-parse", "rehearser", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"] + } +} diff --git a/static/nav.js b/static/nav.js index 99a5ab7..f1bf872 100644 --- a/static/nav.js +++ b/static/nav.js @@ -216,18 +216,24 @@ // Preserve that early intent so startup cannot immediately switch back to // the previously stored section. if (SECTIONS.includes(sectionId)) window._navRequestedSection = sectionId; - showSection(sectionId); - var tabName = Object.keys(TAB_SECTION_MAP).find(function (k) { - return TAB_SECTION_MAP[k] === sectionId; - }); - if (tabName) runSideEffects(tabName); - // Keep the Reader/Casting/Characters/Rehearser breadcrumb in sync with - // plain sidebar navigation (not just the crumb's own buttons). - if (typeof window.refreshWorkflowCrumbs === 'function') { - if (sectionId === 's-rehearser') window.refreshWorkflowCrumbs('rehearser'); - else if (sectionId === 's-reader') window.refreshWorkflowCrumbs('source'); - else if (sectionId === 's-library' && (window._libraryView || 'books') === 'characters') window.refreshWorkflowCrumbs('chars'); + var activate = function () { + showSection(sectionId); + var tabName = Object.keys(TAB_SECTION_MAP).find(function (k) { + return TAB_SECTION_MAP[k] === sectionId; + }); + if (tabName) runSideEffects(tabName); + // Keep the Reader/Casting/Characters/Rehearser breadcrumb in sync with + // plain sidebar navigation (not just the crumb's own buttons). + if (typeof window.refreshWorkflowCrumbs === 'function') { + if (sectionId === 's-rehearser') window.refreshWorkflowCrumbs('rehearser'); + else if (sectionId === 's-reader') window.refreshWorkflowCrumbs('source'); + else if (sectionId === 's-library' && (window._libraryView || 'books') === 'characters') window.refreshWorkflowCrumbs('chars'); + } + }; + if (typeof window.ensureFeatureModules === 'function') { + return window.ensureFeatureModules(sectionId).then(activate, function () { activate(); }); } + activate(); }; // Pulsing dot on the Read Aloud nav item while casting is in progress @@ -246,9 +252,8 @@ window.switchTab = function (name) { var sectionId = TAB_SECTION_MAP[name]; - if (sectionId) showSection(sectionId); - runSideEffects(name); - return true; + if (!sectionId) return false; + return window.navTo(sectionId); }; // ── Voice tree sub-navigation ───────────────────────────────────────────── @@ -431,11 +436,16 @@ window._settingsSidebarCat = localStorage.getItem('ttsvc_settings_cat') || 'connections'; window._enginesSidebarCat = localStorage.getItem('ttsvc_engines_cat') || 'llm'; var _startSection = window._navRequestedSection || hashSection() || ((_savedSection && SECTIONS.includes(_savedSection)) ? _savedSection : 's-voices'); - showSection(_startSection); + if (typeof window.ensureFeatureModules === 'function') { + window.ensureFeatureModules(_startSection).then(function () { showSection(_startSection); }, function () { showSection(_startSection); }); + } else showSection(_startSection); window.addEventListener('hashchange', function () { var section = hashSection(); - if (section && section !== storedSection()) showSection(section); + if (section && section !== storedSection()) { + if (typeof window.ensureFeatureModules === 'function') window.ensureFeatureModules(section).then(function () { showSection(section); }, function () { showSection(section); }); + else showSection(section); + } }); var _startTab = Object.keys(TAB_SECTION_MAP).find(function (k) { return TAB_SECTION_MAP[k] === _startSection; }) || 'library'; runSideEffects(_startTab); diff --git a/tests/characters.spec.js b/tests/characters.spec.js index edfdb40..843bad3 100644 --- a/tests/characters.spec.js +++ b/tests/characters.spec.js @@ -21,13 +21,13 @@ test('Characters / Cast supports profile sorting, column visibility, and voice r await page.route('**/api/voice/meta', route => route.fulfill({ json: { ok: true } })); await page.goto('/', { waitUntil: 'domcontentloaded' }); - await page.waitForFunction(() => typeof window.navTo === 'function' && typeof window.navLibraryView === 'function', null, { timeout: 15_000 }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); // The loader injects inactive section HTML in the background; wait until // the Library panel exists before switching its sub-view. await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 }); - await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); await page.evaluate(() => window.navTo('s-library')); + await page.waitForFunction(() => typeof window.navLibraryView === 'function' && typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 }); await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click()); await expect(page.locator('#s-library')).toHaveClass(/is-active/, { timeout: 15_000 }); @@ -82,6 +82,8 @@ test('consistent image generation can be cancelled from its progress panel', asy })); await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-library')); await page.waitForFunction(() => typeof window._charAutoGenerateConsistentImages === 'function', null, { timeout: 15_000 }); await page.evaluate(() => { window.__cancelRun = window._charAutoGenerateConsistentImages({ @@ -110,8 +112,10 @@ test('current-version image pairs marked inconsistent show a review warning', as await page.route('**/api/settings*', route => route.fulfill({ json: {} })); await page.route('**/api/book-profile*', route => route.fulfill({ json: { profile: {} } })); await page.goto('/', { waitUntil: 'domcontentloaded' }); - await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-library')); + await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); await page.locator('#nav-library-head').evaluate(el => el.click()); await page.locator('.lib-tab[data-library-view="characters"]').evaluate(el => el.click()); @@ -122,8 +126,10 @@ test('current-version image pairs marked inconsistent show a review warning', as test('character appearance forms stay in one record and switch the detail view', async ({ page }) => { await page.route('**/api/book-profile*', route => route.fulfill({ json: { profile: {} } })); await page.goto('/', { waitUntil: 'domcontentloaded' }); - await page.waitForFunction(() => typeof window._charDetailPage === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-library')); + await page.waitForFunction(() => typeof window._charDetailPage === 'function', null, { timeout: 15_000 }); await page.evaluate(() => window._charDetailPage({ id: 'c-forms', book: 'Smoke Book', name: 'Zerwas', voice: 'de_bob', image: '/human.png', sheet: { @@ -144,6 +150,8 @@ test('character appearance forms stay in one record and switch the detail view', test('profile completeness counter distinguishes empty, partial, and full sheets', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-library')); await page.waitForFunction(() => typeof window._charProfileCompleteness === 'function', null, { timeout: 15_000 }); const counts = await page.evaluate(() => ({ empty: window._charProfileCompleteness({}), diff --git a/tests/constrained.spec.js b/tests/constrained.spec.js index 3f407a7..ee871bd 100644 --- a/tests/constrained.spec.js +++ b/tests/constrained.spec.js @@ -30,8 +30,10 @@ test('large character productions keep the rendered DOM paginated', async ({ pag await page.route('**/api/voices*', route => route.fulfill({ json: [] })); await page.route('**/api/settings*', route => route.fulfill({ json: {} })); await page.goto('/', { waitUntil: 'domcontentloaded' }); - await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#lib-chars-list', { state: 'attached', timeout: 15_000 }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-library')); + await page.waitForFunction(() => typeof window.libraryRenderCharacters === 'function', null, { timeout: 15_000 }); await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); await page.evaluate(() => window.navTo('s-library')); await page.locator('.lib-tab[data-library-view="characters"]').click(); @@ -88,6 +90,8 @@ test('voice library cache reuses fresh data and honors explicit refresh', async test('Rehearser audio cache stays bounded with LRU eviction', async ({ page }) => { await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-rehearser')); await page.waitForFunction(() => window.rehState?.synthCache, null, { timeout: 15_000 }); const size = await page.evaluate(() => { const cache = window.rehState.synthCache; diff --git a/tests/lazy-loading.spec.js b/tests/lazy-loading.spec.js new file mode 100644 index 0000000..97da28c --- /dev/null +++ b/tests/lazy-loading.spec.js @@ -0,0 +1,25 @@ +// @ts-check +// Section-specific feature code should not be fetched until its section is opened. +const { test, expect } = require('@playwright/test'); + +test('heavy feature modules load on demand', async ({ page }) => { + const requests = []; + page.on('request', request => { + if (request.url().includes('/static/js/')) requests.push(request.url()); + }); + + await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); + expect(requests.some(url => url.includes('/rehearser.js'))).toBeFalsy(); + expect(requests.some(url => url.includes('/library-characters.js'))).toBeFalsy(); + + await page.evaluate(() => window.navTo('s-rehearser')); + await page.waitForFunction(() => window.rehState?.synthCache, null, { timeout: 15_000 }); + expect(requests.some(url => url.includes('/rehearser.js'))).toBeTruthy(); + + const countBeforeRepeat = requests.filter(url => url.includes('/rehearser.js')).length; + await page.evaluate(() => window.navTo('s-rehearser')); + await page.waitForTimeout(100); + expect(requests.filter(url => url.includes('/rehearser.js')).length).toBe(countBeforeRepeat); +});