diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9e60377..70ce0c9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -22,6 +22,19 @@ 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.16] — 2026-08-12
+
+### Added
+- **"Repair mislabeled dialogue"** — a new casting pass for spoken lines that PDF extraction typed as narration (usually because the `» «` quote marks were lost). These were invisible to "Identify unknown characters", which only scans `type === 'dialogue'`, so re-running it could never fix them; the only pass that could ("Verify all characters") re-sent every narration segment to the LLM. The new pass scores narration for direct-speech signals (a bare inquit in the next segment, stray quote marks, imperative opening, 1st/2nd person, ending in `!`/`?`) and re-checks only the suspicious ones — measured on a real novel: **36 candidates instead of 1036, 29x fewer LLM calls**.
+- **"Auto-repair cast"** — chains the repair sequence automatically (repair mislabeled → resolve unknown, repeated while it still helps, then a full verification only if still above target). Runs on its own after a completed cast when more than 10 Unknown speakers remain; never after a stopped run. Disable via `localStorage['ab-auto-repair-enabled'] = '0'`.
+- **German speaker-attribution rules 13–19** in both the casting and verification prompts: listener/vocative exclusion (a name beside a quote is often the listener, not the speaker), split quotes with pronoun inquits, ranked disambiguation among multiple candidates, continuation over ping-pong, unnamed actors as speakers, epithet resolution ("die Elfe" → Nyrilla), and splitting segments that fuse narration with dialogue or two speakers' turns.
+
+### Fixed
+- **A stopped casting run could silently leave the cast worse than before.** The automatic rollback that undoes a pass which increases Unknown speakers was gated behind `!_audiobook.cancel`, so stopping a run skipped it entirely — confirmed live: an interrupted pass took a book from 80 Unknown lines to 159 with no warning and no way back. Rollback now applies to interrupted runs too.
+- **Corrected a misleading attribution rule.** Rule 4 told the model that whoever acts before *or after* a quote is usually the speaker, but in German prose the neighbouring name is frequently the listener ("Marcian spürte ihre Tränen", "Marcian hörte darüber hinweg") or the person being addressed. It now applies only to active verbs, with perception/reaction verbs marking the listener.
+- **"Run until < N unknown" re-sent every narration segment to the LLM on every iteration** (`includeNarrator`), making each round enormously expensive and re-litigating lines that were already correct. It now uses the targeted repair pass.
+- Identify Characters menu reordered to the order the passes should actually be run in: identify all → repair mislabeled → identify unknown → run until threshold → verify all → check voice consistency.
+
## [1.20.5] — 2026-08-11
### Added
diff --git a/VERSION b/VERSION
index 95393fc..a46bb8e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.20.8
+1.20.16
diff --git a/static/dist/main.min.js b/static/dist/main.min.js
index e0eddd4..02204f7 100644
--- a/static/dist/main.min.js
+++ b/static/dist/main.min.js
@@ -1175,7 +1175,7 @@ ${lines.trim()}
`,confirmOverlay.addEventListener("click",ce=>ce.stopPropagation()),confirmOverlay.querySelector("#btn-cancel-del").addEventListener("click",ce=>{ce.stopPropagation(),confirmOverlay.remove()}),confirmOverlay.querySelector("#btn-confirm-del").addEventListener("click",async ce=>{ce.stopPropagation(),confirmOverlay.innerHTML='';try{if(!(await fetch(`${READER_API}/${el.dataset.id}`,{method:"DELETE"})).ok)throw new Error("Failed to delete book");toast("Book deleted","success"),readerRenderLibrary()}catch(err){toast(err.message,"error"),confirmOverlay.remove()}}),el.appendChild(confirmOverlay)}),el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.style.borderColor="var(--accent)")}),el.addEventListener("dragleave",()=>{el.style.borderColor=""}),el.addEventListener("drop",async e=>{var _a2;if(!(e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")))return;e.preventDefault(),e.stopPropagation(),el.style.borderColor="";const f=(_a2=e.dataTransfer.files)==null?void 0:_a2[0];if(!f||!f.type.startsWith("image/")){toast("Please drop an image file","error");return}try{const id=el.dataset.id,r=await fetch(`${READER_API}/${id}/cover`,{method:"PUT",body:f});if(!r.ok)throw new Error(r.statusText);await fetch(`${READER_API}/${id}/progress`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({updated:new Date().toISOString()})}),toast("Cover image updated","success"),readerRenderLibrary()}catch(err){toast("Failed to upload cover: "+(err.message||err),"error")}})}),list.querySelectorAll(".reader-book-del").forEach(btn=>btn.addEventListener("click",e=>{e.stopPropagation(),confirm("Delete this saved document and its audio?")&&readerDeleteLibraryDoc(btn.dataset.del)}))}(_Kb=$("reader-file-input"))==null||_Kb.addEventListener("change",function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];this.value="",readerImportFile(f)}),(_Lb=$("reader-paste-load"))==null||_Lb.addEventListener("click",readerImportPastedText),(_Mb=$("reader-paste-text"))==null||_Mb.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="Enter"&&(e.preventDefault(),readerImportPastedText())}),(_Nb=$("reader-fetch-voices-btn"))==null||_Nb.addEventListener("click",readerFetchVoices),(_Ob=$("reader-backend-select"))==null||_Ob.addEventListener("change",()=>{const sel=$("reader-voice-select");sel&&(sel.innerHTML=''),window.VoicePicker&&(VoicePicker.upgrade("reader-voice-select"),VoicePicker.populate("reader-voice-select",[])),readerUpdateBackendHint()}),(_Pb=$("reader-chunk-mode"))==null||_Pb.addEventListener("change",function(){readerRechunk(this.value)}),(_Qb=$("reader-normalize"))==null||_Qb.addEventListener("change",function(){readerState.normalize=this.checked}),(_Rb=$("reader-play"))==null||_Rb.addEventListener("click",()=>{readerState.playing?readerPause():readerPlay()}),(_Sb=$("reader-stop"))==null||_Sb.addEventListener("click",()=>{readerStopPlayback(),readerState.idx=0,readerSaveResume(),readerUpdateProgress()}),(_Tb=$("reader-prev"))==null||_Tb.addEventListener("click",()=>readerJumpTo(readerState.idx-1)),(_Ub=$("reader-next"))==null||_Ub.addEventListener("click",()=>readerJumpTo(readerState.idx+1)),(_Vb=$("reader-speed"))==null||_Vb.addEventListener("input",function(){readerState.speed=parseFloat(this.value)||1;const lbl=$("reader-speed-label");lbl&&(lbl.textContent=readerState.speed.toFixed(2).replace(/0$/,"")+"\xD7"),readerState.currentSource&&(readerState.currentSource.playbackRate.value=readerState.speed)}),(_Wb=$("reader-zoom-fitw"))==null||_Wb.addEventListener("click",()=>readerApplyZoom("fit-width")),(_Xb=$("reader-zoom-fith"))==null||_Xb.addEventListener("click",()=>readerApplyZoom("fit-height")),(_Yb=$("reader-zoom-two"))==null||_Yb.addEventListener("click",()=>readerApplyZoom("two")),(_Zb=$("reader-zoom-in"))==null||_Zb.addEventListener("click",()=>{readerState.scale=Math.min(4,readerState.scale*1.2),readerApplyZoom("custom")}),(__b=$("reader-zoom-out"))==null||__b.addEventListener("click",()=>{readerState.scale=Math.max(.2,readerState.scale/1.2),readerApplyZoom("custom")});let _readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,_readerSearchTimer=null;const READER_SEARCH_MAX_HITS=2500;function readerSearchTextMap(words){var _a2;let text="";const spans=[];for(let i=0;isp.end>pos&&sp.startsp.wordIndex);idxs.length&&groups.push(idxs),pos=Math.max(pos+1,end)}return groups}function readerRemoveSearchHighlights(){document.querySelectorAll(".reader-search-hit").forEach(el=>el.remove()),document.querySelectorAll(".reader-word.search-hit, .reader-word.search-current").forEach(el=>{el.classList.remove("search-hit","search-current")})}function readerSetSearchStatus(text){const st=$("reader-search-status");st&&(st.textContent=text||"")}function readerClearSearch({clearInput=!1}={}){if(_readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,readerRemoveSearchHighlights(),readerSetSearchStatus(""),clearInput){const inp=$("reader-search");inp&&(inp.value="")}}function readerCollectSearchHits(term){var _a2,_b2,_c2;const hits=[],sents=readerState.sentences||[];for(let si=0;si=READER_SEARCH_MAX_HITS)return hits}}return hits}function readerPaintSearchHighlights(){if(readerRemoveSearchHighlights(),!_readerSearchTerm||!_readerSearchHits.length)return;const scale=readerState.scale||1;_readerSearchHits.forEach((hit,hi)=>{var _a2;const s=readerState.sentences[hit.sentenceIndex];if(s){hit.nodes=[];for(const wi of hit.wordIndexes){const w=(_a2=s.words)==null?void 0:_a2[wi];if(w)if(readerState.mode==="pdf"){const pg=readerState.pages[w.page];if(!(pg!=null&&pg.overlay))continue;const el=document.createElement("div");el.className="reader-search-hit"+(hi===_readerSearchCurrent?" search-current":""),Object.assign(el.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:Math.max(w.w,2)*scale+"px",height:Math.max(w.h,2)*scale+"px"}),pg.overlay.appendChild(el),hit.nodes.push(el)}else w.el&&(w.el.classList.add("search-hit"),hi===_readerSearchCurrent&&w.el.classList.add("search-current"),hit.nodes.push(w.el))}}})}function readerUpdateSearchStatus(){var _a2,_b2;if(!_readerSearchTerm){readerSetSearchStatus("");return}if(!((_a2=readerState.sentences)!=null&&_a2.length)){readerSetSearchStatus("No text loaded");return}if(!_readerSearchHits.length){readerSetSearchStatus("0/0");return}const current=_readerSearchCurrent>=0?_readerSearchCurrent+1:1,hit=_readerSearchHits[Math.max(0,_readerSearchCurrent)],capped=_readerSearchHits.length>=READER_SEARCH_MAX_HITS?"+":"",page=readerState.mode==="pdf"?" \xB7 p."+(((_b2=hit==null?void 0:hit.page)!=null?_b2:0)+1):"";readerSetSearchStatus(current+"/"+_readerSearchHits.length+capped+page)}function readerJumpToSearchHit(idx){var _a2;if(!_readerSearchHits.length)return;_readerSearchCurrent=(idx+_readerSearchHits.length)%_readerSearchHits.length,readerPaintSearchHighlights(),readerUpdateSearchStatus();const hit=_readerSearchHits[_readerSearchCurrent],node=(_a2=hit==null?void 0:hit.nodes)==null?void 0:_a2[0];if(node)readerEnsureVisible(node);else if(readerState.mode==="pdf"){const pg=readerState.pages[hit.page];pg!=null&&pg.pageDiv&&(readerRenderPage(hit.page),pg.pageDiv.scrollIntoView({behavior:"smooth",block:"center"}))}}function readerSearchApply(term,{jump=!0}={}){if(term=String(term||"").trim(),!term){readerClearSearch();return}_readerSearchTerm=term,_readerSearchHits=readerCollectSearchHits(term),_readerSearchCurrent=_readerSearchHits.length?0:-1,readerPaintSearchHighlights(),readerUpdateSearchStatus(),jump&&_readerSearchHits.length&&readerJumpToSearchHit(0)}function readerSearchStep(direction){var _a2;const term=((_a2=$("reader-search"))==null?void 0:_a2.value.trim())||_readerSearchTerm;if(!_readerSearchHits.length||term!==_readerSearchTerm){readerSearchApply(term,{jump:!0});return}readerJumpToSearchHit(_readerSearchCurrent+(direction===-1?-1:1))}(_$b=$("reader-search"))==null||_$b.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),readerSearchStep(e.shiftKey?-1:1)),e.key==="Escape"&&readerClearSearch({clearInput:!0})}),(_ac=$("reader-search"))==null||_ac.addEventListener("input",e=>{clearTimeout(_readerSearchTimer);const term=e.target.value.trim();if(!term){readerClearSearch();return}_readerSearchTimer=setTimeout(()=>readerSearchApply(term,{jump:!0}),160)}),(_bc=$("reader-search-prev"))==null||_bc.addEventListener("click",()=>readerSearchStep(-1)),(_cc=$("reader-search-next"))==null||_cc.addEventListener("click",()=>readerSearchStep(1)),(_dc=$("reader-search-clear"))==null||_dc.addEventListener("click",()=>readerClearSearch({clearInput:!0})),(_ec=$("reader-save-lib"))==null||_ec.addEventListener("click",readerSaveLibrary),(_fc=$("reader-export-btn"))==null||_fc.addEventListener("click",()=>{var _a2;return readerExport(((_a2=$("reader-export-mode"))==null?void 0:_a2.value)||"page")}),(_gc=$("reader-synth-all"))==null||_gc.addEventListener("click",readerSynthAll),(_hc=$("reader-synth-cancel"))==null||_hc.addEventListener("click",()=>{readerState.synthCancel=!0}),(_ic=$("reader-select-toggle"))==null||_ic.addEventListener("click",()=>{readerState.selecting?readerSetSelecting(!1):(readerClearSelection(),readerSetSelecting(!0))}),(_jc=$("reader-sel-clear"))==null||_jc.addEventListener("click",readerClearSelection),document.addEventListener("keydown",e=>{e.key==="Escape"&&readerState.selecting&&readerSetSelecting(!1)}),(_kc=$("reader-page-from"))==null||_kc.addEventListener("change",readerUpdateScopeLabel),(_lc=$("reader-page-to"))==null||_lc.addEventListener("change",readerUpdateScopeLabel),(_mc=$("reader-doc"))==null||_mc.addEventListener("click",e=>{if(!readerState.selecting||readerState.mode!=="pdf")return;const stat=e.target.closest(".reader-stat");if(!stat)return;const si=parseInt(stat.dataset.si);isNaN(si)||readerPickSentence(si)}),(_nc=$("reader-doc"))==null||_nc.addEventListener("scroll",function(){let t=null;return()=>{t||(t=setTimeout(()=>{t=null,readerRenderVisible()},120))}}()),window.addEventListener("resize",function(){let t=null;return()=>{readerState.mode!=="pdf"||readerState.zoomMode==="custom"||(clearTimeout(t),t=setTimeout(()=>readerApplyZoom(readerState.zoomMode),200))}}()),function(){["reader-dropzone","reader-doc"].map(id=>$(id)).filter(Boolean).forEach(el=>{el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.add("dragover"))}),el.addEventListener("dragleave",()=>el.classList.remove("dragover")),el.addEventListener("drop",e=>{var _a2;e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.remove("dragover"),readerImportFile((_a2=e.dataTransfer.files)==null?void 0:_a2[0]))})})}(),window.showReaderView=function(view){const main=$("reader-main-view"),cast=$("reader-audiobook-panel"),chars=$("reader-charsheets-panel"),lib=$("reader-library-card");main&&(main.hidden=view!=="main"),cast&&(cast.hidden=view!=="cast",view!=="cast"&&(cast.style.display="",cast.style.flexDirection="",cast.style.minHeight=""),view==="cast"&&!cast.innerHTML.trim()&&(cast.innerHTML=`
No active casting session
Open a document in the Reader tab and click "Cast as audiobook" to start extracting characters.
This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.
`,chars.className="")),lib&&(lib.hidden=view!=="library",view==="library"&&(lib.classList.remove("card"),lib.style.boxShadow="none",lib.style.border="none",lib.style.background="transparent",readerRenderLibrary())),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs(view==="cast"?"cast":view==="chars"?"chars":"source")},window.navReaderView=function(view){window.showReaderView(view)},window.readerJumpToPage=function(pageNum){const pg=parseInt(pageNum,10);if(!pg)return Promise.resolve(!1);typeof navTo=="function"&&navTo("s-reader"),window.showReaderView("main");let tries=0;const waitFrame=()=>new Promise(resolve=>requestAnimationFrame(resolve)),jump=async()=>{var _a2;await waitFrame();const pageState=(_a2=readerState.pages)==null?void 0:_a2[pg-1],pageDiv=pageState==null?void 0:pageState.pageDiv;return pageDiv&&!pageDiv.hidden?(await readerRenderPage(pg-1),pageDiv.scrollIntoView({behavior:"smooth",block:"start",inline:"nearest"}),readerRenderVisible(),!0):tries++<20?(await new Promise(resolve=>setTimeout(resolve,100)),jump()):(typeof toast=="function"&&toast("Source page is not loaded in Reader yet","info"),!1)};return jump()},window.readerOnShow=async function(){const sel=$("reader-backend-select");if(sel&&(!sel.value||sel.options.length<=1)){if(typeof availableTtsBackends=="function"&&!availableTtsBackends().length&&typeof refreshTtsBackendAvailability=="function")try{await refreshTtsBackendAvailability()}catch{}typeof availableTtsBackends=="function"&&availableTtsBackends().length&&(sel.innerHTML=ttsBackendOptions(sel.value),sel.disabled=!1)}readerUpdateBackendHint(),window.VoicePicker&&VoicePicker.upgrade("reader-voice-select"),readerRenderLibrary(),window._readerStartView&&(window.showReaderView(window._readerStartView),window._readerStartView=null),_readerEnsureImportVisible()};function _readerEnsureImportVisible(){if(readerState.sentences.length)return;const card=document.getElementById("reader-config-card"),body=card?card.querySelector(".card-col-body"):null;body&&body.hidden&&(body.hidden=!1,card.classList.remove("card-col-closed"))}const AUDIOBOOK_CHUNK_CHARS=3e3,AUDIOBOOK_WARMUP_TIMEOUT_MS=24e4,AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS=6e5,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS=3e5,AUDIOBOOK_RECAST_TIMEOUT_MS=24e4,AUDIOBOOK_RECAST_CONTEXT_CHARS=4200,AUDIOBOOK_RECAST_TARGETS_PER_CALL=6,AUDIOBOOK_DRAFT_AUTOSAVE_MS=5*60*1e3,_audiobook={running:!1,cancel:!1};window._audiobook=_audiobook;let _abDraftAutosaveTimer=null,_abLastServerDraftErrorAt=0;async function audiobookFetchWithTimeout(url,options={},timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const parentSignal=options.signal,ac=new AbortController;let timedOut=!1;const timer=setTimeout(()=>{timedOut=!0,ac.abort()},timeoutMs),onParentAbort=()=>ac.abort(parentSignal==null?void 0:parentSignal.reason);parentSignal&&(parentSignal.aborted?onParentAbort():parentSignal.addEventListener("abort",onParentAbort,{once:!0}));try{return await fetch(url,{...options,signal:ac.signal})}catch(err){if(timedOut){const timeoutErr=new Error(`Timed out after ${Math.ceil(timeoutMs/1e3)}s`);throw timeoutErr.name="TimeoutError",timeoutErr}throw err}finally{clearTimeout(timer),parentSignal&&parentSignal.removeEventListener("abort",onParentAbort)}}function audiobookTimeoutSeconds(timeoutMs){return Math.max(5,Math.round(timeoutMs/1e3))}async function audiobookAttributeStream(body,view,outerSignal,idleTimeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/attribute-dialogue/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify({...body,want_reasoning:!0})});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(`
-`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&(view!=null&&view.thinking)&&view.thinking(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}catch(err){throw(err==null?void 0:err.name)==="AbortError"&&!(outerSignal&&outerSignal.aborted)?new Error("stream idle timeout"):err}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}function audiobookDialogueKey(text){return String(text||"").normalize("NFKC").replace(/[»«„“”"‘’'`]+/g,"").replace(/\s+/g," ").trim().toLowerCase()}function audiobookSegmentText(seg){return seg?seg.type==="narration"||!seg.speaker||/^Unknown|Unbekannt/i.test(seg.speaker)?seg.text||"":`"${seg.text||""}"`:""}function _abWordRangeAtPoint(x,y){let node=null,offset=0;if(document.caretRangeFromPoint){const r=document.caretRangeFromPoint(x,y);if(!r)return null;node=r.startContainer,offset=r.startOffset}else if(document.caretPositionFromPoint){const p=document.caretPositionFromPoint(x,y);if(!p)return null;node=p.offsetNode,offset=p.offset}else return null;if(!node||node.nodeType!==3)return null;const text=node.nodeValue||"",isW=ch=>ch!=null&&/[\p{L}\p{N}'’-]/u.test(ch);if(offset>=text.length&&(offset=text.length-1),!isW(text[offset]))if(offset>0&&isW(text[offset-1]))offset--;else return null;let a=offset,b=offset;for(;a>0&&isW(text[a-1]);)a--;for(;b+1!s||s.type==="narration"||/^Unknown|Unbekannt/i.test(s.speaker||""));!cur.length||shortGap&&cur.lengthsegs.slice(start,end).map(audiobookSegmentText).join(" ").trim();let text=render();for(;text.length>AUDIOBOOK_RECAST_CONTEXT_CHARS&&(starttargetEnd)&&(end>targetEnd&&end--,text=render(),!(text.length<=AUDIOBOOK_RECAST_CONTEXT_CHARS));)start=end?"after":"near"}: ${s.speaker}: ${(s.text||"").slice(0,100)}`)}return lines.slice(-12).join(`
+`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&(view!=null&&view.thinking)&&view.thinking(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}catch(err){throw(err==null?void 0:err.name)==="AbortError"&&!(outerSignal&&outerSignal.aborted)?new Error("stream idle timeout"):err}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}function audiobookDialogueKey(text){return String(text||"").normalize("NFKC").replace(/[»«„“”"‘’'`]+/g,"").replace(/\s+/g," ").trim().toLowerCase()}function audiobookSegmentText(seg){return seg?seg.type==="narration"||!seg.speaker||/^Unknown|Unbekannt/i.test(seg.speaker)?seg.text||"":`"${seg.text||""}"`:""}function _abWordRangeAtPoint(x,y){let node=null,offset=0;if(document.caretRangeFromPoint){const r=document.caretRangeFromPoint(x,y);if(!r)return null;node=r.startContainer,offset=r.startOffset}else if(document.caretPositionFromPoint){const p=document.caretPositionFromPoint(x,y);if(!p)return null;node=p.offsetNode,offset=p.offset}else return null;if(!node||node.nodeType!==3)return null;const text=node.nodeValue||"",isW=ch=>ch!=null&&/[\p{L}\p{N}'’-]/u.test(ch);if(offset>=text.length&&(offset=text.length-1),!isW(text[offset]))if(offset>0&&isW(text[offset-1]))offset--;else return null;let a=offset,b=offset;for(;a>0&&isW(text[a-1]);)a--;for(;b+1{var _a3;return String(((_a3=segs[i])==null?void 0:_a3.text)||"").replace(/\s+/g," ").trim()},out=new Set;for(let i=0;i=minScore&&out.add(i)}return out}window._abSuspectNarration=_abSuspectNarration;function audiobookRecastGroups(unknownIdxs,segs){const groups=[];let cur=[];for(const idx of unknownIdxs){const prev=cur.length?cur[cur.length-1]:-1/0,shortGap=idx<=prev+2&&segs.slice(prev+1,idx).every(s=>!s||s.type==="narration"||/^Unknown|Unbekannt/i.test(s.speaker||""));!cur.length||shortGap&&cur.lengthsegs.slice(start,end).map(audiobookSegmentText).join(" ").trim();let text=render();for(;text.length>AUDIOBOOK_RECAST_CONTEXT_CHARS&&(starttargetEnd)&&(end>targetEnd&&end--,text=render(),!(text.length<=AUDIOBOOK_RECAST_CONTEXT_CHARS));)start=end?"after":"near"}: ${s.speaker}: ${(s.text||"").slice(0,100)}`)}return lines.slice(-12).join(`
`)}function audiobookFindReturnedSegment(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;let loose=null;for(let i=0;i=18&&(candKey.includes(targetKey)||targetKey.includes(candKey))&&(loose=loose||{idx:i,seg:cand})}return loose}function audiobookFindReturnedSegmentSequence(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;for(let i=0;ireturned[k])};if(key.length>targetKey.length*1.35+40)break}}return null}function _abStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):String(v)}const _AB_DRAFT_KEY="ttsvc_ab_draft";function _abBookId(){return window.readerState&&readerState.savedId||_audiobook.bookId||null}function _abDraftKey(bookId){return bookId?_AB_DRAFT_KEY+"_"+bookId:_AB_DRAFT_KEY}function _abTextId(text){const s=(text.slice(0,300)+text.slice(-300)).replace(/\s+/g,"");let h=5381;for(let i=0;i>>0;return h.toString(36)+"_"+text.length}function _abSafeFilename(name,fallback="cast"){return(String(name||fallback||"cast").replace(/[\\/:*?"<>|]+/g,"_").replace(/\s+/g,"_").replace(/^_+|_+$/g,"")||fallback||"cast").slice(0,120)}function _abCastMarkdown(data){const payload=data||{},segments=Array.isArray(payload.segments)?payload.segments:[],speakers=[...new Set(segments.filter(s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker).map(s=>String(s.speaker)))].sort(),lines=[`# ${payload.title||"Cast Script"}`,""],metaBits=[];payload.savedAt&&metaBits.push("exported "+new Date(payload.savedAt).toISOString().slice(0,16).replace("T"," ")),metaBits.push(`${speakers.length} character${speakers.length!==1?"s":""}`),metaBits.push(`${segments.length} segment${segments.length!==1?"s":""}`),lines.push(`*${metaBits.join(" \xB7 ")}*`,""),speakers.length&&lines.push(`**Characters:** ${speakers.join(", ")}`,"");let lastPage=null;for(const seg of segments){const text=String((seg==null?void 0:seg.text)||"").trim();if(text){if(seg.page!=null&&seg.page!==lastPage&&(lines.push("---","",`## Page ${seg.page}`,""),lastPage=seg.page),seg.type==="dialogue"){const speaker=String(seg.speaker||"Narrator"),tag=seg.emotion?` *(${seg.emotion})*`:"";lines.push(`**${speaker.toUpperCase()}**${tag}: "${text}"`)}else lines.push(text);lines.push("")}}return lines.join(`
`)}function _abStampSegmentPages(segs,pageMarks,text){if(!Array.isArray(segs)||!segs.length||segs.some(s=>s&&s.page!=null))return;const marks=(pageMarks||[]).slice().sort((a,b)=>(a.offset||0)-(b.offset||0)),src=text||"";if(!marks.length||!src)return;let markIdx=0,searchPos=0,cur=null;marks[0].offset<=2&&(cur=marks[0].page+1,markIdx=1);for(const s of segs){const probe=String(s.text||"").trim().slice(0,24),at=probe?src.indexOf(probe,searchPos):-1,pos=at>=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)cur=marks[markIdx].page+1,markIdx++;cur!=null&&(s.page=cur)}}let _abLastDraftSaveAt=0;const AB_DRAFT_SAVE_THROTTLE_MS=4e3;function _abSaveDraft(segs,roster,text,done,total){var _a2;const isFinal=total>0&&done>=total,now=Date.now();if(!isFinal&&now-_abLastDraftSaveAt{if(!r.ok)throw new Error(r.statusText||`HTTP ${r.status}`)}).catch(err=>{console.warn("[audiobook] server draft autosave failed:",err);const now2=Date.now();now2-_abLastServerDraftErrorAt>6e4&&(_abLastServerDraftErrorAt=now2,typeof toast=="function"&&toast("Server autosave failed. Browser draft is still saved.","error"))})}let _abExportBusy=!1;async function audiobookExportCastMd(){var _a2;if(_abExportBusy)return;_abExportBusy=!0,setTimeout(()=>{_abExportBusy=!1},2e3);const segs=_audiobook.segments||[];if(!segs.length){toast("No cast to export","error");return}const bookId=_abBookId();_audiobook.lastText&&_abSaveDraft(segs,_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0);const title=((_a2=window.readerState)==null?void 0:_a2.title)||_audiobook.title||"audiobook";if(bookId)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast/export`);if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const blob2=await r.blob(),match=(r.headers.get("Content-Disposition")||"").match(/filename="([^"]+)"/i),name=(match==null?void 0:match[1])||`${_abSafeFilename(title)}_cast.zip`;if(typeof readerDownload=="function")readerDownload(blob2,name);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob2),a.download=name,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}toast("Exported cast + character sheets (.zip)","success");return}catch{toast("Server export failed, downloading local cast copy","error")}const payload={bookId,title,textId:_audiobook.lastText?_abTextId(_audiobook.lastText):"",segments:segs,roster:_audiobook.roster||[],pageMarks:_audiobook.pageMarks||[],rehId:_audiobook.rehId||null,done:_audiobook.completedChunks||0,total:_audiobook.completedTotal||0,savedAt:Date.now()},blob=new Blob([_abCastMarkdown(payload)],{type:"text/markdown;charset=utf-8"});if(typeof readerDownload=="function")readerDownload(blob,`${_abSafeFilename(title)}_cast.md`);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=`${_abSafeFilename(title)}_cast.md`,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}}function _abStartDraftAutosave(saveNow){if(_abStopDraftAutosave(),typeof saveNow!="function")return;_abDraftAutosaveTimer=setInterval(saveNow,AUDIOBOOK_DRAFT_AUTOSAVE_MS);const saveOnLeave=()=>saveNow();_audiobook._draftSaveOnLeave=saveOnLeave,window.addEventListener("pagehide",saveOnLeave),window.addEventListener("beforeunload",saveOnLeave)}function _abStopDraftAutosave(){_abDraftAutosaveTimer&&clearInterval(_abDraftAutosaveTimer),_abDraftAutosaveTimer=null,_audiobook._draftSaveOnLeave&&(window.removeEventListener("pagehide",_audiobook._draftSaveOnLeave),window.removeEventListener("beforeunload",_audiobook._draftSaveOnLeave),_audiobook._draftSaveOnLeave=null)}async function _abEnsureLibraryBook(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2;const rs=window.readerState;if(!rs||rs.savedId||!((_a2=rs.sentences)!=null&&_a2.length))return!!(rs!=null&&rs.savedId);if(rs.mode==="pdf"&&!rs.fileBlob)return!1;const meta={title:rs.title||"Untitled",kind:rs.mode,idx:rs.idx,voice:((_b2=$("reader-voice-select"))==null?void 0:_b2.value)||"",backend:((_c2=$("reader-backend-select"))==null?void 0:_c2.value)||"",speed:rs.speed,instruct:((_d2=$("reader-instruct"))==null?void 0:_d2.value.trim())||"",chunkMode:rs.chunkMode,seed:((_e2=$("reader-seed"))==null?void 0:_e2.value.trim())||"",temperature:((_f2=$("reader-temp"))==null?void 0:_f2.value.trim())||"",tts_speed:parseFloat((_g2=$("reader-tts-speed"))==null?void 0:_g2.value)||1,normalize:rs.normalize,sentenceCount:rs.sentences.length,pageCount:((_h2=rs.pages)==null?void 0:_h2.length)||0,synthCount:0,updated:new Date().toISOString()};try{const r=await fetch("/api/reader/docs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(meta)});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);rs.savedId=(await r.json()).id,_audiobook.bookId=rs.savedId;const ext=rs.mode==="pdf"?"pdf":"txt",body=rs.mode==="pdf"?rs.fileBlob:new Blob([rs.docText||audiobookScopeText()||""],{type:"text/plain"});return(await fetch(`/api/reader/docs/${encodeURIComponent(rs.savedId)}/source?ext=${ext}`,{method:"PUT",body})).ok&&(rs.sourceUploaded=!0),typeof readerRenderLibrary=="function"&&readerRenderLibrary(),!0}catch(err){return console.warn("[audiobook] could not create reader-library autosave target:",err),!1}}async function _abLoadDraftServer(bookId){if(!bookId)return null;for(let attempt=0;attempt<2;attempt++)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`);if(!r.ok){if(attempt===0&&(r.status===404||r.status>=500)){await new Promise(resolve=>setTimeout(resolve,650));continue}return null}const d=await r.json();return d&&Array.isArray(d.segments)&&d.segments.length?d:null}catch{attempt===0&&await new Promise(resolve=>setTimeout(resolve,650))}return null}function _abLoadDraft(text){var _a2;const bookId=_abBookId(),textId=_abTextId(text),title=String(((_a2=window.readerState)==null?void 0:_a2.title)||"").trim().toLowerCase();try{if(bookId){const raw2=localStorage.getItem(_abDraftKey(bookId));if(raw2){const d=JSON.parse(raw2);if(d&&Array.isArray(d.segments)&&d.segments.length)return d}}const raw=localStorage.getItem(_AB_DRAFT_KEY);if(raw){const d=JSON.parse(raw);if(d&&Array.isArray(d.segments)&&d.segments.length&&d.textId===textId)return d}let best=null;for(let i=0;ibest.score)&&(best={score,draft:cand})}catch{}}if(best!=null&&best.draft)return best.draft}catch{return null}return null}function _abClearDraft(){const bookId=_abBookId();try{localStorage.removeItem(_abDraftKey(bookId))}catch{}bookId&&fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`,{method:"DELETE"}).catch(()=>{})}const AB_DIALOGUE_RE=/[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/;function audiobookHasDialogue(t){return AB_DIALOGUE_RE.test(t||"")}function audiobookDehyphenate(t){return(t||"").replace(/([a-zäöüß])-\s+(?=[a-zäöüßA-ZÄÖÜ])/g,"$1")}const AB_SPEECH_VERBS="(?:sagte|fragte|rief|antwortete|erwiderte|entgegnete|meinte|fl\xFCsterte|wisperte|raunte|murmelte|brummte|knurrte|br\xFCllte|schrie|stammelte|fauchte|zischte|seufzte|lachte|kicherte|befahl|wiederholte|fuhr\\s+fort|said|asked|replied|answered|whispered|murmured|muttered|shouted|cried|called|exclaimed|added|continued)",AB_NOTNAME=new Set(["Der","Die","Das","Den","Dem","Ein","Eine","Einen","Er","Sie","Es","Ich","Du","Wir","Ihr","Man","Und","Aber","Da","Dann","Doch","So","Nun","Jetzt","The","He","She","It","They","A","An","And","But","Then","Now","Sofort","Pl\xF6tzlich","Endlich","Schlie\xDFlich","Stille","Schweigen","Stimme","Stimmen","Frage","Antwort","Gel\xE4chter","Wieder","Gleich","Sogleich","Langsam","Leise","Laut","Kaum","Vielleicht","Nat\xFCrlich","Wirklich","Ja","Nein","Komm","Warte","Halt","Geh","Hier","Dort","Oben","Unten","Schon","Noch","Auch","Nur","Immer","Nie"]),_AB_NAME="([A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+)",AB_PERSON_NOUNS=new Set(["Mann","Frau","Junge","M\xE4dchen","Alte","Alter","Fremde","Fremder","Krieger","Kriegerin","W\xE4chter","Wache","Soldat","Hauptmann","Ork","Ritter","Magier","Magierin","Zwerg","Elf","Elfe","H\xE4ndler","Wirt","Wirtin","Bauer","Priester","Priesterin","Nachbar","Nachbarin","Sklave","Sklavin","Verweser","Inquisitor","General","K\xF6nig","K\xF6nigin","Prinz","Prinzessin","F\xFCrst","F\xFCrstin","Baron","Baronin","Bote","Diener","Dienerin","Knabe","Kind","Reiter","Reiterin","Bogensch\xFCtze","Schmied","Schmiedin","Heiler","Heilerin","Gelehrte","Gelehrter","Kapit\xE4n","Anf\xFChrer","Anf\xFChrerin"]);function audiobookResolveUnknowns(segs,prevTail,roster){var _a2,_b2;const isUnknown=s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)),isNamed=s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt|Narrator$/i.test(s.speaker),names=(roster||[]).filter(n=>n&&!/^(Narrator|Unknown|Unbekannt)/i.test(n)).sort((a,b)=>b.length-a.length),lastNameIn=text=>{let best=null,bestAt=-1;for(const n of names){const at=text.lastIndexOf(n);at>bestAt&&(bestAt=at,best=n)}if(best)return best;const clause=text.match(new RegExp(_AB_NAME+"[^.!?:]{0,80}\\b(?:und\\s+)?(?:"+AB_SPEECH_VERBS+")[^:]{0,60}:\\s*$"));if(clause&&!AB_NOTNAME.has(clause[1])&&!AB_PERSON_NOUNS.has(clause[1]))return clause[1];const m=[...text.matchAll(/\b(?:[Dd]er|[Dd]ie|[Dd]en|[Dd]em|[Ee]in|[Ee]ine)\s+([A-ZÄÖÜ][a-zäöüß]{2,})\b/g)].map(x=>x[1]).filter(n=>AB_PERSON_NOUNS.has(n));return m.length?m[m.length-1]:null},all=[...prevTail||[],...segs],offset=(prevTail||[]).length,resolved=[];for(let i=offset;i=0&&j>=i-6&&prevDialogues.length<2&&!(((_a2=all[j])==null?void 0:_a2.page)!=null&&s.page!=null&&all[j].page!==s.page);j--)if(((_b2=all[j])==null?void 0:_b2.type)==="dialogue"){if(!isNamed(all[j])){prevDialogues.length=0;break}prevDialogues.push(all[j].speaker)}prevDialogues.length===2&&prevDialogues[0]!==prevDialogues[1]&&(who=prevDialogues[1])}who&&!/^(Narrator|Unknown|Unbekannt)$/i.test(who)&&(s.speaker=who,resolved.push(who))}return resolved}const _AB_TAG_NAME="[A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+",AB_SPEECH_TAG_ONLY_RE=new RegExp("^\\s*[,.;:!?\u2013-]*\\s*(?:(?:"+AB_SPEECH_VERBS+")\\b\\s+(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")\\s+(?:"+AB_SPEECH_VERBS+")\\b)(?:\\s+(?:mit|in|leise|laut|kalt|heiser|erstickt|ruhig|zornig|w\xFCtend|\xE4ngstlich|sp\xF6ttisch|ver\xE4chtlich|fragend|fl\xFCsternd|schrill|dumpf|slowly|coldly|quietly|softly|angrily|hoarsely)\\b[\\s\\S]*)?[.!?\u2026]*\\s*$","i");function audiobookIsSpeechTagOnly(text){const t=String(text||"").trim();return!t||t.length>180||/[»«„“”"‟‚‘’『「]/.test(t)?!1:AB_SPEECH_TAG_ONLY_RE.test(t)}function audiobookGuessSpeaker(after,before){let m;const ok=n=>n&&!AB_NOTNAME.has(n)?n:null;if(m=new RegExp("^[\\s,;\u2013-]*"+AB_SPEECH_VERBS+"\\s+(?:der|die|das|ein|eine)?\\s*"+_AB_NAME).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp("^[\\s,;\u2013-]*"+_AB_NAME+"\\s+"+AB_SPEECH_VERBS).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp(_AB_NAME+"\\s+"+AB_SPEECH_VERBS+"[\\s:,\u2013-]*$").exec(before||"")){const r=ok(m[1]);if(r)return r}return null}const AB_QUOTE_PAIRS={"\xBB":"\xAB","\xAB":"\xBB","\u201E":"\u201C","\u201C":"\u201D",'"':'"',"\u300C":"\u300D","\u300E":"\u300F","\u2018":"\u2019","\u201A":"\u2018"},_AB_UNCLOSED_TAG_RE=new RegExp("^\\s*(?:[,;:\u2013-]\\s*)?(?:"+AB_SPEECH_VERBS+"|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|I|we|you)\\s+"+AB_SPEECH_VERBS+")\\b","i");function _abUnclosedQuoteEnd(raw,closeIdx){const limit=closeIdx>=0?closeIdx:raw.length;for(let i=0;i40)return m[1].length}return closeIdx>=0?closeIdx:raw.length}function _abOrphanClosingQuoteSpan(text,closeIdx,minStart){var _a2;if(!"\xAB\u201D\u2019\u2039\u300D\u300F".includes(text[closeIdx]))return null;const before=text.slice(minStart,closeIdx),prev=((_a2=before.match(/\S(?=\s*$)/))==null?void 0:_a2[0])||"";if(!/[.!?]/.test(prev))return null;const trimmedLen=before.trimEnd().length;let localStart=0;const boundaryRe=/[\n\r]|[.!?:]\s+/g;let m;for(;m=boundaryRe.exec(before);){const next=m.index+m[0].length;nextch&&(AB_QUOTE_OPENERS+AB_QUOTE_CLOSERS).includes(ch);for(let i=0;is.text.trim())}function _audiobookMergeAdjacentSameSpeaker(segments){const out=[];for(const s of segments){const last=out[out.length-1];if(last&&last.type===s.type&&last.page===s.page){const lastSpeaker=(last.speaker||"Narrator").trim().toLowerCase(),curSpeaker=(s.speaker||"Narrator").trim().toLowerCase(),sameEmotion=s.type!=="dialogue"||(last.emotion||"").toLowerCase()===(s.emotion||"").toLowerCase();if(lastSpeaker===curSpeaker&&sameEmotion){const lastText=last.text.trimEnd(),curText=s.text.trimStart();last.text=/[.!?…»«”"’']$/.test(lastText)?lastText+`
@@ -1292,7 +1292,7 @@ ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv \u2014 arbeite w
1. Direkte Zuordnung: "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) IMMER zum tats\xE4chlichen Namen auf: Das Pronomen meint die zuletzt genannte Person passenden Geschlechts.
2. Doppelpunkt-Regel: Endet ein Erz\xE4hlersatz mit ":", spricht das Subjekt dieses Satzes das folgende Zitat (z.B. "Dann richtete er sich auf und rief in die Runde:" \u2192 der zuvor genannte Charakter spricht das n\xE4chste Zitat; "Ein anderer fragte verschlafen:" \u2192 dieser andere spricht).
3. Nachgestellte Zuordnung: Der Erz\xE4hlersatz NACH einem Zitat verr\xE4t oft den Sprecher \u2014 auch bei unpers\xF6nlicher Formel: "\xBBWas machst du denn da?\xAB ert\xF6nte es \xFCber ihm. Karyla hatte ihren Hammer weggelegt und war her\xFCbergekommen." \u2192 Karyla sprach das Zitat.
-4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat handelt, spricht meist (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau).
+4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat AKTIV HANDELT (tritt vor, dreht sich, hebt die Hand), spricht meist \u2014 ABER NUR bei Handlungsverben. Verben der WAHRNEHMUNG oder REAKTION (sp\xFCrte, h\xF6rte, f\xFChlte, sah, blickte, lauschte, bemerkte, nickte) kennzeichnen den ZUH\xD6RER, NICHT den Sprecher (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau).
5. Adressaten-Regel: "X wandte sich an Y" / "X sah Y an" \u2192 X spricht das n\xE4chste Zitat, und Y ist der wahrscheinlichste Antwortende.
6. Das Ping-Pong-Prinzip: Sprechen zwei Personen miteinander, wechseln sie sich strikt ab \u2014 auch \xFCber viele Zitate OHNE Inquit-Formeln hinweg. Verfolge die Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung und f\xFChre sie bis zum Szenenende fort. In einer Zwei-Personen-Szene ist 'Unknown' fast immer falsch.
7. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt sie zum Wissen oder Tonfall eines bestimmten Charakters?
@@ -1301,6 +1301,13 @@ ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv \u2014 arbeite w
10. 'Unknown' NUR, wenn eine Zuordnung trotz aller obigen Regeln absolut unm\xF6glich ist. Bevorzuge immer einen genannten Charakter oder eine Rollenbezeichnung gegen\xFCber 'Unknown'.
11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende \u2014 die Rede geht danach unver\xE4ndert weiter, bis das tats\xE4chliche schlie\xDFende Anf\xFChrungszeichen erscheint.
12. Stimm-Ank\xFCndigung: Erw\xE4hnt ein Erz\xE4hlersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), geh\xF6rt diese Zeile dieser Person \u2014 nicht 'Unknown'.
+13. ZUH\xD6RER-AUSSCHLUSS (negative Evidenz \u2014 sehr wichtig): Ein Name im Erz\xE4hlsatz neben einem Zitat ist H\xC4UFIG der Zuh\xF6rer, nicht der Sprecher. Schlie\xDFe eine Person als Sprecher AUS, wenn sie (a) im Nachbarsatz wahrnimmt oder reagiert (Marcian sp\xFCrte ihre Tr\xE4nen auf seiner Brust; Marcian h\xF6rte dar\xFCber hinweg \u2192 Marcian H\xD6RT ZU, jemand anderes spricht), oder (b) im Zitat selbst ANGEREDET wird (Vokativ nach Komma: M\xF6gen die G\xF6tter dich sch\xFCtzen, Marcian. \u2192 Marcian ist der ANGESPROCHENE). Wer angeredet wird, spricht nicht. Wenn durch diesen Ausschluss kein Kandidat mehr \xFCbrig bleibt, suche WEITER ZUR\xDCCK (letzte eindeutige Nennung, Ping-Pong-Kette, wer in der Szene anwesend ist) \u2014 der Ausschluss grenzt die Kandidaten nur ein und rechtfertigt f\xFCr sich allein NIEMALS 'Unknown'.
+16. FORTSETZUNGS-REGEL (wichtiger als Ping-Pong): Beschreibt der Erzaehlsatz ZWISCHEN zwei Zitaten dieselbe Person, die gerade gesprochen hat (per Pronomen oder Name, z.B. "Leise schluchzte sie.", "Er atmete tief durch."), dann spricht diese Person WEITER \u2014 das folgende Zitat gehoert IHR, nicht dem Gespraechspartner. Das Ping-Pong-Prinzip (Regel 6) gilt NUR, wenn der Erzaehlsatz auf die ANDERE Person zeigt oder gar keinen Hinweis enthaelt. Beispiel: Cindira spricht, dann "Leise schluchzte sie." \u2192 das naechste Zitat ist wieder CINDIRA; ein folgendes "Marcian spuerte ihre Traenen" bestaetigt nur, dass Marcian zuhoert (Regel 13).
+17. NEU AUFTRETENDE, UNBENANNTE HANDELNDE: Fuehrt der Erzaehlsatz unmittelbar vor einem Zitat eine unbenannte Person handelnd ein ("Eine Frau durchbrach die Kette der Krieger und warf sich...", "Ein Soldat trat vor"), dann spricht GENAU DIESE Person. Verwende die Rollenbezeichnung aus dem Text als Sprechernamen ("Eine Frau", "Ein Soldat") \u2014 niemals 'Unknown'. Spricht dieselbe Person spaeter erneut (erkennbar am Pronomen im Erzaehlsatz), nutze exakt DENSELBEN Rollennamen weiter, damit sie eine durchgehende Stimme behaelt.
+18. EPITHETA / BESTIMMTE BESCHREIBUNGEN als Sprecher: Bezeichnungen wie "die Elfe", "der Zauberer", "der Zwerg", "der Hauptmann" meinen fast immer eine BEREITS BEKANNTE Figur. Loese sie auf den kanonischen Namen auf, sobald der Text die Verbindung herstellt (Nyrilla ist die Elfe \u2192 "Laut hallte die Stimme der Elfe durch den Gang." bedeutet: NYRILLA hat gesprochen). Nutze immer den kanonischen Namen als Sprecher, niemals das Epitheton und niemals 'Unknown', damit die Figur durchgehend dieselbe Stimme behaelt.
+19. GEMISCHTE SEGMENTE TRENNEN: Enthaelt ein Segment sowohl Erzaehltext als auch ein Zitat ("Nyrilla richtete sich auf. Alle anderen wichen zurueck. \xBBIch werde jetzt ...\xAB"), dann zerlege es: der Erzaehlteil wird ein eigenes Segment mit speaker 'Narrator' und type 'narration', das Zitat ein eigenes Segment mit type 'dialogue' und der handelnden Person als Sprecher (hier NYRILLA, denn sie richtet sich unmittelbar davor auf \u2014 siehe Regel 4). Ein gemischtes Segment darf niemals als Ganzes 'Unknown' bleiben. ZERLEGE EBENSO Segmente, die REDEBEITRAEGE MEHRERER SPRECHER enthalten: Ein Befehl, der jemanden beim Namen anspricht, und die darauffolgende Antwort sind ZWEI Sprecher, nicht einer (z.B. "Bringt Fackeln ... Lysandra, du nimmst dir einige Krieger ..." = MARCIAN befiehlt; "Mit Vergn\xFCgen, ich hab doch gleich gesagt ..." = LYSANDRA antwortet). Merkmal: Wer im ersten Teil per Vokativ ANGESPROCHEN wird (Regel 13b), ist typischerweise der Sprecher des NAECHSTEN Redebeitrags. Lege fuer jeden Redebeitrag ein eigenes Segment mit dem jeweiligen Sprecher an, statt den ganzen Block als 'Unknown' zu lassen.
+14. GETEILTE ZITATE / Pronomen-Inquit: Beginnt ein Segment mit Komma oder Kleinbuchstaben plus Sprechverb und Pronomen (, antwortete er knapp. / , murmelte sie leise.), ist es die abgetrennte Inquit-Formel des DAVORSTEHENDEN Zitats. Ordne jenes Zitat der Person zu, auf die das Pronomen zeigt: die zuletzt genannte Person passenden Geschlechts VOR dem Zitat (er \u2192 letzter Mann, sie \u2192 letzte Frau). Solche Zeilen d\xFCrfen niemals 'Unknown' bleiben.
+15. MEHRERE KANDIDATEN in der N\xE4he: Stehen mehrere bekannte Namen im Umfeld, entscheide in dieser Reihenfolge: (a) streiche alle nach Regel 13 ausgeschlossenen (Zuh\xF6rer/Angeredete); (b) bevorzuge, wer grammatisches SUBJEKT eines Sprech- oder Handlungsverbs ist, gegen\xFCber jemandem in Objekt- oder Pr\xE4positionalstellung; (c) pr\xFCfe, wessen Wissensstand, Rolle und Sprechweise zum Inhalt passt; (d) nutze das Ping-Pong-Prinzip aus Regel 6. Erst wenn danach zwei gleichwertige Kandidaten bleiben, ist 'Unknown' zul\xE4ssig.
F\xDCR JEDES SEGMENT GIBST DU FOLGENDES AUS:
- speaker: 'Narrator' f\xFCr Narration/Erz\xE4hlertext, oder den EXAKTEN Namen des Charakters f\xFCr gesprochene Dialoge.
@@ -1327,7 +1334,7 @@ STRIKTE FORMAT- UND TEXTREGELN:
1. Direkte Zuordnung: "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) IMMER zum tats\xE4chlichen Namen auf: Das Pronomen meint die zuletzt genannte Person passenden Geschlechts.
2. Doppelpunkt-Regel: Endet ein Erz\xE4hlersatz mit ":", spricht das Subjekt dieses Satzes das folgende Zitat (z.B. "Dann richtete er sich auf und rief in die Runde:" \u2192 der zuvor genannte Charakter spricht das n\xE4chste Zitat; "Ein anderer fragte verschlafen:" \u2192 dieser andere spricht).
3. Nachgestellte Zuordnung: Der Erz\xE4hlersatz NACH einem Zitat verr\xE4t oft den Sprecher \u2014 auch bei unpers\xF6nlicher Formel: "\xBBWas machst du denn da?\xAB ert\xF6nte es \xFCber ihm. Karyla hatte ihren Hammer weggelegt und war her\xFCbergekommen." \u2192 Karyla sprach das Zitat.
-4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat handelt, spricht meist (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau).
+4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat AKTIV HANDELT (tritt vor, dreht sich, hebt die Hand), spricht meist \u2014 ABER NUR bei Handlungsverben. Verben der WAHRNEHMUNG oder REAKTION (sp\xFCrte, h\xF6rte, f\xFChlte, sah, blickte, lauschte, bemerkte, nickte) kennzeichnen den ZUH\xD6RER, NICHT den Sprecher (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau).
5. Adressaten-Regel: "X wandte sich an Y" / "X sah Y an" \u2192 X spricht das n\xE4chste Zitat, und Y ist der wahrscheinlichste Antwortende.
6. Das Ping-Pong-Prinzip: Sprechen zwei Personen miteinander, wechseln sie sich strikt ab \u2014 auch \xFCber viele Zitate OHNE Inquit-Formeln hinweg. Verfolge die Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung und f\xFChre sie bis zum Szenenende fort. In einer Zwei-Personen-Szene ist 'Unknown' fast immer falsch.
7. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt sie zum Wissen oder Tonfall eines bestimmten Charakters?
@@ -1342,8 +1349,15 @@ GRAMMATIK-REGELN F\xDCR NARRATION:
- Satzfragmente mit f\xFChrendem Komma/Punkt wie ", sagte sie leise." oder ". fragte Uriens." sind niemals eigenst\xE4ndige Dialoge; sie geh\xF6ren als narration zum Erz\xE4hlertext.`)),/ABSATZ- UND KAPITELSTRUKTUR/.test(p)||(p=p.replace('Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).',`Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).
ABSATZ- UND KAPITELSTRUKTUR:
-Eine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/Szenenanfang). Eine sehr kurze, alleinstehende Zeile direkt vor einer Leerzeile (z.B. "1. Kapitel", "Prolog", ein Zahlwort) ist eine Kapitel\xFCberschrift \u2014 immer 'narration'/'Narrator', niemals Dialog. Behalte Leerzeilen als eigenst\xE4ndige narration-Segmente oder als Teil des umgebenden Erz\xE4hler-Segments bei; erfinde daraus keinen Dialog und l\xF6sche sie nicht aus dem rekonstruierten Text.`)),!/Gedankenstrich-Pause-Regel/.test(p)){const newRules=`11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende \u2014 die Rede geht danach unver\xE4ndert weiter, bis das tats\xE4chliche schlie\xDFende Anf\xFChrungszeichen erscheint.
-12. Stimm-Ank\xFCndigung: Erw\xE4hnt ein Erz\xE4hlersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), geh\xF6rt diese Zeile dieser Person \u2014 nicht 'Unknown'.`,unknownRuleRe=/(\n\d+\. 'Unknown' NUR,[^\n]*'Unknown'\.)/;unknownRuleRe.test(p)?p=p.replace(unknownRuleRe,`$1
+Eine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/Szenenanfang). Eine sehr kurze, alleinstehende Zeile direkt vor einer Leerzeile (z.B. "1. Kapitel", "Prolog", ein Zahlwort) ist eine Kapitel\xFCberschrift \u2014 immer 'narration'/'Narrator', niemals Dialog. Behalte Leerzeilen als eigenst\xE4ndige narration-Segmente oder als Teil des umgebenden Erz\xE4hler-Segments bei; erfinde daraus keinen Dialog und l\xF6sche sie nicht aus dem rekonstruierten Text.`)),!/Gedankenstrich-Pause-Regel/.test(p)||!/ZUHÖRER-AUSSCHLUSS/.test(p)||!/FORTSETZUNGS-REGEL/.test(p)||!/EPITHETA/.test(p)){const newRules=`11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende \u2014 die Rede geht danach unver\xE4ndert weiter, bis das tats\xE4chliche schlie\xDFende Anf\xFChrungszeichen erscheint.
+12. Stimm-Ank\xFCndigung: Erw\xE4hnt ein Erz\xE4hlersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), geh\xF6rt diese Zeile dieser Person \u2014 nicht 'Unknown'.
+13. ZUH\xD6RER-AUSSCHLUSS (negative Evidenz \u2014 sehr wichtig): Ein Name im Erz\xE4hlsatz neben einem Zitat ist H\xC4UFIG der Zuh\xF6rer, nicht der Sprecher. Schlie\xDFe eine Person als Sprecher AUS, wenn sie (a) im Nachbarsatz wahrnimmt oder reagiert (Marcian sp\xFCrte ihre Tr\xE4nen auf seiner Brust; Marcian h\xF6rte dar\xFCber hinweg \u2192 Marcian H\xD6RT ZU, jemand anderes spricht), oder (b) im Zitat selbst ANGEREDET wird (Vokativ nach Komma: M\xF6gen die G\xF6tter dich sch\xFCtzen, Marcian. \u2192 Marcian ist der ANGESPROCHENE). Wer angeredet wird, spricht nicht. Wenn durch diesen Ausschluss kein Kandidat mehr \xFCbrig bleibt, suche WEITER ZUR\xDCCK (letzte eindeutige Nennung, Ping-Pong-Kette, wer in der Szene anwesend ist) \u2014 der Ausschluss grenzt die Kandidaten nur ein und rechtfertigt f\xFCr sich allein NIEMALS 'Unknown'.
+16. FORTSETZUNGS-REGEL (wichtiger als Ping-Pong): Beschreibt der Erzaehlsatz ZWISCHEN zwei Zitaten dieselbe Person, die gerade gesprochen hat (per Pronomen oder Name, z.B. "Leise schluchzte sie.", "Er atmete tief durch."), dann spricht diese Person WEITER \u2014 das folgende Zitat gehoert IHR, nicht dem Gespraechspartner. Das Ping-Pong-Prinzip (Regel 6) gilt NUR, wenn der Erzaehlsatz auf die ANDERE Person zeigt oder gar keinen Hinweis enthaelt. Beispiel: Cindira spricht, dann "Leise schluchzte sie." \u2192 das naechste Zitat ist wieder CINDIRA; ein folgendes "Marcian spuerte ihre Traenen" bestaetigt nur, dass Marcian zuhoert (Regel 13).
+17. NEU AUFTRETENDE, UNBENANNTE HANDELNDE: Fuehrt der Erzaehlsatz unmittelbar vor einem Zitat eine unbenannte Person handelnd ein ("Eine Frau durchbrach die Kette der Krieger und warf sich...", "Ein Soldat trat vor"), dann spricht GENAU DIESE Person. Verwende die Rollenbezeichnung aus dem Text als Sprechernamen ("Eine Frau", "Ein Soldat") \u2014 niemals 'Unknown'. Spricht dieselbe Person spaeter erneut (erkennbar am Pronomen im Erzaehlsatz), nutze exakt DENSELBEN Rollennamen weiter, damit sie eine durchgehende Stimme behaelt.
+18. EPITHETA / BESTIMMTE BESCHREIBUNGEN als Sprecher: Bezeichnungen wie "die Elfe", "der Zauberer", "der Zwerg", "der Hauptmann" meinen fast immer eine BEREITS BEKANNTE Figur. Loese sie auf den kanonischen Namen auf, sobald der Text die Verbindung herstellt (Nyrilla ist die Elfe \u2192 "Laut hallte die Stimme der Elfe durch den Gang." bedeutet: NYRILLA hat gesprochen). Nutze immer den kanonischen Namen als Sprecher, niemals das Epitheton und niemals 'Unknown', damit die Figur durchgehend dieselbe Stimme behaelt.
+19. GEMISCHTE SEGMENTE TRENNEN: Enthaelt ein Segment sowohl Erzaehltext als auch ein Zitat ("Nyrilla richtete sich auf. Alle anderen wichen zurueck. \xBBIch werde jetzt ...\xAB"), dann zerlege es: der Erzaehlteil wird ein eigenes Segment mit speaker 'Narrator' und type 'narration', das Zitat ein eigenes Segment mit type 'dialogue' und der handelnden Person als Sprecher (hier NYRILLA, denn sie richtet sich unmittelbar davor auf \u2014 siehe Regel 4). Ein gemischtes Segment darf niemals als Ganzes 'Unknown' bleiben. ZERLEGE EBENSO Segmente, die REDEBEITRAEGE MEHRERER SPRECHER enthalten: Ein Befehl, der jemanden beim Namen anspricht, und die darauffolgende Antwort sind ZWEI Sprecher, nicht einer (z.B. "Bringt Fackeln ... Lysandra, du nimmst dir einige Krieger ..." = MARCIAN befiehlt; "Mit Vergn\xFCgen, ich hab doch gleich gesagt ..." = LYSANDRA antwortet). Merkmal: Wer im ersten Teil per Vokativ ANGESPROCHEN wird (Regel 13b), ist typischerweise der Sprecher des NAECHSTEN Redebeitrags. Lege fuer jeden Redebeitrag ein eigenes Segment mit dem jeweiligen Sprecher an, statt den ganzen Block als 'Unknown' zu lassen.
+14. GETEILTE ZITATE / Pronomen-Inquit: Beginnt ein Segment mit Komma oder Kleinbuchstaben plus Sprechverb und Pronomen (, antwortete er knapp. / , murmelte sie leise.), ist es die abgetrennte Inquit-Formel des DAVORSTEHENDEN Zitats. Ordne jenes Zitat der Person zu, auf die das Pronomen zeigt: die zuletzt genannte Person passenden Geschlechts VOR dem Zitat (er \u2192 letzter Mann, sie \u2192 letzte Frau). Solche Zeilen d\xFCrfen niemals 'Unknown' bleiben.
+15. MEHRERE KANDIDATEN in der N\xE4he: Stehen mehrere bekannte Namen im Umfeld, entscheide in dieser Reihenfolge: (a) streiche alle nach Regel 13 ausgeschlossenen (Zuh\xF6rer/Angeredete); (b) bevorzuge, wer grammatisches SUBJEKT eines Sprech- oder Handlungsverbs ist, gegen\xFCber jemandem in Objekt- oder Pr\xE4positionalstellung; (c) pr\xFCfe, wessen Wissensstand, Rolle und Sprechweise zum Inhalt passt; (d) nutze das Ping-Pong-Prinzip aus Regel 6. Erst wenn danach zwei gleichwertige Kandidaten bleiben, ist 'Unknown' zul\xE4ssig.`,unknownRuleRe=/(\n\d+\. 'Unknown' NUR,[^\n]*'Unknown'\.)/;unknownRuleRe.test(p)?p=p.replace(unknownRuleRe,`$1
${newRules}`):p+=`
${newRules}`}return/gehören IMMER zum Dialog-Segment/.test(p)||(p=p.replace("- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!",`- Anf\xFChrungszeichen geh\xF6ren IMMER zum Dialog-Segment, NIEMALS zum Narration-Segment davor oder danach: Das schlie\xDFende \xAB am Ende einer Figurenrede geh\xF6rt ans ENDE des dialogue-Segments, nicht an den Anfang des folgenden narration-Segments. Das \xF6ffnende \xBB am Anfang einer Figurenrede geh\xF6rt an den ANFANG des dialogue-Segments, nicht ans Ende des vorherigen narration-Segments. Ein narration-Segment darf NIE mit einem einzelnen \xBB, \u201E, \u201A oder \u203A beginnen oder enden, und NIE mit einem einzelnen \xAB, ", ' oder \u2039 enden oder beginnen \u2014 verschiebe das Zeichen ins richtige Nachbar-Segment.
- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!`)),p},rawPrompt=typeof _appSettings!="undefined"&&_appSettings.audiobook_prompt?_appSettings.audiobook_prompt:AB_DEFAULT_PROMPT,globalPrompt=normalizeCastingPrompt(rawPrompt);globalPrompt!==rawPrompt&&typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=globalPrompt,fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:globalPrompt})}).catch(()=>{})),panel.querySelector("#ab-cv-prompt-text").value=globalPrompt;const closePanel=()=>{if(panel.hidden=!0,panel.innerHTML="",typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1),typeof window.navReaderView=="function")window.navReaderView("main");else if(typeof window.showReaderView=="function")window.showReaderView("main");else{const mv=document.getElementById("reader-main-view");mv&&(mv.hidden=!1)}},setStoppingState=()=>{const cancelBtn=panel.querySelector("#ab-cv-cancel");cancelBtn&&(cancelBtn.disabled=!0,cancelBtn.innerHTML=' Stopping...',cancelBtn.title="Stopping the current casting request");const status2=panel.querySelector("#ab-cv-status-msg");status2&&(status2.style.display="inline-block",status2.textContent="Stopping... completed passages will stay saved.")};panel.querySelector("#ab-cv-cancel").addEventListener("click",()=>{if(_audiobook.running){_audiobook.cancel=!0,setStoppingState(),typeof _audiobook.abort=="function"&&_audiobook.abort();return}closePanel()}),panel.querySelector("#ab-cv-prompt-btn").addEventListener("click",()=>{const p=panel.querySelector("#ab-cv-prompt-panel");p.hidden=!p.hidden;const chevron=panel.querySelector("#ab-cv-prompt-chevron");chevron&&(chevron.className=p.hidden?"mdi mdi-chevron-down":"mdi mdi-chevron-up")}),function(){const tBtn=panel.querySelector("#ab-cv-settings-toggle"),sBox=panel.querySelector("#ab-cv-settings"),chev=panel.querySelector("#ab-cv-settings-chevron");function apply(collapsed2){if(sBox&&(sBox.style.display=collapsed2?"none":"flex"),collapsed2){const p=panel.querySelector("#ab-cv-prompt-panel");p&&(p.hidden=!0)}chev&&(chev.className="mdi "+(collapsed2?"mdi-chevron-down":"mdi-chevron-up"))}let collapsed=!1;try{collapsed=localStorage.getItem("ttsvc_ab_settings_collapsed")==="1"}catch{}apply(collapsed),tBtn&&tBtn.addEventListener("click",()=>{collapsed=!collapsed;try{localStorage.setItem("ttsvc_ab_settings_collapsed",collapsed?"1":"0")}catch{}apply(collapsed)})}(),function(){const side=panel.querySelector("#ab-cv-side"),body=panel.querySelector(".ab-cv-body"),cBtn=panel.querySelector("#ab-cv-side-collapse");function apply(collapsed2){side&&side.classList.toggle("is-collapsed",collapsed2),body&&body.classList.toggle("side-collapsed",collapsed2),cBtn&&(cBtn.querySelector(".mdi").className="mdi "+(collapsed2?"mdi-chevron-left":"mdi-chevron-right")),cBtn&&(cBtn.title=collapsed2?"Expand character list":"Collapse to avatars")}let collapsed=!1;try{collapsed=localStorage.getItem("ttsvc_ab_side_collapsed")==="1"}catch{}apply(collapsed),cBtn&&cBtn.addEventListener("click",()=>{collapsed=!collapsed;try{localStorage.setItem("ttsvc_ab_side_collapsed",collapsed?"1":"0")}catch{}apply(collapsed)})}();let savedPrompts=[];try{savedPrompts=JSON.parse(localStorage.getItem("ttsvc_ab_prompts")||"[]")}catch{savedPrompts=[]}const libSelect=panel.querySelector("#ab-cv-prompt-lib"),delBtn=panel.querySelector("#ab-cv-prompt-del"),promptText=panel.querySelector("#ab-cv-prompt-text"),renderPromptLib=(selectedIdx=-1)=>{libSelect.innerHTML=''+savedPrompts.map((p,i)=>``).join(""),selectedIdx>=0?(libSelect.value=selectedIdx,delBtn.style.display="block"):(libSelect.value="",delBtn.style.display="none")};renderPromptLib(),libSelect.addEventListener("change",()=>{const idx=parseInt(libSelect.value),nameInput=panel.querySelector("#ab-cv-prompt-name");!isNaN(idx)&&savedPrompts[idx]?(promptText.value=savedPrompts[idx].prompt,nameInput&&(nameInput.value=savedPrompts[idx].name),delBtn.style.display="block"):(nameInput&&(nameInput.value=""),delBtn.style.display="none")}),delBtn.addEventListener("click",()=>{const idx=parseInt(libSelect.value);isNaN(idx)||confirm("Delete this saved prompt preset?")&&(savedPrompts.splice(idx,1),localStorage.setItem("ttsvc_ab_prompts",JSON.stringify(savedPrompts)),renderPromptLib(),toast("Prompt deleted","success"))}),panel.querySelector("#ab-cv-prompt-save").addEventListener("click",async()=>{const val=promptText.value.trim();if(!val){toast("Prompt is empty","error");return}const name=panel.querySelector("#ab-cv-prompt-name").value.trim()||"Custom Prompt "+(savedPrompts.length+1);let targetIdx=parseInt(libSelect.value);!isNaN(targetIdx)&&savedPrompts[targetIdx]&&savedPrompts[targetIdx].name===name?savedPrompts[targetIdx].prompt=val:(savedPrompts.push({name,prompt:val}),targetIdx=savedPrompts.length-1),localStorage.setItem("ttsvc_ab_prompts",JSON.stringify(savedPrompts)),renderPromptLib(targetIdx),toast("Prompt preset saved","success"),typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=val);try{await fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:val})})}catch{}}),(()=>{const localSel=panel.querySelector("#ab-cv-llm-url"),globalSel=document.getElementById("llm-active-url");if(globalSel&&localSel){const currentVal=localSel.value;localSel.innerHTML=globalSel.innerHTML,localSel.value=currentVal||globalSel.value||""}})();const loadModels=()=>{const urlInput2=panel.querySelector("#ab-cv-llm-url"),sel=panel.querySelector("#ab-cv-llm-select");if(!sel||!urlInput2)return;const currentUrl=urlInput2.value.trim(),oldVal=sel.value;sel.innerHTML='',fetch("/api/conversation/llm-models"+(currentUrl?"?url="+encodeURIComponent(currentUrl):"")).then(r=>r.json()).then(d=>{if(d.models&&d.models.length){const preferred=audiobookSafeLlmModel(oldVal||defaultModel);sel.innerHTML=d.models.map(m=>``).join(""),preferred&&d.models.includes(preferred)?sel.value=preferred:oldVal&&d.models.includes(oldVal)&&!audiobookIsRouterModel(oldVal)?sel.value=oldVal:d.models.includes(defaultModel)&&(sel.value=defaultModel)}else sel.innerHTML=``}).catch(()=>{sel.innerHTML=``})};loadModels(),panel.querySelector("#ab-cv-llm-refresh").addEventListener("click",loadModels);const urlInput=panel.querySelector("#ab-cv-llm-url");urlInput&&urlInput.addEventListener("change",loadModels);const applyPromptAndRun=callback=>{const newPrompt=panel.querySelector("#ab-cv-prompt-text").value,choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt})}).finally(()=>{callback&&callback(savedChoice.url,savedChoice.model)})};if(isIdle){const hasSegs=_audiobook.segments&&_audiobook.segments.length>0;if(panel.querySelector("#ab-cv-status-msg").style.display="inline-block",hasSegs){const recastUnkBtn=panel.querySelector("#ab-cv-start-recast-unk"),recastBtn=panel.querySelector("#ab-cv-start-recast");recastUnkBtn.style.display="inline-block",recastBtn.style.display="inline-block",recastUnkBtn.addEventListener("click",()=>applyPromptAndRun(audiobookRecastUnknown)),recastBtn.addEventListener("click",async()=>{await confirmDialog("Start from scratch? This will discard the current cast for this book and rebuild the character definitions from the text.",{title:"Discard current cast?",okLabel:"Start from scratch",danger:!0})&&applyPromptAndRun(audiobookCast)}),panel.querySelector("#ab-cv-status-msg").textContent="Ready to identify characters."}else{const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn.style.display="inline-block",castBtn.addEventListener("click",()=>applyPromptAndRun(audiobookCast))}}else panel.querySelector("#ab-cv-cancel").innerHTML=' Stop Casting';const fill=panel.querySelector("#ab-cv-fill"),count=panel.querySelector("#ab-cv-count"),feed=panel.querySelector("#ab-cv-feed"),chars=panel.querySelector("#ab-cv-chars"),roster=new Map,characterRecords=new Map,identityNames=recOrSheet=>{const s=(recOrSheet==null?void 0:recOrSheet.sheet)||recOrSheet||{},out=new Set,add=(v,opts={})=>clSplitIdentityTokens(v,opts).forEach(x=>out.add(x));return add((recOrSheet==null?void 0:recOrSheet.name)||s.name),["aliases","first_name","last_name","full_name","title"].forEach(k=>add(s[k],{aliases:k==="aliases"})),[...out]};let _hlVer=0,_hlCache={ver:-1,rosterSize:-1,regex:null,byLower:new Map};const registerCharacterRecord=rec=>{if(rec!=null&&rec.name){_hlVer++;for(const n of identityNames(rec))characterRecords.set(n.toLowerCase(),rec)}},recordForName=name=>characterRecords.get(String(name||"").toLowerCase())||null,colorFor=(name,rec)=>{const key=String(name||"").toLowerCase(),stored=rec||characterRecords.get(key),color=stored?_abRecordColor(stored,name):_abDefaultCharacterColor(name);return roster.has(name)?roster.get(name).color=color:roster.set(name,{count:0,color}),roster.get(name).color},_abAvatarHtml=(name,color)=>{var _a3;const img=(_a3=recordForName(name))==null?void 0:_a3.image,c=color||colorFor(name);return img?``:`${escHtml((name||"?")[0].toUpperCase())}`},setCharacterColor=(name,color)=>{const safe=_abNormalizeColor(color,name),info=roster.get(name);info&&(info.color=safe);const key=String(name||"").toLowerCase(),rec=characterRecords.get(key);return rec&&(rec.color=safe,rec.sheet&&(rec.sheet.color=safe)),safe},highlightText2=text=>{if(!text)return"";let html=escHtml(text);if(_hlCache.ver!==_hlVer||_hlCache.rosterSize!==roster.size){const extraNames=[];new Set([...characterRecords.values()]).forEach(rec=>extraNames.push(...identityNames(rec)));const seen=new Set,names=[];for(const n of[...roster.keys(),...extraNames]){const k=n.toLowerCase();n.length<2||k==="narrator"||/^unknown|unbekannt/i.test(k)||seen.has(k)||(seen.add(k),names.push(n))}names.sort((a,b)=>b.length-a.length);const pattern=names.map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|");_hlCache={ver:_hlVer,rosterSize:roster.size,regex:pattern?new RegExp(`\\b(${pattern})\\b`,"gi"):null,byLower:new Map(names.map(n=>[n.toLowerCase(),n]))}}return _hlCache.regex&&(html=html.replace(_hlCache.regex,match=>{const canon=_hlCache.byLower.get(match.toLowerCase())||match;return`${match}`})),html},feedWrap=panel.querySelector(".ab-cv-feed-wrap"),jumpBtn=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=`
@@ -1442,7 +1456,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 runVerificationPass=()=>{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(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?".
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.
@@ -1453,7 +1467,9 @@ WORAUF DU PR\xDCFST \u2014 IN DIESER REIHENFOLGE (h\xF6chste Priorit\xE4t zuerst
DEDUKTIONS-WERKZEUGE f\xFCr die Sprecherzuordnung:
- Doppelpunkt-Regel: Endet der Satz davor mit ":", spricht dessen Subjekt das Folgende.
- Nachgestellte Zuordnung: Folgt einer als 'Narrator' markierten Zeile direkt eine kurze Inquit-Formel (Sprechverb + Name/Pronomen, z.B. "entgegnete Oberst von Blautann.", ", meldete sich Lysandra zu Wort.", "murmelte er."), dann war die VORHERIGE Zeile in Wahrheit das Zitat dieser Person \u2014 auch ohne Anf\xFChrungszeichen. Das ist der h\xE4ufigste Fehler der ersten Zuordnung: gesprochene Rede wird f\xE4lschlich als Narration markiert, weil die Anf\xFChrungszeichen bei der Extraktion verloren gingen.
-- Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einer fraglichen Zeile handelt (aufblickt, sich umdreht, den Kopf sch\xFCttelt), ist der wahrscheinlichste Sprecher dieser Zeile.
+- Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einer fraglichen Zeile AKTIV HANDELT (aufblickt, sich umdreht, den Kopf sch\xFCttelt), ist der wahrscheinlichste Sprecher dieser Zeile.
+- ZUH\xD6RER-AUSSCHLUSS (negative Evidenz): Wahrnehmungs- und Reaktionsverben kennzeichnen den ZUH\xD6RER, nicht den Sprecher ("Marcian sp\xFCrte ihre Tr\xE4nen", "Marcian h\xF6rte dar\xFCber hinweg" \u2192 Marcian h\xF6rt zu, jemand anderes spricht). Ebenso ist eine Person, die im Zitat per Vokativ ANGEREDET wird ("..., Marcian."), nicht der Sprecher. Pr\xFCfe diese Ausschl\xFCsse ZUERST, bevor du einen Namen aus dem Nachbarsatz \xFCbernimmst \u2014 das ist die h\xE4ufigste Fehlerquelle bei nicht zugeordneten Zeilen.
+- Pronomen-Inquit bei geteilten Zitaten: Eine Zeile wie ", antwortete er knapp." geh\xF6rt zum vorherigen Zitat; l\xF6se das Pronomen auf die zuletzt genannte Person passenden Geschlechts VOR dem Zitat auf (er \u2192 letzter Mann, sie \u2192 letzte Frau).
- Ping-Pong-Prinzip: Zwei Personen im Gespr\xE4ch wechseln sich strikt ab, auch ohne Tags \u2014 verfolge die Kette zur letzten eindeutigen Nennung zur\xFCck.
- Pronomen-Aufl\xF6sung: er/sie/es meint die zuletzt genannte Person passenden Geschlechts.
- Rollenbezeichnungen sind g\xFCltige Sprecher (z.B. 'Ork', 'Der Fremde', 'W\xE4chter') statt 'Unknown'.
@@ -1468,12 +1484,12 @@ 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.`,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),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.`,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.
-${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:"Identify all characters",title:"Scan the text and build the cast list from scratch",onClick:runIdentifyAll,danger:!0},{icon:"mdi-account-question-outline",label:"Identify unknown characters",title:"Re-scan only the unknown segments with the current prompt",onClick:runIdentifyUnknown},{icon:"mdi-shield-check-outline",label:"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:"Check voice consistency",title:"Third-pass check: gathers every line already credited to each character across the whole book and flags any that don\u2019t match their established voice",onClick:runConsistencyPass},{icon:"mdi-repeat",label:"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)}}])),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=[];for(let i=0;iac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang();let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel());const promptOverride=typeof options.prompt=="string"?options.prompt:null,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);!_audiobook.cancel&&afterUnknownCount>beforeUnknownCount&&(segs.splice(0,segs.length,...originalSegments),view.note(`Quality run rolled back: Unknown segments increased from ${beforeUnknownCount} to ${afterUnknownCount}. Existing cast preserved.`),toast("Quality run 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")}}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,{includeNarrator:!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);if(_applyDraft(_localDraft,view,"local"),_abBookId()){const serverCopy={..._localDraft,bookId:_abBookId(),title:((_a2=window.readerState)==null?void 0:_a2.title)||_localDraft.title||""};fetch(`/api/reader/docs/${encodeURIComponent(_abBookId())}/scripts/cast`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(serverCopy)}).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(`
+${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:"Identify all characters",title:"Scan the text and build the cast list from scratch",onClick:runIdentifyAll,danger:!0},{icon:"mdi-comment-alert-outline",label:"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:"Identify unknown characters",title:"Re-scan only the unknown segments with the current prompt",onClick:runIdentifyUnknown},{icon:"mdi-auto-fix",label:"Auto-repair cast",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-repeat",label:"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:"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:"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}])),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());const promptOverride=typeof options.prompt=="string"?options.prompt:null,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(`Still ${prev} unknown \u2014 running full verification pass\u2026`,"info"),await audiobookRecastUnknown(null,null,{includeNarrator:!0});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);if(_applyDraft(_localDraft,view,"local"),_abBookId()){const serverCopy={..._localDraft,bookId:_abBookId(),title:((_a2=window.readerState)==null?void 0:_a2.title)||_localDraft.title||""};fetch(`/api/reader/docs/${encodeURIComponent(_abBookId())}/scripts/cast`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(serverCopy)}).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()+`
-`+s.text.trimStart():last.text=last.text.trimEnd()+" "+s.text.trimStart();continue}}mergedSegs.push(s)}let respiltSegs=[];for(const s of mergedSegs)s.type==="narration"&&audiobookHasDialogue(s.text)?respiltSegs.push(...audiobookSplitByQuotes(s.text)):respiltSegs.push(s);segs=respiltSegs}}catch(chunkErr){if(chunkErr.name==="AbortError")throw chunkErr;degraded++,segs=audiobookSplitByQuotes(chunks[i]),view.note(`\u274C Passage ${i+1} \u2014 unexpected error (${chunkErr.message}) \u2014 auto-detected dialogue instead`)}audiobookResolveUnknowns(segs,allSegments2.slice(-2),roster),segs.forEach(s=>{const speakerName=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator"?"Narrator":s.speaker;!/^Unknown|Unbekannt/i.test(speakerName)&&!roster.includes(speakerName)&&roster.push(speakerName)}),segs.forEach(s=>{s.page=_curPageNum,allSegments2.push(s)}),view.addSegments(segs),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)}view.update(_audiobook.cancel?completedChunks:chunks.length)}catch(err){err.name==="AbortError"?(_audiobook.cancel=!0,view.note("Casting stopped. Completed passages were preserved.")):(_audiobook.cancel=!0,view.note(`\u274C Casting stopped \u2014 unexpected error: ${err.message}`))}finally{_abStopDraftAutosave(),_audiobook.running=!1,_audiobook.abort=null}if(!allSegments2.length){if(_audiobook.cancel){view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}view.done(),toast("No segments produced","error");return}try{const _quoteFixedSegs=_audiobookFixOrphanedQuoteMarks(allSegments2),_mergedSegs=_audiobookMergeAdjacentSameSpeaker(_quoteFixedSegs);allSegments2.length=0,allSegments2.push(..._mergedSegs),_audiobook.segments=allSegments2,_audiobook.lastText=text,_audiobook.roster=roster,_audiobook.narratedPassages=narrationOnly,_audiobook.degraded=degraded,_audiobook.completedChunks=_audiobook.cancel?completedChunks:chunks.length,_audiobook.completedTotal=chunks.length,_abSaveDraft(allSegments2,roster,text,_audiobook.completedChunks,chunks.length),audiobookSaveAsRehearsal({silent:!0}),_audiobook.cancel&&toast("Casting stopped early. Progress preserved.","info");const speakers=new Set(allSegments2.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${_audiobook.cancel&&completedChunks{}),!0}catch(err){console.warn("[audiobook] failed to build rehearser record directly, falling back:",err)}await audiobookSaveAsRehearsal({silent:!0});const{script,emotions}=audiobookBuildScript(segs);return await audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),!0}function audiobookShowPreview(){var _a2;audiobookOpenCurrentInRehearser()}function audiobookApplyPreviewAndOpen(){const segs=_audiobook.segments;document.querySelectorAll("#audiobook-seglist .audiobook-seg-sp").forEach(inp=>{const i=+inp.dataset.i,v=inp.value.trim()||"Narrator";segs[i].speaker=v,segs[i].type=v.toLowerCase()==="narrator"?"narration":"dialogue"}),document.querySelectorAll("#audiobook-seglist .audiobook-seg-emo").forEach(inp=>{const i=+inp.dataset.i;segs[i].emotion=inp.value.trim()});const{script,emotions}=audiobookBuildScript(segs);audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions)}function audiobookBuildScript(segments){let script="";const emotions=[],marks=_audiobook.pageMarks||[],src=_audiobook.lastText||"";let markIdx=0,searchPos=0;marks.length&&marks[0].offset<=2&&(markIdx=1);for(const s of segments){const t=(s.text||"").trim();if(!t)continue;if(src&&markIdx=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)script.trim()&&(script+=`
+`+s.text.trimStart():last.text=last.text.trimEnd()+" "+s.text.trimStart();continue}}mergedSegs.push(s)}let respiltSegs=[];for(const s of mergedSegs)s.type==="narration"&&audiobookHasDialogue(s.text)?respiltSegs.push(...audiobookSplitByQuotes(s.text)):respiltSegs.push(s);segs=respiltSegs}}catch(chunkErr){if(chunkErr.name==="AbortError")throw chunkErr;degraded++,segs=audiobookSplitByQuotes(chunks[i]),view.note(`\u274C Passage ${i+1} \u2014 unexpected error (${chunkErr.message}) \u2014 auto-detected dialogue instead`)}audiobookResolveUnknowns(segs,allSegments2.slice(-2),roster),segs.forEach(s=>{const speakerName=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator"?"Narrator":s.speaker;!/^Unknown|Unbekannt/i.test(speakerName)&&!roster.includes(speakerName)&&roster.push(speakerName)}),segs.forEach(s=>{s.page=_curPageNum,allSegments2.push(s)}),view.addSegments(segs),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)}view.update(_audiobook.cancel?completedChunks:chunks.length)}catch(err){err.name==="AbortError"?(_audiobook.cancel=!0,view.note("Casting stopped. Completed passages were preserved.")):(_audiobook.cancel=!0,view.note(`\u274C Casting stopped \u2014 unexpected error: ${err.message}`))}finally{_abStopDraftAutosave(),_audiobook.running=!1,_audiobook.abort=null}if(!allSegments2.length){if(_audiobook.cancel){view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}view.done(),toast("No segments produced","error");return}try{const _quoteFixedSegs=_audiobookFixOrphanedQuoteMarks(allSegments2),_mergedSegs=_audiobookMergeAdjacentSameSpeaker(_quoteFixedSegs);allSegments2.length=0,allSegments2.push(..._mergedSegs),_audiobook.segments=allSegments2,_audiobook.lastText=text,_audiobook.roster=roster,_audiobook.narratedPassages=narrationOnly,_audiobook.degraded=degraded,_audiobook.completedChunks=_audiobook.cancel?completedChunks:chunks.length,_audiobook.completedTotal=chunks.length,_abSaveDraft(allSegments2,roster,text,_audiobook.completedChunks,chunks.length),audiobookSaveAsRehearsal({silent:!0}),_audiobook.cancel&&toast("Casting stopped early. Progress preserved.","info");const speakers=new Set(allSegments2.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),stoppedEarly=_audiobook.cancel&&completedChunks(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length>10&&setTimeout(()=>{audiobookAutoRepairCast().catch(e=>console.warn("[auto-repair]",e))},800)}catch(e){console.error("[audiobookCast] post-processing failed",e),view.done({stopped:!0,message:"Finished casting, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}async function audiobookOpenCurrentInRehearser(){var _a2;const segs=_audiobook.segments||[];if(!segs.length)return toast("No cast to open","error"),!1;if(typeof window.rehLoadRecord=="function"&&typeof _audiobookBuildRehRecord=="function")try{const rec=await _audiobookBuildRehRecord(segs);return _audiobook.rehId&&(rec.id=_audiobook.rehId),(_a2=document.getElementById("audiobook-preview"))==null||_a2.remove(),typeof navTo=="function"&&navTo("s-rehearser"),window.rehLoadRecord(rec),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),toast("Opened cast in Script Rehearser","success"),audiobookSaveAsRehearsal({silent:!0}).catch(()=>{}),!0}catch(err){console.warn("[audiobook] failed to build rehearser record directly, falling back:",err)}await audiobookSaveAsRehearsal({silent:!0});const{script,emotions}=audiobookBuildScript(segs);return await audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),!0}function audiobookShowPreview(){var _a2;audiobookOpenCurrentInRehearser()}function audiobookApplyPreviewAndOpen(){const segs=_audiobook.segments;document.querySelectorAll("#audiobook-seglist .audiobook-seg-sp").forEach(inp=>{const i=+inp.dataset.i,v=inp.value.trim()||"Narrator";segs[i].speaker=v,segs[i].type=v.toLowerCase()==="narrator"?"narration":"dialogue"}),document.querySelectorAll("#audiobook-seglist .audiobook-seg-emo").forEach(inp=>{const i=+inp.dataset.i;segs[i].emotion=inp.value.trim()});const{script,emotions}=audiobookBuildScript(segs);audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions)}function audiobookBuildScript(segments){let script="";const emotions=[],marks=_audiobook.pageMarks||[],src=_audiobook.lastText||"";let markIdx=0,searchPos=0;marks.length&&marks[0].offset<=2&&(markIdx=1);for(const s of segments){const t=(s.text||"").trim();if(!t)continue;if(src&&markIdx=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx=marks[markIdx].offset;)script.trim()&&(script+=`
\f${marks[markIdx].page+1}
`),markIdx++}s.type==="dialogue"&&s.speaker&&s.speaker.toLowerCase()!=="narrator"?(script+=`
`+s.speaker.toUpperCase()+`
diff --git a/static/index.html b/static/index.html
index 6c93981..cc413ae 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) {
-
+