From 1d8e0ff2a689e6b194c5d83bca414d664984609d Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Sun, 23 Aug 2026 10:15:46 +0200 Subject: [PATCH] feat: Audiobook Export Modal, UI sync fixes, and initialization stability (v1.22.0) --- CHANGELOG.md | 12 +++ VERSION | 2 +- static/dist/main.min.js | 8 +- static/js/audiobook.js | 123 ++++++++++++++++++++++++++++-- static/js/rehearser.js | 5 +- static/js/studio.js | 1 - static/sections/s-caststudio.html | 39 ---------- static/sections/s-rehearser.html | 73 ++++++++++++++++++ 8 files changed, 206 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e28535..03733d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.22.0] — 2026-08-23 + +### Added +- **Audiobook Export Modal.** Introduced a comprehensive GUI for exporting audiobooks directly from the Rehearser. +- **Granular Export Controls.** Users can now select specific chapters to export, define the output format (one MP3 per chapter vs. single book file), and configure custom server-side save locations. +- **Pacing Configuration.** Added UI settings to adjust pacing with precise pause durations between paragraphs and chapters. +- **Tone Warning Integration.** Warnings for out-of-bounds tone settings are now displayed within the export modal for safety before batch generation. + +### Fixed +- **UI Synchronization.** Fixed an issue where the Rehearser's green dots for cached audio lines would not synchronize reliably during navigation updates. +- **Bundle Stability.** Addressed a script hoisting issue in the production bundle that caused silent initialization failures and a "Loading..." stall. + ## [1.21.1] — 2026-08-23 ### Added diff --git a/VERSION b/VERSION index 2844977..57807d6 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.21.1 +1.22.0 diff --git a/static/dist/main.min.js b/static/dist/main.min.js index a17b169..21ae3fd 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -1116,7 +1116,7 @@ This warms each voice so the engine caches its .pt and first playback is instant ${check(backend.style_aware)} ${check(backend.uses_wav)} ${action} - `}function _checkToneStyleSupport(){const warn=$("reh-tone-warn");if(!warn)return;const txtEl=$("reh-tone-warn-txt"),b=typeof backendById=="function"?backendById(rehState.backend):null;if(!b){warn.hidden=!0;return}const all=typeof availableTtsBackends=="function"?availableTtsBackends():[],hasTone=rehState.lines.some(l=>l.type==="dialog"&&l.emotion);if(_rehBackendIsFish())txtEl&&(txtEl.innerHTML=`${escHtml(b.label)} keeps each character\u2019s voice consistent and applies tone. Per-line tones are sent as inline [tags] (e.g. [whisper], [excited], [laughing]). You can also type a custom tone like [professional broadcast tone] \u2014 S2 supports free-form descriptions. Fish-Speech S2 \u2197`),warn.hidden=!1;else if(!b.style_aware&&hasTone){const styleAware=all.find(x=>x.style_aware&&x.uses_wav)||all.find(x=>x.style_aware);txtEl&&(txtEl.innerHTML=styleAware?`${_rehToneCmpRow(b,!0)}${_rehToneCmpRow(styleAware,!1)}
Tone controlVoice stays identical
`:`${escHtml(b.label)} keeps each character\u2019s voice consistent but has weak tone control \u2014 tone picks may have little effect.`),warn.hidden=!1}else if(b.style_aware&&!b.uses_wav){const wavBackend=all.find(x=>x.uses_wav&&x.style_aware)||all.find(x=>x.uses_wav),qwenHint=/qwen|voice design|custom/i.test((b.id||"")+" "+(b.label||""))?'

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

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

No clips in this session.

';return}list.innerHTML=rehState.clips.map((clip,idx)=>{const line=rehState.lines[clip.lineIndex]||{text:"\u2014",speaker:clip.speaker},c=rehState.cast[clip.speaker]||{color:"#89b4fa"},typeLabel=clip.type==="me"?"\u{1F3A4} Recorded":clip.type==="tts"?"\u{1F50A} Synthesized":"\u23ED Skipped",audioHtml=clip.blob?``:"",dlHtml=clip.blob?``:"";return`
+ `}function _checkToneStyleSupport(){const warn=$("reh-tone-warn");if(!warn)return;const txtEl=$("reh-tone-warn-txt"),b=typeof backendById=="function"?backendById(rehState.backend):null;if(!b){warn.hidden=!0;return}const all=typeof availableTtsBackends=="function"?availableTtsBackends():[],hasTone=rehState.lines.some(l=>l.type==="dialog"&&l.emotion);if(_rehBackendIsFish())txtEl&&(txtEl.innerHTML=`${escHtml(b.label)} keeps each character\u2019s voice consistent and applies tone. Per-line tones are sent as inline [tags] (e.g. [whisper], [excited], [laughing]). You can also type a custom tone like [professional broadcast tone] \u2014 S2 supports free-form descriptions. Fish-Speech S2 \u2197`),warn.hidden=!1;else if(!b.style_aware&&hasTone){const styleAware=all.find(x=>x.style_aware&&x.uses_wav)||all.find(x=>x.style_aware);txtEl&&(txtEl.innerHTML=styleAware?`${_rehToneCmpRow(b,!0)}${_rehToneCmpRow(styleAware,!1)}
Tone controlVoice stays identical
`:`${escHtml(b.label)} keeps each character\u2019s voice consistent but has weak tone control \u2014 tone picks may have little effect.`),warn.hidden=!1}else if(b.style_aware&&!b.uses_wav){const wavBackend=all.find(x=>x.uses_wav&&x.style_aware)||all.find(x=>x.uses_wav),qwenHint=/qwen|voice design|custom/i.test((b.id||"")+" "+(b.label||""))?'

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

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

No clips in this session.

';return}list.innerHTML=rehState.clips.map((clip,idx)=>{const line=rehState.lines[clip.lineIndex]||{text:"\u2014",speaker:clip.speaker},c=rehState.cast[clip.speaker]||{color:"#89b4fa"},typeLabel=clip.type==="me"?"\u{1F3A4} Recorded":clip.type==="tts"?"\u{1F50A} Synthesized":"\u23ED Skipped",audioHtml=clip.blob?``:"",dlHtml=clip.blob?``:"";return`
${escHtml(clip.speaker||"\u2014")} ${typeLabel}
@@ -1495,7 +1495,7 @@ ${preview}`,{title:"Clean up character library?",okLabel:`Remove ${toDelete.leng `+t+` `,emotions.push(s.emotion||"")):script+=` `+t+` -`}return{script:script.trim(),emotions}}async function audiobookOpenInRehearser(script,title,dialogueEmotions){$("reh-script-text")&&($("reh-script-text").value=script),$("reh-script-title")&&($("reh-script-title").value=title),typeof navTo=="function"&&navTo("s-rehearser");const btn=$("reh-parse-btn");btn?btn.click():typeof parseScript=="function"&&(rehState.lines=parseScript(script));let speakers=0,lines=0;if(window.rehState&&Array.isArray(rehState.lines)){let k=0;rehState.lines.forEach(l=>{if(l.type==="dialog"){const e=dialogueEmotions[k++];e&&(l.emotion=e),lines++}}),speakers=Object.keys(rehState.cast||{}).filter(s=>!String(s).includes("NARRATOR")).length}let saved=!1;if(typeof saveToLibrary=="function")try{rehState.savedId=null,await saveToLibrary(),saved=!0}catch{}toast(`Cast ${speakers} character${speakers!==1?"s":""} \xB7 ${lines} lines`+(saved?" \u2014 saved to Rehearser \u2192 Bibliothek":""),"success")}function audiobookIsChapter(line){if(line.type==="act"||line.type==="scene")return!0;const t=(typeof stripMarkdown=="function"?stripMarkdown(line.text||""):line.text||"").trim();if(!t||t.length>60)return!1;const NUM=/^(?:[ivxlcdm]+(?=[.\s]|$)|\d{1,3})\.?\s*/i,rest=t.replace(NUM,""),KEYWORD=/^(chapter|kapitel|chap\.?|part|book|prologue|epilogue|prolog|epilog|teil)\b/i;if(!KEYWORD.test(rest))return!1;const afterKeyword=rest.replace(KEYWORD,"").trim();return afterKeyword===""||/^[ivxlcdm\d]{1,6}\.?$/i.test(afterKeyword)?!0:/^[ivxlcdm\d]{0,6}\.?\s*[-:–—]\s*.{1,40}$/i.test(afterKeyword)}function _abPauseSettings(){const num=(key,def)=>{let v;try{v=parseFloat(localStorage.getItem(key))}catch{v=NaN}return Number.isFinite(v)&&v>=0?v:def};return{paragraphMs:num("ttsvc_ab_pause_paragraph_s",2)*1e3,chapterMs:num("ttsvc_ab_pause_chapter_s",4)*1e3}}function _abSetPauseSetting(key,seconds){try{localStorage.setItem(key,String(seconds))}catch{}}function _abChapterSfxDataUrl(){try{return localStorage.getItem("ttsvc_ab_chapter_sfx")||null}catch{return null}}function _abSetChapterSfx(dataUrl){try{dataUrl?localStorage.setItem("ttsvc_ab_chapter_sfx",dataUrl):localStorage.removeItem("ttsvc_ab_chapter_sfx")}catch{toast("Could not save chapter sound \u2014 file may be too large","error")}}async function _abChapterSfxWavBlob(fmt){const dataUrl=_abChapterSfxDataUrl();if(!dataUrl)return null;try{const arrayBuf=await(await fetch(dataUrl)).arrayBuffer(),AC=window.AudioContext||window.webkitAudioContext,OAC=window.OfflineAudioContext||window.webkitOfflineAudioContext,tmpCtx=new AC,decoded=await tmpCtx.decodeAudioData(arrayBuf.slice(0));typeof tmpCtx.close=="function"&&tmpCtx.close();const offline=new OAC(fmt.channels,Math.max(1,Math.ceil(decoded.duration*fmt.sampleRate)),fmt.sampleRate),src=offline.createBufferSource();src.buffer=decoded,src.connect(offline.destination),src.start();const rendered=await offline.startRendering(),frames=rendered.length,pcm=new Int16Array(frames*fmt.channels);for(let ch=0;ch_abSetPauseSetting("ttsvc_ab_pause_paragraph_s",Math.max(0,parseFloat(pEl.value)||0))),cEl.addEventListener("change",()=>_abSetPauseSetting("ttsvc_ab_pause_chapter_s",Math.max(0,parseFloat(cEl.value)||0))),sfxClear==null||sfxClear.addEventListener("click",()=>{_abSetChapterSfx(null),sfxFile&&(sfxFile.value=""),sfxCurrent&&(sfxCurrent.textContent="No custom sound")}),sfxFile==null||sfxFile.addEventListener("change",()=>{const file=sfxFile.files&&sfxFile.files[0];if(!file)return;if(file.size>2*1024*1024){toast("Chapter sound is too large (max 2 MB) \u2014 trim it to a short chime/sting","error"),sfxFile.value="";return}const reader=new FileReader;reader.onload=()=>{_abSetChapterSfx(reader.result),sfxCurrent&&(sfxCurrent.textContent=file.name)},reader.onerror=()=>toast("Could not read that audio file","error"),reader.readAsDataURL(file)}))}window._abInitPauseUI=_abInitPauseUI;function audiobookLineVoice(l){if(l.type==="dialog"){const c=rehState.cast[l.speaker]||{};return{voice:c.voice,instruct:typeof _buildInstruct=="function"?_buildInstruct(c.instruct,l.emotion,c.voice):""}}return{voice:rehState.narratorVoice,instruct:""}}async function _abSynthesizeLongText(voice,text,instruct,backend){const pieces=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,900):[text];if(pieces.length<=1)return fetchTtsPreviewBlob(voice,text,"wav",instruct,backend);const blobs=[];for(const piece of pieces)blobs.push(await fetchTtsPreviewBlob(voice,piece,"wav",instruct,backend));return mergeWavBlobs(blobs,120)}async function audiobookExport(){var _a2,_b2;if(_audiobook.running)return;if(!window.rehState||!(rehState.lines||[]).length){toast("Open a script in the rehearser first","error");return}if(!rehState.backend){toast("Select a TTS backend in the rehearser first","error");return}typeof _ensureNarrator=="function"&&_ensureNarrator();const speakable=i=>{const l=rehState.lines[i];if(!l||l.ignored||l.hidden)return!1;if(l.type==="dialog"){const c=rehState.cast[l.speaker];return!!(c&&c.voice&&c.voice!=="me")}return!!(rehState.narratorVoice&&(l.text||"").trim())},buckets=[];let cur=null;rehState.lines.forEach((l,i)=>{audiobookIsChapter(l)&&(cur={title:(typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text).trim().slice(0,50),idx:[]},buckets.push(cur)),speakable(i)&&(cur||(cur={title:"",idx:[]},buckets.push(cur)),cur.idx.push(i))});const allIdx=buckets.flatMap(b=>b.idx);if(!allIdx.length){toast("Nothing to synthesise \u2014 cast voices first","error");return}_audiobook.running=!0,_audiobook.cancel=!1;const prog=audiobookProgress(allIdx.length),wavClips=new Map,failedLines=[];let done=0;const queue=allIdx.slice(),worker=async()=>{for(;queue.length&&!_audiobook.cancel;){const i=queue.shift();if(rehState.synthCache.has(i)&&!rehState.staleLines.has(i)){wavClips.set(i,rehState.synthCache.get(i)),prog.update(++done,`Synthesising line ${done} / ${allIdx.length}\u2026`);continue}const l=rehState.lines[i],{voice,instruct}=audiobookLineVoice(l),text=typeof _rehInlineTone=="function"?_rehInlineTone(stripMarkdown(l.text),l.emotion):typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text;try{let blob=null,cacheKey=null,book=null;if(typeof _lineAudioCacheKey=="function"&&(book=_lineAudioBookName(),cacheKey=await _lineAudioCacheKey(text,voice,instruct),blob=await _lineAudioCacheGet(book,cacheKey)),!blob){let lastErr;for(let attempt=1;attempt<=3;attempt++)try{blob=await _abSynthesizeLongText(voice,text,instruct,_ttsBackendForVoice(voice,rehState.backend)),lastErr=null;break}catch(e){lastErr=e,attempt<3&&await new Promise(r=>setTimeout(r,3e3*attempt))}if(lastErr)throw lastErr;cacheKey&&_lineAudioCachePut(book,cacheKey,blob)}wavClips.set(i,blob),rehState.synthCache.set(i,blob)}catch(e){failedLines.push(i),console.error("[audiobook export] synth failed for line",i,e)}prog.update(++done,`Synthesising line ${done} / ${allIdx.length}\u2026`)}};try{await worker()}finally{prog.done(),_audiobook.running=!1}if(_audiobook.cancel){toast("Export cancelled","error");return}if(failedLines.length){toast(failedLines.length+" line(s) failed to synthesise \u2014 retry them (Script Rehearser \u2192 re-synthesise stale) before exporting, or they will silently drop from the audiobook","error");return}const title=typeof readerSafeName=="function"?readerSafeName(((_a2=$("reh-script-title"))==null?void 0:_a2.value)||"Audiobook"):((_b2=$("reh-script-title"))==null?void 0:_b2.value)||"Audiobook",realChapters=buckets.filter(b=>b.title).length>0;let files=0;const savedFiles=[],bookForExport=_lineAudioBookName(),encMsg=$("audiobook-msg"),pauses=_abPauseSettings();for(let c=0;cwavClips.get(i)).filter(Boolean);if(!blobs.length)continue;encMsg&&(encMsg.textContent=`Merging chapter ${c+1} / ${buckets.length}\u2026`);let mergedWav=await mergeWavBlobs(blobs,pauses.paragraphMs);if(c>0&&buckets[c].title&&(pauses.chapterMs>0||_abChapterSfxDataUrl())){const ref=_parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt,lead=[],sfx=await _abChapterSfxWavBlob(ref);sfx&&lead.push(sfx),pauses.chapterMs>0&&lead.push(_buildWavBlob(_silencePcmBytes(pauses.chapterMs,ref),ref)),lead.length&&(mergedWav=await mergeWavBlobs([...lead,mergedWav]))}encMsg&&(encMsg.textContent=`Encoding chapter ${c+1} / ${buckets.length}\u2026`);let blob=mergedWav;try{const encResp=await fetch("/api/audio/encode-mp3",{method:"POST",body:mergedWav});encResp.ok?blob=await encResp.blob():console.error("[audiobook export] mp3 encode failed, shipping wav instead:",encResp.status)}catch(e){console.error("[audiobook export] mp3 encode request failed, shipping wav instead:",e)}const ext=blob===mergedWav?"wav":"mp3",ch=buckets[c].title?" "+readerSafeName(buckets[c].title):"",name=realChapters||buckets.length>1?`${title} - ${String(c+1).padStart(2,"0")}${ch}.${ext}`:`${title}.${ext}`;typeof readerDownload=="function"&&readerDownload(blob,name);try{(await fetch(`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`,{method:"POST",body:blob})).ok&&savedFiles.push({name,url:`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`})}catch{}files++,await new Promise(r=>setTimeout(r,400))}toast("Exported audiobook \xB7 "+files+(realChapters?" chapter file(s)":" file(s)"),"success"),savedFiles.length&&typeof _abShowExportResults=="function"&&_abShowExportResults(bookForExport,savedFiles)}function _abShowExportResults(book,files,opts={}){var _a2;(_a2=document.getElementById("ab-export-results"))==null||_a2.remove();const ov=document.createElement("div");ov.id="ab-export-results",ov.className="audiobook-overlay";const zipUrl=`/api/audiobook-export/${encodeURIComponent(book)}/zip`;ov.innerHTML=`
+`}return{script:script.trim(),emotions}}async function audiobookOpenInRehearser(script,title,dialogueEmotions){$("reh-script-text")&&($("reh-script-text").value=script),$("reh-script-title")&&($("reh-script-title").value=title),typeof navTo=="function"&&navTo("s-rehearser");const btn=$("reh-parse-btn");btn?btn.click():typeof parseScript=="function"&&(rehState.lines=parseScript(script));let speakers=0,lines=0;if(window.rehState&&Array.isArray(rehState.lines)){let k=0;rehState.lines.forEach(l=>{if(l.type==="dialog"){const e=dialogueEmotions[k++];e&&(l.emotion=e),lines++}}),speakers=Object.keys(rehState.cast||{}).filter(s=>!String(s).includes("NARRATOR")).length}let saved=!1;if(typeof saveToLibrary=="function")try{rehState.savedId=null,await saveToLibrary(),saved=!0}catch{}toast(`Cast ${speakers} character${speakers!==1?"s":""} \xB7 ${lines} lines`+(saved?" \u2014 saved to Rehearser \u2192 Bibliothek":""),"success")}function audiobookIsChapter(line){if(line.type==="act"||line.type==="scene")return!0;const t=(typeof stripMarkdown=="function"?stripMarkdown(line.text||""):line.text||"").trim();if(!t||t.length>60)return!1;const NUM=/^(?:[ivxlcdm]+(?=[.\s]|$)|\d{1,3})\.?\s*/i,rest=t.replace(NUM,""),KEYWORD=/^(chapter|kapitel|chap\.?|part|book|prologue|epilogue|prolog|epilog|teil)\b/i;if(!KEYWORD.test(rest))return!1;const afterKeyword=rest.replace(KEYWORD,"").trim();return afterKeyword===""||/^[ivxlcdm\d]{1,6}\.?$/i.test(afterKeyword)?!0:/^[ivxlcdm\d]{0,6}\.?\s*[-:–—]\s*.{1,40}$/i.test(afterKeyword)}function _abPauseSettings(){const num=(key,def)=>{let v;try{v=parseFloat(localStorage.getItem(key))}catch{v=NaN}return Number.isFinite(v)&&v>=0?v:def};return{paragraphMs:num("ttsvc_ab_pause_paragraph_s",2)*1e3,chapterMs:num("ttsvc_ab_pause_chapter_s",4)*1e3}}function _abSetPauseSetting(key,seconds){try{localStorage.setItem(key,String(seconds))}catch{}}function _abChapterSfxDataUrl(){try{return localStorage.getItem("ttsvc_ab_chapter_sfx")||null}catch{return null}}function _abSetChapterSfx(dataUrl){try{dataUrl?localStorage.setItem("ttsvc_ab_chapter_sfx",dataUrl):localStorage.removeItem("ttsvc_ab_chapter_sfx")}catch{toast("Could not save chapter sound \u2014 file may be too large","error")}}async function _abChapterSfxWavBlob(fmt){const dataUrl=_abChapterSfxDataUrl();if(!dataUrl)return null;try{const arrayBuf=await(await fetch(dataUrl)).arrayBuffer(),AC=window.AudioContext||window.webkitAudioContext,OAC=window.OfflineAudioContext||window.webkitOfflineAudioContext,tmpCtx=new AC,decoded=await tmpCtx.decodeAudioData(arrayBuf.slice(0));typeof tmpCtx.close=="function"&&tmpCtx.close();const offline=new OAC(fmt.channels,Math.max(1,Math.ceil(decoded.duration*fmt.sampleRate)),fmt.sampleRate),src=offline.createBufferSource();src.buffer=decoded,src.connect(offline.destination),src.start();const rendered=await offline.startRendering(),frames=rendered.length,pcm=new Int16Array(frames*fmt.channels);for(let ch=0;ch_abSetPauseSetting("ttsvc_ab_pause_paragraph_s",Math.max(0,parseFloat(pEl.value)||0))),cEl.addEventListener("change",()=>_abSetPauseSetting("ttsvc_ab_pause_chapter_s",Math.max(0,parseFloat(cEl.value)||0))),sfxClear==null||sfxClear.addEventListener("click",()=>{_abSetChapterSfx(null),sfxFile&&(sfxFile.value=""),sfxCurrent&&(sfxCurrent.textContent="No custom sound")}),sfxFile==null||sfxFile.addEventListener("change",()=>{const file=sfxFile.files&&sfxFile.files[0];if(!file)return;if(file.size>2*1024*1024){toast("Chapter sound is too large (max 2 MB) \u2014 trim it to a short chime/sting","error"),sfxFile.value="";return}const reader=new FileReader;reader.onload=()=>{_abSetChapterSfx(reader.result),sfxCurrent&&(sfxCurrent.textContent=file.name)},reader.onerror=()=>toast("Could not read that audio file","error"),reader.readAsDataURL(file)}))}window._abInitPauseUI=_abInitPauseUI;function audiobookLineVoice(l){if(l.type==="dialog"){const c=rehState.cast[l.speaker]||{};return{voice:c.voice,instruct:typeof _buildInstruct=="function"?_buildInstruct(c.instruct,l.emotion,c.voice):""}}return{voice:rehState.narratorVoice,instruct:""}}async function _abSynthesizeLongText(voice,text,instruct,backend){const pieces=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,900):[text];if(pieces.length<=1)return fetchTtsPreviewBlob(voice,text,"wav",instruct,backend);const blobs=[];for(const piece of pieces)blobs.push(await fetchTtsPreviewBlob(voice,piece,"wav",instruct,backend));return mergeWavBlobs(blobs,120)}async function audiobookExport(opts={}){var _a2,_b2;if(_audiobook.running)return;if(!window.rehState||!(rehState.lines||[]).length){toast("Open a script in the rehearser first","error");return}if(!rehState.backend){toast("Select a TTS backend in the rehearser first","error");return}typeof _ensureNarrator=="function"&&_ensureNarrator();const speakable=i=>{const l=rehState.lines[i];if(!l||l.ignored||l.hidden)return!1;if(l.type==="dialog"){const c=rehState.cast[l.speaker];return!!(c&&c.voice&&c.voice!=="me")}return!!(rehState.narratorVoice&&(l.text||"").trim())},buckets=[];let cur=null;rehState.lines.forEach((l,i)=>{audiobookIsChapter(l)&&(cur={title:(typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text).trim().slice(0,50),idx:[]},buckets.push(cur)),speakable(i)&&(cur||(cur={title:"",idx:[]},buckets.push(cur)),cur.idx.push(i))});let activeBuckets=buckets;opts.selectedChapterIndices&&(activeBuckets=buckets.filter((_,i)=>opts.selectedChapterIndices.includes(i)));const allIdx=activeBuckets.flatMap(b=>b.idx);if(!allIdx.length){toast("Nothing to synthesise \u2014 cast voices first","error");return}_audiobook.running=!0,_audiobook.cancel=!1;const prog=audiobookProgress(allIdx.length),wavClips=new Map,failedLines=[];let done=0;const queue=allIdx.slice(),worker=async()=>{for(;queue.length&&!_audiobook.cancel;){const i=queue.shift();if(rehState.synthCache.has(i)&&!rehState.staleLines.has(i)){wavClips.set(i,rehState.synthCache.get(i)),prog.update(++done,`Synthesising line ${done} / ${allIdx.length}\u2026`);continue}const l=rehState.lines[i],{voice,instruct}=audiobookLineVoice(l),text=typeof _rehInlineTone=="function"?_rehInlineTone(stripMarkdown(l.text),l.emotion):typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text;try{let blob=null,cacheKey=null,book=null;if(typeof _lineAudioCacheKey=="function"&&(book=_lineAudioBookName(),cacheKey=await _lineAudioCacheKey(text,voice,instruct),blob=await _lineAudioCacheGet(book,cacheKey)),!blob){let lastErr;for(let attempt=1;attempt<=3;attempt++)try{blob=await _abSynthesizeLongText(voice,text,instruct,_ttsBackendForVoice(voice,rehState.backend)),lastErr=null;break}catch(e){lastErr=e,attempt<3&&await new Promise(r=>setTimeout(r,3e3*attempt))}if(lastErr)throw lastErr;cacheKey&&_lineAudioCachePut(book,cacheKey,blob)}wavClips.set(i,blob),rehState.synthCache.set(i,blob)}catch(e){failedLines.push(i),console.error("[audiobook export] synth failed for line",i,e)}prog.update(++done,`Synthesising line ${done} / ${allIdx.length}\u2026`)}};try{await worker()}finally{prog.done(),_audiobook.running=!1}if(_audiobook.cancel){toast("Export cancelled","error");return}if(failedLines.length){toast(failedLines.length+" line(s) failed to synthesise \u2014 retry them (Script Rehearser \u2192 re-synthesise stale) before exporting, or they will silently drop from the audiobook","error");return}const title=typeof readerSafeName=="function"?readerSafeName(((_a2=$("reh-script-title"))==null?void 0:_a2.value)||"Audiobook"):((_b2=$("reh-script-title"))==null?void 0:_b2.value)||"Audiobook",realChapters=buckets.filter(b=>b.title).length>0;let files=0;const savedFiles=[],bookForExport=_lineAudioBookName(),encMsg=$("audiobook-msg"),pauses=_abPauseSettings();if(opts.format==="single"){let masterBlobs=[];for(let c=0;cwavClips.get(i)).filter(Boolean);if(!blobs.length)continue;encMsg&&(encMsg.textContent=`Merging chapter ${c+1} / ${activeBuckets.length}\u2026`);let mergedWav=await mergeWavBlobs(blobs,pauses.paragraphMs);if(c>0&&activeBuckets[c].title&&(pauses.chapterMs>0||_abChapterSfxDataUrl())){const ref=_parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt,lead=[],sfx=await _abChapterSfxWavBlob(ref);sfx&&lead.push(sfx),pauses.chapterMs>0&&lead.push(_buildWavBlob(_silencePcmBytes(pauses.chapterMs,ref),ref)),lead.length&&(mergedWav=await mergeWavBlobs([...lead,mergedWav]))}masterBlobs.push(mergedWav)}if(masterBlobs.length){encMsg&&(encMsg.textContent="Encoding full book to MP3\u2026");let finalWav=await mergeWavBlobs(masterBlobs,0),blob=finalWav;try{const encResp=await fetch("/api/audio/encode-mp3",{method:"POST",body:finalWav});encResp.ok&&(blob=await encResp.blob())}catch{}const name=`${title}.${blob===finalWav?"wav":"mp3"}`;typeof readerDownload=="function"&&readerDownload(blob,name);try{let exportUrl=`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`;opts.folder&&(exportUrl+=`?folder=${encodeURIComponent(opts.folder)}`),(await fetch(exportUrl,{method:"POST",body:blob})).ok&&savedFiles.push({name,url:exportUrl})}catch{}files++}}else for(let c=0;cwavClips.get(i)).filter(Boolean);if(!blobs.length)continue;encMsg&&(encMsg.textContent=`Merging chapter ${c+1} / ${activeBuckets.length}\u2026`);let mergedWav=await mergeWavBlobs(blobs,pauses.paragraphMs);if(c>0&&activeBuckets[c].title&&(pauses.chapterMs>0||_abChapterSfxDataUrl())){const ref=_parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt,lead=[],sfx=await _abChapterSfxWavBlob(ref);sfx&&lead.push(sfx),pauses.chapterMs>0&&lead.push(_buildWavBlob(_silencePcmBytes(pauses.chapterMs,ref),ref)),lead.length&&(mergedWav=await mergeWavBlobs([...lead,mergedWav]))}encMsg&&(encMsg.textContent=`Encoding chapter ${c+1} / ${activeBuckets.length}\u2026`);let blob=mergedWav;try{const encResp=await fetch("/api/audio/encode-mp3",{method:"POST",body:mergedWav});encResp.ok?blob=await encResp.blob():console.error("[audiobook export] mp3 encode failed, shipping wav instead:",encResp.status)}catch(e){console.error("[audiobook export] mp3 encode request failed, shipping wav instead:",e)}const ext=blob===mergedWav?"wav":"mp3",ch=buckets[c].title?" "+readerSafeName(buckets[c].title):"",name=realChapters||buckets.length>1?`${title} - ${String(c+1).padStart(2,"0")}${ch}.${ext}`:`${title}.${ext}`;typeof readerDownload=="function"&&readerDownload(blob,name);try{(await fetch(`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`,{method:"POST",body:blob})).ok&&savedFiles.push({name,url:`/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`})}catch{}files++,await new Promise(r=>setTimeout(r,400))}toast("Exported audiobook \xB7 "+files+(realChapters?" chapter file(s)":" file(s)"),"success"),savedFiles.length&&typeof _abShowExportResults=="function"&&_abShowExportResults(bookForExport,savedFiles)}function _abShowExportResults(book,files,opts={}){var _a2;(_a2=document.getElementById("ab-export-results"))==null||_a2.remove();const ov=document.createElement("div");ov.id="ab-export-results",ov.className="audiobook-overlay";const zipUrl=`/api/audiobook-export/${encodeURIComponent(book)}/zip`;ov.innerHTML=`
${opts.browsing?"Saved audiobook exports":"Audiobook exported"}

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

@@ -1507,7 +1507,7 @@ ${preview}`,{title:"Clean up character library?",okLabel:`Remove ${toDelete.leng
`).join("")}
${files.length>1?``:""} -
`,document.body.appendChild(ov);const close=()=>ov.remove();ov.querySelector("#ab-export-close").addEventListener("click",close),ov.addEventListener("click",e=>{e.target===ov&&close()})}async function audiobookBrowseExports(){var _a2;const book=typeof _lineAudioBookName=="function"?_lineAudioBookName():((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled";try{const r=await fetch(`/api/audiobook-export/${encodeURIComponent(book)}`);if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();if(!d.files||!d.files.length){toast('No saved exports yet for this book \u2014 run "Audiobook" first',"info");return}const files=d.files.map(f=>({name:f.name,url:`/api/audiobook-export/${encodeURIComponent(book)}/${encodeURIComponent(f.name)}`}));_abShowExportResults(book,files,{browsing:!0})}catch(e){toast("Could not load saved exports: "+(e.message||e),"error")}}(_oc=$("reh-tb-browse-exports"))==null||_oc.addEventListener("click",audiobookBrowseExports),(_pc=$("reader-audiobook-btn"))==null||_pc.addEventListener("click",audiobookOpenCastView),(_qc=$("reh-tb-audiobook"))==null||_qc.addEventListener("click",audiobookExport);async function _audiobookBuildRehRecord(segs){const{script,emotions}=audiobookBuildScript(segs),title=typeof readerState!="undefined"&&readerState.title?readerState.title:"Audiobook",lines=typeof parseScript=="function"?parseScript(script):[];if(emotions&&emotions.length){let eIdx=0;lines.forEach(l=>{l.type==="dialog"&&eIdx{cast[sp]={voice:def.voice,color:def.color,instruct:"",lang:"",gender:"",tags:"",soul:"",ignored:!1,hidden:!1,voiceData:null}})}let existing=null;if(_audiobook.rehId&&typeof window.rehDbGetById=="function")try{existing=await window.rehDbGetById(_audiobook.rehId)}catch{}existing&&existing.cast&&Object.entries(existing.cast).forEach(([sp,info])=>{cast[sp]?cast[sp]={...cast[sp],...info}:cast[sp]=info});const emotions_map={};return lines.forEach((l,i)=>{l.type==="dialog"&&l.emotion&&(emotions_map[i]=l.emotion)}),{title,script,cast,emotions:emotions_map,notes:existing?existing.notes||{}:{},ignored:existing?existing.ignored||{}:{},hidden:existing?existing.hidden||{}:{},backend:existing&&existing.backend||"",narratorVoice:existing&&existing.narratorVoice||"",lineIndex:existing&&existing.lineIndex||0,clips:existing?existing.clips||[]:[],created:existing?existing.created:new Date,updated:new Date}}let _abRehSaveTimer=null;async function audiobookSaveAsRehearsal(opts){const silent=opts&&opts.silent,segs=_audiobook.segments;if(!segs||!segs.length){silent||toast("No segments to save","error");return}if(typeof rehDbAdd!="function"){silent||toast("Rehearser DB not available","error");return}try{const rec=await _audiobookBuildRehRecord(segs);_audiobook.rehId?(rec.id=_audiobook.rehId,await rehDbPut(rec)):_audiobook.rehId=await rehDbAdd(rec),typeof renderLibraryList=="function"&&renderLibraryList(),silent||(_abClearDraft(),toast("Saved as Script Rehearsal","success"))}catch(e){silent?console.warn("[audiobook] auto-save failed:",e):toast("Failed to save rehearsal: "+e.message,"error")}}function _audiobookDebouncedSave(){clearTimeout(_abRehSaveTimer),_abRehSaveTimer=setTimeout(()=>audiobookSaveAsRehearsal({silent:!0}),1500)}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(` +
`,document.body.appendChild(ov);const close=()=>ov.remove();ov.querySelector("#ab-export-close").addEventListener("click",close),ov.addEventListener("click",e=>{e.target===ov&&close()})}async function audiobookBrowseExports(){var _a2;const book=typeof _lineAudioBookName=="function"?_lineAudioBookName():((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled";try{const r=await fetch(`/api/audiobook-export/${encodeURIComponent(book)}`);if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();if(!d.files||!d.files.length){toast('No saved exports yet for this book \u2014 run "Audiobook" first',"info");return}const files=d.files.map(f=>({name:f.name,url:`/api/audiobook-export/${encodeURIComponent(book)}/${encodeURIComponent(f.name)}`}));_abShowExportResults(book,files,{browsing:!0})}catch(e){toast("Could not load saved exports: "+(e.message||e),"error")}}(_oc=$("reh-tb-browse-exports"))==null||_oc.addEventListener("click",audiobookBrowseExports),(_pc=$("reader-audiobook-btn"))==null||_pc.addEventListener("click",audiobookOpenCastView),(_qc=$("reh-tb-audiobook"))==null||_qc.addEventListener("click",audiobookExportDialog);async function _audiobookBuildRehRecord(segs){const{script,emotions}=audiobookBuildScript(segs),title=typeof readerState!="undefined"&&readerState.title?readerState.title:"Audiobook",lines=typeof parseScript=="function"?parseScript(script):[];if(emotions&&emotions.length){let eIdx=0;lines.forEach(l=>{l.type==="dialog"&&eIdx{cast[sp]={voice:def.voice,color:def.color,instruct:"",lang:"",gender:"",tags:"",soul:"",ignored:!1,hidden:!1,voiceData:null}})}let existing=null;if(_audiobook.rehId&&typeof window.rehDbGetById=="function")try{existing=await window.rehDbGetById(_audiobook.rehId)}catch{}existing&&existing.cast&&Object.entries(existing.cast).forEach(([sp,info])=>{cast[sp]?cast[sp]={...cast[sp],...info}:cast[sp]=info});const emotions_map={};return lines.forEach((l,i)=>{l.type==="dialog"&&l.emotion&&(emotions_map[i]=l.emotion)}),{title,script,cast,emotions:emotions_map,notes:existing?existing.notes||{}:{},ignored:existing?existing.ignored||{}:{},hidden:existing?existing.hidden||{}:{},backend:existing&&existing.backend||"",narratorVoice:existing&&existing.narratorVoice||"",lineIndex:existing&&existing.lineIndex||0,clips:existing?existing.clips||[]:[],created:existing?existing.created:new Date,updated:new Date}}let _abRehSaveTimer=null;async function audiobookSaveAsRehearsal(opts){const silent=opts&&opts.silent,segs=_audiobook.segments;if(!segs||!segs.length){silent||toast("No segments to save","error");return}if(typeof rehDbAdd!="function"){silent||toast("Rehearser DB not available","error");return}try{const rec=await _audiobookBuildRehRecord(segs);_audiobook.rehId?(rec.id=_audiobook.rehId,await rehDbPut(rec)):_audiobook.rehId=await rehDbAdd(rec),typeof renderLibraryList=="function"&&renderLibraryList(),silent||(_abClearDraft(),toast("Saved as Script Rehearsal","success"))}catch(e){silent?console.warn("[audiobook] auto-save failed:",e):toast("Failed to save rehearsal: "+e.message,"error")}}function _audiobookDebouncedSave(){clearTimeout(_abRehSaveTimer),_abRehSaveTimer=setTimeout(()=>audiobookSaveAsRehearsal({silent:!0}),1500)}function audiobookExportDialog(){if(_audiobook.running)return;if(!window.rehState||!(rehState.lines||[]).length){toast("Open a script in the rehearser first","error");return}if(!rehState.backend){toast("Select a TTS backend in the rehearser first","error");return}const speakable=i=>{const l=rehState.lines[i];if(!l||l.ignored||l.hidden)return!1;if(l.type==="dialog"){const c=rehState.cast[l.speaker];return!!(c&&c.voice&&c.voice!=="me")}return!!(rehState.narratorVoice&&(l.text||"").trim())},buckets=[];let cur=null;rehState.lines.forEach((l,i)=>{audiobookIsChapter(l)&&(cur={title:(typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text).trim().slice(0,50),idx:[]},buckets.push(cur)),speakable(i)&&(cur||(cur={title:"",idx:[]},buckets.push(cur)),cur.idx.push(i))});const listEl=document.getElementById("ab-export-chapter-list");if(listEl){listEl.innerHTML="";const countEl=document.getElementById("ab-export-chapter-count");countEl&&(countEl.textContent=buckets.length),buckets.forEach((b,i)=>{const lbl=document.createElement("label");lbl.style.display="block",lbl.style.marginBottom="4px";const chk=document.createElement("input");chk.type="checkbox",chk.checked=!0,chk.value=i,chk.className="ab-export-chapter-cb",lbl.appendChild(chk),lbl.appendChild(document.createTextNode(" Chapter "+(i+1)+(b.title?": "+b.title:""))),listEl.appendChild(lbl)})}const warn=document.getElementById("reh-tone-warn"),slot=document.getElementById("ab-export-tone-warn-slot");warn&&slot&&(warn.hidden=!1,slot.appendChild(warn)),typeof _abInitPauseUI=="function"&&_abInitPauseUI();const modal=document.getElementById("audiobook-export-modal");modal&&(modal.hidden=!1)}document.addEventListener("DOMContentLoaded",()=>{var _a2,_b2,_c2;(_a2=document.getElementById("ab-export-modal-close"))==null||_a2.addEventListener("click",()=>document.getElementById("audiobook-export-modal").hidden=!0),(_b2=document.getElementById("ab-export-modal-cancel"))==null||_b2.addEventListener("click",()=>document.getElementById("audiobook-export-modal").hidden=!0),(_c2=document.getElementById("ab-export-modal-start"))==null||_c2.addEventListener("click",()=>{document.getElementById("audiobook-export-modal").hidden=!0;const formatEl=document.querySelector('input[name="ab_export_format"]:checked'),format=formatEl?formatEl.value:"chapter",folderEl=document.getElementById("ab-export-server-folder"),folder=folderEl?folderEl.value:"",selectedChapterIndices=Array.from(document.querySelectorAll(".ab-export-chapter-cb")).filter(cb=>cb.checked).map(cb=>parseInt(cb.value,10));audiobookExport({format,selectedChapterIndices,folder})})});async function audiobookFetchWithTimeout(url,options={},timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const parentSignal=options.signal,ac=new AbortController;let timedOut=!1;const timer=setTimeout(()=>{timedOut=!0,ac.abort()},timeoutMs),onParentAbort=()=>ac.abort(parentSignal==null?void 0:parentSignal.reason);parentSignal&&(parentSignal.aborted?onParentAbort():parentSignal.addEventListener("abort",onParentAbort,{once:!0}));try{return await fetch(url,{...options,signal:ac.signal})}catch(err){if(timedOut){const timeoutErr=new Error(`Timed out after ${Math.ceil(timeoutMs/1e3)}s`);throw timeoutErr.name="TimeoutError",timeoutErr}throw err}finally{clearTimeout(timer),parentSignal&&parentSignal.removeEventListener("abort",onParentAbort)}}function audiobookTimeoutSeconds(timeoutMs){return Math.max(5,Math.round(timeoutMs/1e3))}async function audiobookAttributeStream(body,view,outerSignal,idleTimeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/attribute-dialogue/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify({...body,want_reasoning:!0})});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(` `))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&(view!=null&&view.thinking)&&view.thinking(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}catch(err){throw(err==null?void 0:err.name)==="AbortError"&&!(outerSignal&&outerSignal.aborted)?new Error("stream idle timeout"):err}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}const CS_CHUNK_CHARS=4e3,_cs={running:!1,cancel:!1,cache:{}},CS_DEFAULT_PROMPT=`You are an expert dramaturge, developmental editor, and tabletop RPG game master building rich character sheets passage by passage as a book is read. Extract playable, action-oriented sheets an actor can use to immediately know how to PLAY the character. PROGRESSIVE FILLING: you may be given the sheets built so far. For returning characters, ADD any NEW detail this passage reveals and refine vague fields; do not contradict solid earlier facts or blank out a field you cannot improve. In this cast-character pass, ONLY refine the already-casted roster and do NOT invent new profiles, places, or institutions. The text may be a focused evidence window around a mention, so use the nearby paragraphs as context. Leave a field empty if the book genuinely hasn't shown it yet (a later passage can fill it). Extrapolate from dialogue and actions when reasonable, and mark any deduced value with a trailing ' *'. @@ -1823,4 +1823,4 @@ Respond with STRICT JSON only: `)[0].slice(0,60)||"",backstory:data.description||data.char_persona||"",mannerisms:data.personality||"",arc_note:data.scenario||"",voice_pattern:data.mes_example||data.example_dialogue||data.first_mes||data.char_greeting||"",gender:ext.gender||stGuessGender((data.description||"")+" "+(data.personality||"")),note:data.creator_notes||"",tags,voice:ext.tts_voice||null,tier:"supporting",st_card:data}}function stFromRecord(rec){const sh=rec.sheet||{},prev=sh.st_card||{},descParts=[sh.physical,sh.clothing,sh.backstory,sh.alignment].map(x=>String(x||"").trim()).filter(Boolean),persona=[sh.archetype,sh.mannerisms,sh.motivation,sh.fears].map(x=>String(x||"").trim()).filter(Boolean).join(` `);return{spec:"chara_card_v2",spec_version:"2.0",data:{name:rec.name,description:prev.description||descParts.join(` -`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'
').join("")}async function libraryRenderBooks(){const list=document.getElementById("lib-books-list");if(!list)return;list.innerHTML=_libSkeleton(4);let all=[];try{const r=await fetch(LIB_READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='

No books yet.

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

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

No theater plays yet.

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

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

No characters yet.

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

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

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

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

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

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

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

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

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

';const goBtn=document.getElementById("stu-goto-cast-menu");goBtn&&goBtn.addEventListener("click",function(){typeof window.csForReader=="function"&&window.csForReader()})}}}document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(tab){tab.addEventListener("click",function(){_stuShowCastView(tab.dataset.stuCastView)})}),(_uc=document.getElementById("stu-book-context-btn"))==null||_uc.addEventListener("click",async function(){var _a2;const bookTitle=((_a2=window.readerState)==null?void 0:_a2.title)||"";!bookTitle||typeof window.openBookContextEditor!="function"||await window.openBookContextEditor(bookTitle)}),(_vc=document.getElementById("stu-change-book-btn"))==null||_vc.addEventListener("click",function(){showStudioPhase(1)});let _stuCastFootObserver=null,_stuCastFootHealthTimer=null;function _stuCastIsRunning(){return typeof _audiobook!="undefined"&&!!_audiobook.running}function _stuSyncCastFallbackControls(tabs,innerTabs,hasActionFooter){const running=_stuCastIsRunning();tabs.hidden=!1,tabs.style.display="flex",innerTabs.forEach(function(tab){const hideFallback=running||hasActionFooter;tab.hidden=hideFallback,tab.style.display=hideFallback?"none":""})}function _stuRelocateCastFoot(){_stuTryRelocateCastFoot(),!_stuCastFootObserver&&(_stuCastFootObserver=new MutationObserver(function(){_stuTryRelocateCastFoot()}),_stuCastFootObserver.observe(document.body,{childList:!0,subtree:!0}),[0,100,300,800,1500].forEach(ms=>setTimeout(_stuTryRelocateCastFoot,ms)),_stuCastFootHealthTimer=setInterval(function(){_stuIsActive&&_stuActive===2&&_stuTryRelocateCastFoot()},500))}function _stuTryRelocateCastFoot(){const slot=document.getElementById("stu-cast-slot"),tabs=document.getElementById("stu-cast-inner-tabs");if(!tabs)return;const freshFoot=slot&&slot.querySelector("#ab-cv-foot")||document.querySelector("#ab-cv-foot"),innerTabs=Array.from(tabs.querySelectorAll(":scope > .stu-inner-tab")),footerHasActions=freshFoot&&freshFoot.querySelector("#ab-cv-menu-identify, #ab-cv-menu-cast, #ab-cv-menu-viewcast");if(_stuSyncCastFallbackControls(tabs,innerTabs,!!footerHasActions),!freshFoot||!footerHasActions)return;if(freshFoot.parentElement===tabs){freshFoot.hidden=!1,freshFoot.style.display="flex";return}tabs.querySelectorAll("#ab-cv-foot").forEach(function(stale){stale.remove()}),tabs.hidden=!1,tabs.style.display="flex",freshFoot.style.borderTop="none",freshFoot.style.padding="0",freshFoot.style.justifyContent="flex-start",freshFoot.hidden=!1,freshFoot.style.display="flex",tabs.appendChild(freshFoot);const openReh=freshFoot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!0)}function _stuRestoreCastFoot(){_stuCastFootObserver&&(_stuCastFootObserver.disconnect(),_stuCastFootObserver=null),_stuCastFootHealthTimer&&(clearInterval(_stuCastFootHealthTimer),_stuCastFootHealthTimer=null);const tabs=document.getElementById("stu-cast-inner-tabs"),panel=document.getElementById("reader-audiobook-panel"),foot=tabs?tabs.querySelector("#ab-cv-foot"):null;if(foot&&panel){foot.style.borderTop="",foot.style.padding="",foot.style.justifyContent="";const openReh=foot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!1),panel.appendChild(foot)}document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!1})}function showStudioPhase(n){_stuActive=n,_stuSaveResume(n);for(let i=1;i<=4;i++){const el=document.getElementById("stu-phase-"+i);el&&(el.hidden=i!==n)}document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.classList.toggle("active",parseInt(tab.dataset.stuPhase,10)===n)}),document.querySelectorAll("#nav-caststudio-tree [data-stu-phase]").forEach(function(item){item.classList.toggle("is-active",parseInt(item.dataset.stuPhase,10)===n)});const prevBtn=document.getElementById("stu-phase-prev"),nextBtn=document.getElementById("stu-phase-next");prevBtn&&(prevBtn.disabled=n<=1),nextBtn&&(nextBtn.disabled=n>=4),typeof _stuEnterPhase=="function"&&_stuEnterPhase(n)}window.showStudioPhase=showStudioPhase,document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.addEventListener("click",function(){showStudioPhase(parseInt(tab.dataset.stuPhase,10))})}),(_wc=document.getElementById("stu-phase-prev"))==null||_wc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_xc=document.getElementById("stu-phase-next"))==null||_xc.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){var _a2,_b2,_c2;_stuInstallNavGuardOnce(),_stuIsActive=!0;const resume=_stuReadResume();resume.bookId&&window._audiobook&&(window._audiobook.bookId=resume.bookId),resume.phase>=1&&resume.phase<=4&&(_stuActive=resume.phase,resume.characterId&&(window._libResumeCharacterId=resume.characterId));const resumeBookId=resume.bookId,hasCurrentBook=!!((_b2=(_a2=window.readerState)==null?void 0:_a2.sentences)!=null&&_b2.length&&((_c2=window.readerState)!=null&&_c2.savedId));if(resumeBookId&&!hasCurrentBook&&typeof window.readerOpenLibraryDoc=="function"){window.readerOpenLibraryDoc(resumeBookId).then(function(){showStudioPhase(_stuActive)});return}showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow; +`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'
').join("")}async function libraryRenderBooks(){const list=document.getElementById("lib-books-list");if(!list)return;list.innerHTML=_libSkeleton(4);let all=[];try{const r=await fetch(LIB_READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='

No books yet.

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

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

No theater plays yet.

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

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

No characters yet.

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

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

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

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

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

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

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

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

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

';const goBtn=document.getElementById("stu-goto-cast-menu");goBtn&&goBtn.addEventListener("click",function(){typeof window.csForReader=="function"&&window.csForReader()})}}}document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(tab){tab.addEventListener("click",function(){_stuShowCastView(tab.dataset.stuCastView)})}),(_uc=document.getElementById("stu-book-context-btn"))==null||_uc.addEventListener("click",async function(){var _a2;const bookTitle=((_a2=window.readerState)==null?void 0:_a2.title)||"";!bookTitle||typeof window.openBookContextEditor!="function"||await window.openBookContextEditor(bookTitle)}),(_vc=document.getElementById("stu-change-book-btn"))==null||_vc.addEventListener("click",function(){showStudioPhase(1)});let _stuCastFootObserver=null,_stuCastFootHealthTimer=null;function _stuCastIsRunning(){return typeof _audiobook!="undefined"&&!!_audiobook.running}function _stuSyncCastFallbackControls(tabs,innerTabs,hasActionFooter){const running=_stuCastIsRunning();tabs.hidden=!1,tabs.style.display="flex",innerTabs.forEach(function(tab){const hideFallback=running||hasActionFooter;tab.hidden=hideFallback,tab.style.display=hideFallback?"none":""})}function _stuRelocateCastFoot(){_stuTryRelocateCastFoot(),!_stuCastFootObserver&&(_stuCastFootObserver=new MutationObserver(function(){_stuTryRelocateCastFoot()}),_stuCastFootObserver.observe(document.body,{childList:!0,subtree:!0}),[0,100,300,800,1500].forEach(ms=>setTimeout(_stuTryRelocateCastFoot,ms)),_stuCastFootHealthTimer=setInterval(function(){_stuIsActive&&_stuActive===2&&_stuTryRelocateCastFoot()},500))}function _stuTryRelocateCastFoot(){const slot=document.getElementById("stu-cast-slot"),tabs=document.getElementById("stu-cast-inner-tabs");if(!tabs)return;const freshFoot=slot&&slot.querySelector("#ab-cv-foot")||document.querySelector("#ab-cv-foot"),innerTabs=Array.from(tabs.querySelectorAll(":scope > .stu-inner-tab")),footerHasActions=freshFoot&&freshFoot.querySelector("#ab-cv-menu-identify, #ab-cv-menu-cast, #ab-cv-menu-viewcast");if(_stuSyncCastFallbackControls(tabs,innerTabs,!!footerHasActions),!freshFoot||!footerHasActions)return;if(freshFoot.parentElement===tabs){freshFoot.hidden=!1,freshFoot.style.display="flex";return}tabs.querySelectorAll("#ab-cv-foot").forEach(function(stale){stale.remove()}),tabs.hidden=!1,tabs.style.display="flex",freshFoot.style.borderTop="none",freshFoot.style.padding="0",freshFoot.style.justifyContent="flex-start",freshFoot.hidden=!1,freshFoot.style.display="flex",tabs.appendChild(freshFoot);const openReh=freshFoot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!0)}function _stuRestoreCastFoot(){_stuCastFootObserver&&(_stuCastFootObserver.disconnect(),_stuCastFootObserver=null),_stuCastFootHealthTimer&&(clearInterval(_stuCastFootHealthTimer),_stuCastFootHealthTimer=null);const tabs=document.getElementById("stu-cast-inner-tabs"),panel=document.getElementById("reader-audiobook-panel"),foot=tabs?tabs.querySelector("#ab-cv-foot"):null;if(foot&&panel){foot.style.borderTop="",foot.style.padding="",foot.style.justifyContent="";const openReh=foot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!1),panel.appendChild(foot)}document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!1})}function showStudioPhase(n){_stuActive=n,_stuSaveResume(n);for(let i=1;i<=4;i++){const el=document.getElementById("stu-phase-"+i);el&&(el.hidden=i!==n)}document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.classList.toggle("active",parseInt(tab.dataset.stuPhase,10)===n)}),document.querySelectorAll("#nav-caststudio-tree [data-stu-phase]").forEach(function(item){item.classList.toggle("is-active",parseInt(item.dataset.stuPhase,10)===n)});const prevBtn=document.getElementById("stu-phase-prev"),nextBtn=document.getElementById("stu-phase-next");prevBtn&&(prevBtn.disabled=n<=1),nextBtn&&(nextBtn.disabled=n>=4),typeof _stuEnterPhase=="function"&&_stuEnterPhase(n)}window.showStudioPhase=showStudioPhase,document.querySelectorAll(".stu-subtab").forEach(function(tab){tab.addEventListener("click",function(){showStudioPhase(parseInt(tab.dataset.stuPhase,10))})}),(_wc=document.getElementById("stu-phase-prev"))==null||_wc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_xc=document.getElementById("stu-phase-next"))==null||_xc.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){var _a2,_b2,_c2;_stuInstallNavGuardOnce(),_stuIsActive=!0;const resume=_stuReadResume();resume.bookId&&window._audiobook&&(window._audiobook.bookId=resume.bookId),resume.phase>=1&&resume.phase<=4&&(_stuActive=resume.phase,resume.characterId&&(window._libResumeCharacterId=resume.characterId));const resumeBookId=resume.bookId,hasCurrentBook=!!((_b2=(_a2=window.readerState)==null?void 0:_a2.sentences)!=null&&_b2.length&&((_c2=window.readerState)!=null&&_c2.savedId));if(resumeBookId&&!hasCurrentBook&&typeof window.readerOpenLibraryDoc=="function"){window.readerOpenLibraryDoc(resumeBookId).then(function(){showStudioPhase(_stuActive)});return}showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow; diff --git a/static/js/audiobook.js b/static/js/audiobook.js index a8e8303..9a054d0 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -5736,7 +5736,7 @@ async function _abSynthesizeLongText(voice, text, instruct, backend) { return mergeWavBlobs(blobs, 120); } -async function audiobookExport() { +async function audiobookExport(opts = {}) { if (_audiobook.running) return; if (!window.rehState || !(rehState.lines || []).length) { toast('Open a script in the rehearser first', 'error'); return; } if (!rehState.backend) { toast('Select a TTS backend in the rehearser first', 'error'); return; } @@ -5756,7 +5756,11 @@ async function audiobookExport() { if (audiobookIsChapter(l)) { cur = { title: (typeof stripMarkdown === 'function' ? stripMarkdown(l.text) : l.text).trim().slice(0, 50), idx: [] }; buckets.push(cur); } if (speakable(i)) { if (!cur) { cur = { title: '', idx: [] }; buckets.push(cur); } cur.idx.push(i); } }); - const allIdx = buckets.flatMap(b => b.idx); + let activeBuckets = buckets; + if (opts.selectedChapterIndices) { + activeBuckets = buckets.filter((_, i) => opts.selectedChapterIndices.includes(i)); + } + const allIdx = activeBuckets.flatMap(b => b.idx); if (!allIdx.length) { toast('Nothing to synthesise — cast voices first', 'error'); return; } _audiobook.running = true; _audiobook.cancel = false; @@ -5866,8 +5870,45 @@ async function audiobookExport() { const bookForExport = _lineAudioBookName(); const encMsg = $('audiobook-msg'); const pauses = _abPauseSettings(); - for (let c = 0; c < buckets.length; c++) { - const blobs = buckets[c].idx.map(i => wavClips.get(i)).filter(Boolean); + if (opts.format === 'single') { + let masterBlobs = []; + for (let c = 0; c < activeBuckets.length; c++) { + const blobs = activeBuckets[c].idx.map(i => wavClips.get(i)).filter(Boolean); + if (!blobs.length) continue; + if (encMsg) encMsg.textContent = `Merging chapter ${c + 1} / ${activeBuckets.length}…`; + let mergedWav = await mergeWavBlobs(blobs, pauses.paragraphMs); + if (c > 0 && activeBuckets[c].title && (pauses.chapterMs > 0 || _abChapterSfxDataUrl())) { + const ref = _parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt; + const lead = []; + const sfx = await _abChapterSfxWavBlob(ref); + if (sfx) lead.push(sfx); + if (pauses.chapterMs > 0) lead.push(_buildWavBlob(_silencePcmBytes(pauses.chapterMs, ref), ref)); + if (lead.length) mergedWav = await mergeWavBlobs([...lead, mergedWav]); + } + masterBlobs.push(mergedWav); + } + if (masterBlobs.length) { + if (encMsg) encMsg.textContent = `Encoding full book to MP3…`; + let finalWav = await mergeWavBlobs(masterBlobs, 0); + let blob = finalWav; + try { + const encResp = await fetch('/api/audio/encode-mp3', { method: 'POST', body: finalWav }); + if (encResp.ok) blob = await encResp.blob(); + } catch (e) {} + const ext = blob === finalWav ? 'wav' : 'mp3'; + const name = `${title}.${ext}`; + if (typeof readerDownload === 'function') readerDownload(blob, name); + try { + let exportUrl = `/api/audiobook-export/${encodeURIComponent(bookForExport)}/${encodeURIComponent(name)}`; + if (opts.folder) exportUrl += `?folder=${encodeURIComponent(opts.folder)}`; + const r = await fetch(exportUrl, { method: 'POST', body: blob }); + if (r.ok) savedFiles.push({ name, url: exportUrl }); + } catch (e) {} + files++; + } + } else { + for (let c = 0; c < activeBuckets.length; c++) { + const blobs = activeBuckets[c].idx.map(i => wavClips.get(i)).filter(Boolean); if (!blobs.length) continue; // mergeWavBlobs (generation.js) properly concatenates raw PCM sample // data under one RIFF header — unlike naively Blob-concatenating @@ -5876,7 +5917,7 @@ async function audiobookExport() { // A silent gap is inserted between every pair of lines (`pauses.paragraphMs`) // — mergeWavBlobs used to butt clips together with literally zero space, // which read as characters teleporting mid-scene with no beat between them. - if (encMsg) encMsg.textContent = `Merging chapter ${c + 1} / ${buckets.length}…`; + if (encMsg) encMsg.textContent = `Merging chapter ${c + 1} / ${activeBuckets.length}…`; let mergedWav = await mergeWavBlobs(blobs, pauses.paragraphMs); // Every chapter after the first gets a longer lead-in pause (and, if the // user configured one, a custom transition sound) prepended — the first @@ -5884,7 +5925,7 @@ async function audiobookExport() { // from. This survives even when chapters export as separate files (the // normal case — see `realChapters` below): most audiobook players queue // consecutive tracks back-to-back with no gap of their own. - if (c > 0 && buckets[c].title && (pauses.chapterMs > 0 || _abChapterSfxDataUrl())) { + if (c > 0 && activeBuckets[c].title && (pauses.chapterMs > 0 || _abChapterSfxDataUrl())) { const ref = _parseWavBytes(new Uint8Array(await mergedWav.arrayBuffer())).fmt; const lead = []; const sfx = await _abChapterSfxWavBlob(ref); @@ -5897,7 +5938,7 @@ async function audiobookExport() { // per-line encoding left the bitrate at ffmpeg/lame's unset default, // which came out to 32kbps, well below what even a 24kHz mono voice // source needs to avoid audible compression artifacts. - if (encMsg) encMsg.textContent = `Encoding chapter ${c + 1} / ${buckets.length}…`; + if (encMsg) encMsg.textContent = `Encoding chapter ${c + 1} / ${activeBuckets.length}…`; let blob = mergedWav; try { const encResp = await fetch('/api/audio/encode-mp3', { method: 'POST', body: mergedWav }); @@ -5922,6 +5963,7 @@ async function audiobookExport() { files++; await new Promise(r => setTimeout(r, 400)); } + } // close else block toast('Exported audiobook · ' + files + (realChapters ? ' chapter file(s)' : ' file(s)'), 'success'); if (savedFiles.length && typeof _abShowExportResults === 'function') _abShowExportResults(bookForExport, savedFiles); } @@ -5978,7 +6020,7 @@ $('reh-tb-browse-exports')?.addEventListener('click', audiobookBrowseExports); // ── Wiring ─────────────────────────────────────────────────────────────────── $('reader-audiobook-btn')?.addEventListener('click', audiobookOpenCastView); -$('reh-tb-audiobook')?.addEventListener('click', audiobookExport); +$('reh-tb-audiobook')?.addEventListener('click', audiobookExportDialog); // Build a rehearsal record object from the current segments. // If _audiobook.rehId is set, fetches the existing record so voice assignments @@ -6068,3 +6110,68 @@ function _audiobookDebouncedSave() { _abRehSaveTimer = setTimeout(() => audiobookSaveAsRehearsal({ silent: true }), 1500); } +function audiobookExportDialog() { + if (_audiobook.running) return; + if (!window.rehState || !(rehState.lines || []).length) { toast('Open a script in the rehearser first', 'error'); return; } + if (!rehState.backend) { toast('Select a TTS backend in the rehearser first', 'error'); return; } + + const speakable = i => { + const l = rehState.lines[i]; + if (!l || l.ignored || l.hidden) return false; + if (l.type === 'dialog') { const c = rehState.cast[l.speaker]; return !!(c && c.voice && c.voice !== 'me'); } + return !!(rehState.narratorVoice && (l.text || '').trim()); + }; + const buckets = []; + let cur = null; + rehState.lines.forEach((l, i) => { + if (audiobookIsChapter(l)) { cur = { title: (typeof stripMarkdown === 'function' ? stripMarkdown(l.text) : l.text).trim().slice(0, 50), idx: [] }; buckets.push(cur); } + if (speakable(i)) { if (!cur) { cur = { title: '', idx: [] }; buckets.push(cur); } cur.idx.push(i); } + }); + + const listEl = document.getElementById('ab-export-chapter-list'); + if (listEl) { + listEl.innerHTML = ''; + const countEl = document.getElementById('ab-export-chapter-count'); + if (countEl) countEl.textContent = buckets.length; + buckets.forEach((b, i) => { + const lbl = document.createElement('label'); + lbl.style.display = 'block'; + lbl.style.marginBottom = '4px'; + const chk = document.createElement('input'); + chk.type = 'checkbox'; + chk.checked = true; + chk.value = i; + chk.className = 'ab-export-chapter-cb'; + lbl.appendChild(chk); + lbl.appendChild(document.createTextNode(' Chapter ' + (i+1) + (b.title ? ': ' + b.title : ''))); + listEl.appendChild(lbl); + }); + } + + const warn = document.getElementById('reh-tone-warn'); + const slot = document.getElementById('ab-export-tone-warn-slot'); + if (warn && slot) { + warn.hidden = false; + slot.appendChild(warn); + } + + if (typeof _abInitPauseUI === 'function') _abInitPauseUI(); + + const modal = document.getElementById('audiobook-export-modal'); + if (modal) modal.hidden = false; +}; + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('ab-export-modal-close')?.addEventListener('click', () => document.getElementById('audiobook-export-modal').hidden = true); + document.getElementById('ab-export-modal-cancel')?.addEventListener('click', () => document.getElementById('audiobook-export-modal').hidden = true); + document.getElementById('ab-export-modal-start')?.addEventListener('click', () => { + document.getElementById('audiobook-export-modal').hidden = true; + const formatEl = document.querySelector('input[name="ab_export_format"]:checked'); + const format = formatEl ? formatEl.value : 'chapter'; + const folderEl = document.getElementById('ab-export-server-folder'); + const folder = folderEl ? folderEl.value : ''; + const checkboxes = Array.from(document.querySelectorAll('.ab-export-chapter-cb')); + const selectedChapterIndices = checkboxes.filter(cb => cb.checked).map(cb => parseInt(cb.value, 10)); + audiobookExport({ format, selectedChapterIndices, folder }); + }); +}); diff --git a/static/js/rehearser.js b/static/js/rehearser.js index a63c98b..41e1d8b 100644 --- a/static/js/rehearser.js +++ b/static/js/rehearser.js @@ -4065,12 +4065,9 @@ function _lineAudioCachePut(book, key, blob) { // of the current script's lines already have a matching file, and lights // up their dots — without downloading any actual audio (that still only // happens lazily, right when a line is about to play). -let _lineAudioSyncedFor = null; // `${book}::${lineCount}` — avoid re-running the same scan on every small re-render +let _lineAudioSyncedFor = null; // unused now, kept for compat async function _lineAudioSyncDots() { const book = _lineAudioBookName(); - const scanId = `${book}::${rehState.lines.length}`; - if (_lineAudioSyncedFor === scanId) return; - _lineAudioSyncedFor = scanId; _ensureNarrator(); const idxToKey = new Map(); for (let i = 0; i < rehState.lines.length; i++) { diff --git a/static/js/studio.js b/static/js/studio.js index fa90ca3..b234476 100644 --- a/static/js/studio.js +++ b/static/js/studio.js @@ -233,7 +233,6 @@ function _stuEnterPhase(n) { // further down. Order matters: this must run after the #reh-phase-3 // borrow above, since that's what puts #reh-tone-warn somewhere this // second, more specific borrow can find and re-extract it from. - _stuBorrow('reh-tone-warn', 'stu-tone-warn-slot'); if (typeof _abInitPauseUI === 'function') _abInitPauseUI(); // A stacked-sticky "toggle above the transport bar" version of this was // tried and reverted — confirmed live to break scrolling entirely on a diff --git a/static/sections/s-caststudio.html b/static/sections/s-caststudio.html index defc9f7..dcd6613 100644 --- a/static/sections/s-caststudio.html +++ b/static/sections/s-caststudio.html @@ -67,46 +67,7 @@ Generate full audiobook (voice the Narrator too) - -
- -
- Pacing (pauses between paragraphs & chapters) -
-
- -
- - seconds -
-
-
- -
- - seconds -
-
-
- -
- - - -
- Plays once before each chapter's pause, e.g. a page-turn or chime. Any audio format your browser can play. -
-
-
diff --git a/static/sections/s-rehearser.html b/static/sections/s-rehearser.html index cd315c3..6d38343 100644 --- a/static/sections/s-rehearser.html +++ b/static/sections/s-rehearser.html @@ -566,4 +566,77 @@ + + +