diff --git a/static/dist/main.min.js b/static/dist/main.min.js
index 0397876..b2208de 100644
--- a/static/dist/main.min.js
+++ b/static/dist/main.min.js
@@ -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",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(`
+ `,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(){var _a2,_b2,_c2;if(_audiobook.running)return;if(!window.rehState||!(rehState.lines||[]).length){toast("Open a script in the rehearser first","error");return}if(!rehState.backend){toast("Select a TTS backend in the rehearser first","error");return}const speakable=i=>{const l=rehState.lines[i];if(!l||l.ignored||l.hidden)return!1;if(l.type==="dialog"){const c=rehState.cast[l.speaker];return!!(c&&c.voice&&c.voice!=="me")}return!!(rehState.narratorVoice&&(l.text||"").trim())},buckets=[];let cur=null;rehState.lines.forEach((l,i)=>{audiobookIsChapter(l)&&(cur={title:(typeof stripMarkdown=="function"?stripMarkdown(l.text):l.text).trim().slice(0,50),idx:[]},buckets.push(cur)),speakable(i)&&(cur||(cur={title:"",idx:[]},buckets.push(cur)),cur.idx.push(i))});const listEl=document.getElementById("ab-export-chapter-list");if(listEl){listEl.innerHTML="";const countEl=document.getElementById("ab-export-chapter-count");countEl&&(countEl.textContent=buckets.length),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.parentElement!==document.body&&(document.body.appendChild(modal),(_a2=document.getElementById("ab-export-modal-close"))==null||_a2.addEventListener("click",()=>modal.hidden=!0),(_b2=document.getElementById("ab-export-modal-cancel"))==null||_b2.addEventListener("click",()=>modal.hidden=!0),(_c2=document.getElementById("ab-export-modal-start"))==null||_c2.addEventListener("click",()=>{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})})),modal.hidden=!1)}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 ' *'.
diff --git a/static/js/audiobook.js b/static/js/audiobook.js
index 9a054d0..17e34cf 100644
--- a/static/js/audiobook.js
+++ b/static/js/audiobook.js
@@ -6158,20 +6158,24 @@ function audiobookExportDialog() {
if (typeof _abInitPauseUI === 'function') _abInitPauseUI();
const modal = document.getElementById('audiobook-export-modal');
- if (modal) modal.hidden = false;
-};
+ if (modal) {
+ if (modal.parentElement !== document.body) {
+ document.body.appendChild(modal);
+ // Bind event listeners once when the modal is first moved
+ document.getElementById('ab-export-modal-close')?.addEventListener('click', () => modal.hidden = true);
+ document.getElementById('ab-export-modal-cancel')?.addEventListener('click', () => modal.hidden = true);
+ document.getElementById('ab-export-modal-start')?.addEventListener('click', () => {
+ 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 });
+ });
+ }
+ 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 });
- });
-});