diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6b945c..6f13fb3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
- **Emotion instructions are now always written in English**, even for non-English voices (the spoken text and the native-accent clause stay in the book's own language). Confirmed by controlled A/B testing — same line, same voice, only the instruct language varying — that Qwen3-TTS follows English emotion instructions far more reliably: German instructs produced barely-differentiated output, while English instructs yield a clean, correctly-ordered prosodic gradient (whisper 128 Hz → sad 142 → neutral 179 → scared 203 → happy 225 → angry 269 Hz), with sensible duration changes too (sad slowest, scared fastest).
- **Fish-Speech generation parameters (`temperature` / `top_p` / `repetition_penalty`) were never forwarded.** Every Fish-Speech line synthesized at the server's fixed defaults, ignoring the app's per-backend stability settings — the only backend not routed through the shared `_apply_tts_extra_params` helper.
+## [1.20.29] — 2026-08-12
+
+### Added
+- **Quotation marks in the casting script, with a toggle.** German prose never quotes narration — only characters speak in `» «` — so showing the marks makes a mislabeled line obvious at a glance. PDF extraction strips them from roughly 80% of segments, so they are now *rendered* rather than stored: dialogue rows are displayed wrapped in `» «` (lines that kept their own marks are left untouched), while the saved text stays exactly as cast so synthesis and exports are unaffected. The quote button in the casting view toggles them on or off, and the choice is remembered.
+
## [1.20.27] — 2026-08-12
### Fixed
diff --git a/VERSION b/VERSION
index 8db5200..f4e3c08 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.20.27
+1.20.29
diff --git a/static/dist/main.min.js b/static/dist/main.min.js
index 3212437..08c1509 100644
--- a/static/dist/main.min.js
+++ b/static/dist/main.min.js
@@ -1188,7 +1188,7 @@ ${lines.trim()}
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=[...selected];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;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=`
+ `},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(""):'
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=[...selected];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;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=`
@@ -1360,7 +1361,7 @@ Eine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/
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=panel.querySelector("#ab-cv-jump-btn"),_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=`
+- 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=`
@@ -1368,7 +1369,7 @@ ${newRules}`}return/gehören IMMER zum Dialog-Segment/.test(p)||(p=p.replace("-
`,_abTopbar.appendChild(_abPageNav);const _abFontCtl=document.createElement("div");_abFontCtl.className="ab-font-ctl",_abFontCtl.innerHTML=`
- `,_abTopbar.appendChild(_abFontCtl);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=highlightText2(seg.text||""))}),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,_g2;_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(name)}${_abCharRows.length} Zeilen
@@ -1432,15 +1433,15 @@ ${newRules}`}return/gehören IMMER zum Dialog-Segment/.test(p)||(p=p.replace("-
-
`,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)};(_a2=_abPageNav.querySelector(".ab-page-select"))==null||_a2.addEventListener("change",e=>_abScrollToCastPage(e.target.value)),(_b2=_abPageNav.querySelector(".ab-page-prev"))==null||_b2.addEventListener("click",()=>_abStepPage(-1)),(_c2=_abPageNav.querySelector(".ab-page-next"))==null||_c2.addEventListener("click",()=>_abStepPage(1)),(_d2=_abPageNav.querySelector(".ab-page-source"))==null||_d2.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=`
+
`,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 _abRedrawSegmentsChunked2(_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)};(_b2=_abPageNav.querySelector(".ab-page-select"))==null||_b2.addEventListener("change",e=>_abScrollToCastPage(e.target.value)),(_c2=_abPageNav.querySelector(".ab-page-prev"))==null||_c2.addEventListener("click",()=>_abStepPage(-1)),(_d2=_abPageNav.querySelector(".ab-page-next"))==null||_d2.addEventListener("click",()=>_abStepPage(1)),(_e2=_abPageNav.querySelector(".ab-page-source"))==null||_e2.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)+")":""}
- ${highlightText2(s.text||"")}`,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=highlightText2(s.text||"")};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())};(_e2=_abEditToolbar.querySelector(".ab-edit-undo"))==null||_e2.addEventListener("click",_abUndoEdit),(_f2=_abEditToolbar.querySelector(".ab-edit-redo"))==null||_f2.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 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(`
+ ${highlightText2(_abDisplayText(s))}`,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=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 _abRedrawSegmentsChunked2=(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())};(_f2=_abEditToolbar.querySelector(".ab-edit-undo"))==null||_f2.addEventListener("click",_abUndoEdit),(_g2=_abEditToolbar.querySelector(".ab-edit-redo"))==null||_g2.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?_abRedrawSegmentsChunked2(active.arr):_abPatchRowRange([leftRow,rightRow],[merged],active.arr)||_abRedrawSegmentsChunked2(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 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=`
+`)||/^[»„“"'‘]/.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(),_abRedrawSegmentsChunked2(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
@@ -1456,7 +1457,7 @@ ${newRules}`}return/gehören IMMER zum Dialog-Segment/.test(p)||(p=p.replace("-
- `,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)+")":""}${highlightText2(s.text||"")}`,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)+")":""}${highlightText2(s.text||"")}`,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;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?".
+ `,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)+")":""}${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)+")":""}${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;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&&_abRedrawSegmentsChunked2(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.
@@ -1484,7 +1485,7 @@ 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,includeNarrator:!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()=>{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.
+- 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,includeNarrator:!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 _abRedrawSegmentsChunked2(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()=>{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}])),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:"Cast selected character roles",title:existing.length?"Generate / refresh the character sheets for selected cast characters":"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(!(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)}))}}}async function audiobookRecastUnknown(overrideUrl,overrideModel,options={}){var _a2,_b2,_c2,_d2,_e2,_f2;if(_audiobook.running)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)));if(repl.length&&hasResolved&&!hasUnresolvedDialogue){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}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}_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,{includeNarrator:!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)}async function audiobookCast(overrideUrl,overrideModel,resume){var _a2,_b2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}if(typeof parseScript!="function"){toast("Rehearser not loaded yet \u2014 try again in a moment","error");return}_audiobook.running=!0,!!_abBookId()||!await _abEnsureLibraryBook()&&typeof toast=="function"&&toast("Autosave will use this browser only until the book is saved to the library.","info");const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text],startIndex=resume&&resume.startIndex>0&&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,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(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())}}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()+`
diff --git a/static/index.html b/static/index.html
index d5065b6..9f0c88c 100644
--- a/static/index.html
+++ b/static/index.html
@@ -10,7 +10,7 @@
-
+
@@ -27,7 +27,7 @@
-
+
@@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) {
-
+