tts-voice-creator-clone-and.../static/dist/main.min.js
mARTin-B78 ca654608ac Never merge adjacent Unknown dialogue lines into one segment (v1.20.27)
Adjacent segments merge when they share a speaker, but an unattributed line
carries the speaker "Unknown", so two consecutive Unknown lines compared as
equal and were fused into a single block joined by a blank line. Confirmed
against the source text: Lysandra's line and Perdia's interruption — two
separate paragraphs in the book — became one segment, after which no
attribution pass could separate them and one voice would read both sides of
the exchange.

"Unknown" means the speaker is not known, never that two lines share one.
Narration and lines with a real speaker name merge exactly as before.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 10:46:55 +02:00

1826 lines
1.1 MiB
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_z,_A,_B,_C,_D,_E,_F,_G,_H,_I,_J,_K,_L,_M,_N,_O,_P,_Q,_R,_S,_T,_U,_V,_W,_X,_Y,_Z,__,_$,_aa,_ba,_ca,_da,_ea,_fa,_ga,_ha,_ia,_ja,_ka,_la,_ma,_na,_oa,_pa,_qa,_ra,_sa,_ta,_ua,_va,_wa,_xa,_ya,_za,_Aa,_Ba,_Ca,_Da,_Ea,_Fa,_Ga,_Ha,_Ia,_Ja,_Ka,_La,_Ma,_Na,_Oa,_Pa,_Qa,_Ra,_Sa,_Ta,_Ua,_Va,_Wa,_Xa,_Ya,_Za,__a,_$a,_ab,_bb,_cb,_db,_eb,_fb,_gb,_hb,_ib,_jb,_kb,_lb,_mb,_nb,_ob,_pb,_qb,_rb,_sb,_tb,_ub,_vb,_wb,_xb,_yb,_zb,_Ab,_Bb,_Cb,_Db,_Eb,_Fb,_Gb,_Hb,_Ib,_Jb,_Kb,_Lb,_Mb,_Nb,_Ob,_Pb,_Qb,_Rb,_Sb,_Tb,_Ub,_Vb,_Wb,_Xb,_Yb,_Zb,__b,_$b,_ac,_bc,_cc,_dc,_ec,_fc,_gc,_hc,_ic,_jc,_kc,_lc,_mc,_nc,_oc,_pc,_qc,_rc,_sc,_tc,_uc,_vc;(function(){"use strict";const _pickers={};function _voiceData(id){return(window._voices||[]).find(v=>v.id===id)||null}function _voiceLang(v,id){const raw=String((v==null?void 0:v.lang)||(v==null?void 0:v.language)||"").trim(),fromMeta=raw&&raw.length<=5?raw:"",fromId=String(id||"").split("_")[0]||"";return(fromMeta||fromId).toUpperCase()}function _voiceGender(v,id){const raw=String((v==null?void 0:v.gender)||(v==null?void 0:v.sex)||"").trim(),first=raw?raw.charAt(0).toUpperCase():"";if(["F","M","N"].includes(first))return first;const fromId=(String(id||"").split("_")[1]||"").charAt(0).toUpperCase();return["F","M","N"].includes(fromId)?fromId:""}function _voiceMetaLabel(id){const v=_voiceData(id);return[_voiceGender(v,id),_voiceLang(v,id)].filter(Boolean).join(" ")}function _voiceOptionLabel(id){const meta=_voiceMetaLabel(id);return meta?`${id} ${meta}`:id}const VOICE_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"},VOICE_AVATAR_COLORS={male:"#3b82f6",female:"#ec4899",neutral:"#6b7280",robot:"#0ea5e9",animal:"#f59e0b"};window.VOICE_AVATAR_ICONS=VOICE_AVATAR_ICONS,window.voiceAvatarIcon=function(avatarKey,size){const icon=VOICE_AVATAR_ICONS[avatarKey];if(!icon)return null;const s=size+"px",r=Math.round(size/2)+"px",bg=VOICE_AVATAR_COLORS[avatarKey]||"#6b7280";return`<span class="vp-avatar" style="width:${s};height:${s};border-radius:${r};background:${bg};color:#fff;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><span class="mdi ${icon}" style="font-size:${Math.round(size*.58)}px"></span></span>`};function _avatarHtml(id,size){const v=_voiceData(id),s=size+"px",r=Math.round(size/2)+"px";if(v!=null&&v.has_picture)return`<img src="/api/voice/picture/${encodeURIComponent(id)}" class="vp-avatar" style="width:${s};height:${s};border-radius:${r};object-fit:cover;flex-shrink:0" alt="">`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(v==null?void 0:v.avatar,size):null;if(icon)return icon;const lang=(v==null?void 0:v.lang)||"",color=_langColor(lang,id),init=(id||"?")[0].toUpperCase();return`<span class="vp-avatar" style="width:${s};height:${s};border-radius:${r};background:${color};color:#fff;font-weight:700;font-size:${Math.round(size*.44)}px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0">${init}</span>`}function _langColor(lang,id){const str=(lang||id||"").toLowerCase();if(str.startsWith("de"))return"#3b82f6";if(str.startsWith("en"))return"#10b981";if(str.startsWith("fr"))return"#8b5cf6";if(str.startsWith("es"))return"#f59e0b";if(str.startsWith("it"))return"#ef4444";if(str.startsWith("zh"))return"#ec4899";if(str.startsWith("ja"))return"#f97316";const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<str.length;i++)h=h*31+str.charCodeAt(i)>>>0;return palette[h%palette.length]}function _flagSpan(v){return v&&v.flag?`<span class="vp-flag">${v.flag}</span>`:""}function _buildItem(id,label){const v=_voiceData(id),meta=_voiceMetaLabel(id),name=id||label||"";return`<div class="vp-item" data-value="${_esc(id)}" tabindex="-1">
${_avatarHtml(id,26)}
<span class="vp-item-name">${_esc(name)}</span>
${meta?`<span class="vp-item-meta">${_esc(meta)}</span>`:_flagSpan(v)}
</div>`}function _esc(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function upgrade(selectId){const sel=document.getElementById(selectId);if(!sel||sel.dataset.vpUpgraded)return;sel.dataset.vpUpgraded="1",sel.style.display="none";const root=document.createElement("div");root.className="vp-root",root.dataset.vpFor=selectId,sel.parentNode.insertBefore(root,sel);const trigger=document.createElement("div");trigger.className="vp-trigger",trigger.tabIndex=0,trigger.innerHTML='<span class="vp-trigger-content"><span class="vp-trigger-placeholder">\u2014 select after fetch \u2014</span></span><span class="mdi mdi-chevron-down vp-chevron"></span>',root.appendChild(trigger);const drop=document.createElement("div");drop.className="vp-drop",drop.hidden=!0,drop.innerHTML='<div class="vp-search-wrap"><span class="mdi mdi-magnify vp-search-icon"></span><input class="vp-search" placeholder="Search voices\u2026" autocomplete="off" spellcheck="false"></div><div class="vp-list"></div>',root.appendChild(drop);const searchInp=drop.querySelector(".vp-search"),list=drop.querySelector(".vp-list");function _renderList(filter){const opts=Array.from(sel.options).filter(o=>o.value),q=(filter||"").toLowerCase(),filtered=q?opts.filter(o=>o.value.toLowerCase().includes(q)||o.textContent.toLowerCase().includes(q)):opts;if(!filtered.length){list.innerHTML='<div class="vp-empty">No voices found</div>';return}list.innerHTML=filtered.map(o=>_buildItem(o.value,o.textContent)).join(""),list.querySelectorAll(".vp-item").forEach(el=>{el.classList.toggle("selected",el.dataset.value===sel.value)})}function _open(){drop.hidden=!1,drop.style.display="",trigger.classList.add("open"),searchInp.value="",_renderList(""),searchInp.focus(),requestAnimationFrame(()=>{const sel2=list.querySelector(".vp-item.selected");sel2&&sel2.scrollIntoView({block:"nearest"})})}function _close(){drop.hidden=!0,drop.style.display="none",trigger.classList.remove("open"),searchInp.blur()}function _select(value){sel.value=value,sel.dispatchEvent(new Event("change",{bubbles:!0})),_syncTrigger(),_close(),trigger.focus()}function _syncTrigger(){const v=sel.value,content=trigger.querySelector(".vp-trigger-content");if(!v)content.innerHTML='<span class="vp-trigger-placeholder">\u2014 select after fetch \u2014</span>';else{const label=v,meta=_voiceMetaLabel(v);content.innerHTML=`${_avatarHtml(v,24)}<span class="vp-trigger-name">${_esc(label)}</span>${meta?`<span class="vp-trigger-meta">${_esc(meta)}</span>`:""}`}}trigger.addEventListener("click",()=>drop.hidden?_open():_close()),trigger.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),_open())}),searchInp.addEventListener("input",()=>_renderList(searchInp.value)),searchInp.addEventListener("keydown",e=>{var _a2;e.key==="Escape"&&(e.stopPropagation(),_close(),trigger.focus()),e.key==="ArrowDown"&&(e.preventDefault(),(_a2=list.querySelector(".vp-item"))==null||_a2.focus())}),list.addEventListener("pointerdown",e=>{const item=e.target.closest(".vp-item");item&&(e.preventDefault(),_select(item.dataset.value))}),list.addEventListener("click",e=>{const item=e.target.closest(".vp-item");item&&_select(item.dataset.value)}),list.addEventListener("keydown",e=>{var _a2;const item=e.target.closest(".vp-item");item&&((e.key==="Enter"||e.key===" ")&&(e.preventDefault(),_select(item.dataset.value)),e.key==="ArrowDown"&&(e.preventDefault(),(item.nextElementSibling||item).focus()),e.key==="ArrowUp"&&(e.preventDefault(),(_a2=item.previousElementSibling||item)!=null&&_a2.focus()||searchInp.focus()),e.key==="Escape"&&(_close(),trigger.focus()))}),document.addEventListener("click",e=>{root.contains(e.target)||_close()},!0),sel.addEventListener("change",()=>{_syncTrigger(),_close()}),new MutationObserver(()=>{_syncTrigger()}).observe(sel,{childList:!0,attributes:!0,subtree:!0}),_pickers[selectId]={root,trigger,drop,searchInp,list,sel,renderList:_renderList,syncTrigger:_syncTrigger}}function populate(selectId,voices){const p=_pickers[selectId];if(!p)return;const{sel,renderList,syncTrigger}=p,prev=sel.value;sel.innerHTML='<option value="">\u2014 select after fetch \u2014</option>',voices.forEach(id=>{const opt=document.createElement("option");opt.value=id,opt.textContent=_voiceOptionLabel(id),sel.appendChild(opt)}),prev&&voices.includes(prev)&&(sel.value=prev),renderList(""),syncTrigger()}function getValue(selectId){var _a2;return((_a2=document.getElementById(selectId))==null?void 0:_a2.value)||""}function setValue(selectId,value){var _a2;const sel=document.getElementById(selectId);sel&&(sel.value=value,(_a2=_pickers[selectId])==null||_a2.syncTrigger())}function attachTextPicker(input){if(!input||input.dataset.vpTextAttached)return;input.dataset.vpTextAttached="1";const btn=document.createElement("button");btn.type="button",btn.className="vp-text-browse",btn.title="Browse voices",btn.innerHTML='<span class="mdi mdi-chevron-down"></span>',input.insertAdjacentElement("afterend",btn);const drop=document.createElement("div");drop.className="vp-drop vp-drop-floating",drop.hidden=!0,drop.innerHTML='<div class="vp-search-wrap"><span class="mdi mdi-magnify vp-search-icon"></span><input class="vp-search" placeholder="Search voices\u2026" autocomplete="off" spellcheck="false"></div><div class="vp-list"></div>',document.body.appendChild(drop);const searchInp=drop.querySelector(".vp-search"),list=drop.querySelector(".vp-list");function _voices2(){return(window._voices||[]).filter(v=>v.enabled!==!1)}function _renderList(filter){const q=(filter||"").toLowerCase(),filtered=q?_voices2().filter(v=>v.id.toLowerCase().includes(q)):_voices2();if(!filtered.length){list.innerHTML='<div class="vp-empty">No voices found</div>';return}list.innerHTML=filtered.map(v=>_buildItem(v.id,v.id)).join(""),list.querySelectorAll(".vp-item").forEach(el=>el.classList.toggle("selected",el.dataset.value===input.value))}function _position(){const r=input.getBoundingClientRect();drop.style.left=r.left+"px",drop.style.top=r.bottom+4+"px",drop.style.width=Math.max(240,r.width+32)+"px"}function _open(){_position(),drop.hidden=!1,searchInp.value="",_renderList(""),searchInp.focus()}function _close(){drop.hidden=!0}function _select(value){input.value=value,input.dispatchEvent(new Event("input",{bubbles:!0})),input.dispatchEvent(new Event("change",{bubbles:!0})),_close()}btn.addEventListener("click",e=>{e.stopPropagation(),drop.hidden?_open():_close()}),searchInp.addEventListener("input",()=>_renderList(searchInp.value)),searchInp.addEventListener("keydown",e=>{e.key==="Escape"&&(e.stopPropagation(),_close())}),list.addEventListener("click",e=>{const item=e.target.closest(".vp-item");item&&_select(item.dataset.value)}),document.addEventListener("click",e=>{!drop.contains(e.target)&&e.target!==btn&&_close()},!0),window.addEventListener("resize",()=>{drop.hidden||_position()})}window.VoicePicker={upgrade,populate,getValue,setValue,attachTextPicker}})(),function(){"use strict";const pickers={};function esc(value){return String(value==null?"":value).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function norm(value){if(typeof value=="string")return{id:value,label:value,meta:null,raw:value};const id=(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||(value==null?void 0:value.voice_id)||"",hasExplicitMeta=Object.prototype.hasOwnProperty.call(value||{},"meta"),looksLikeVoice=!!(value!=null&&value.display_name||value!=null&&value.duration||value!=null&&value.benchmark||value!=null&&value.has_picture||value!=null&&value.transcript);return{id,label:(value==null?void 0:value.label)||(value==null?void 0:value.display_name)||(value==null?void 0:value.name)||(value==null?void 0:value.voice)||id,meta:hasExplicitMeta?value.meta:looksLikeVoice?value:null,raw:value}}function libraryVoice(id){return(window._voices||[]).find(v=>v&&v.id===id)||null}function metaFor(item){return(item==null?void 0:item.meta)||libraryVoice(item==null?void 0:item.id)||(item==null?void 0:item.raw)||null}function langColor(lang,id){const str=String(lang||id||"").toLowerCase();if(str.startsWith("de"))return"#3b82f6";if(str.startsWith("en"))return"#10b981";if(str.startsWith("fr"))return"#8b5cf6";if(str.startsWith("es"))return"#f59e0b";if(str.startsWith("it"))return"#ef4444";if(str.startsWith("zh"))return"#ec4899";if(str.startsWith("ja"))return"#f97316";const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<str.length;i++)h=h*31+str.charCodeAt(i)>>>0;return palette[h%palette.length]}function avatarHtml(item,size){var _a2;const id=(item==null?void 0:item.id)||"",meta=metaFor(item),s=`${size}px`,r=`${Math.round(size/2)}px`;if(meta!=null&&meta.has_picture)return`<img src="/api/voice/picture/${encodeURIComponent(id)}" class="bvp-avatar" style="width:${s};height:${s};border-radius:${r};object-fit:cover" alt="">`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta==null?void 0:meta.avatar,size):null;if(icon)return icon.replace(/vp-avatar/g,"bvp-avatar");const color=langColor((meta==null?void 0:meta.lang)||(meta==null?void 0:meta.language),id),init=((_a2=((item==null?void 0:item.label)||id||"?").trim()[0])==null?void 0:_a2.toUpperCase())||"?";return`<span class="bvp-avatar" style="width:${s};height:${s};border-radius:${r};background:${color};color:#fff;font-weight:700;font-size:${Math.round(size*.44)}px">${esc(init)}</span>`}function numeric(value){const n=Number(value);return Number.isFinite(n)?n:null}function statsFor(item){var _a2,_b2,_c2,_d2,_e2,_f2;const meta=metaFor(item)||{},b=meta.benchmark||(item==null?void 0:item.benchmark)||{},parts=[],duration=numeric((_a2=meta.duration)!=null?_a2:item==null?void 0:item.duration),speed=numeric((_c2=(_b2=b.speed)!=null?_b2:b.avg_speed)!=null?_c2:meta.speed),rtf=numeric((_e2=(_d2=b.rtf)!=null?_d2:b.avg_rtf)!=null?_e2:meta.rtf),elapsed=numeric((_f2=b.elapsed_sec)!=null?_f2:b.avg_elapsed_sec);return duration!=null&&duration>0&&parts.push(`${duration.toFixed(1)}s`),speed!=null&&speed>0?parts.push(`${speed.toFixed(2)}x`):rtf!=null&&rtf>0&&parts.push(`RTF ${rtf.toFixed(2)}`),!parts.length&&elapsed!=null&&elapsed>0&&parts.push(`${elapsed.toFixed(1)}s TTS`),!parts.length&&b.ok===!1&&parts.push("benchmark failed"),parts.join(" \xB7 ")}function searchText(item){const meta=metaFor(item)||{};return[item==null?void 0:item.id,item==null?void 0:item.label,meta.display_name,meta.name,meta.lang,meta.language,Array.isArray(meta.tags)?meta.tags.join(" "):meta.tags,statsFor(item)].filter(Boolean).join(" ").toLowerCase()}function optionLabel(item){const stats=statsFor(item);return`${item.label||item.id}${stats?` (${stats})`:""}`}function renderTrigger(picker){const value=picker.select.value,item=picker.items.find(v=>v.id===value),content=picker.trigger.querySelector(".bvp-trigger-content");if(!item){content.innerHTML=`<span class="bvp-placeholder">${esc(picker.placeholder)}</span>`;return}const stats=statsFor(item);content.innerHTML=`${avatarHtml(item,24)}<span class="bvp-trigger-main"><span class="bvp-trigger-name">${esc(item.label||item.id)}</span>${stats?`<span class="bvp-trigger-stats">${esc(stats)}</span>`:""}</span>`}function renderList(picker,filter){const q=String(filter||"").trim().toLowerCase(),items=q?picker.items.filter(item=>searchText(item).includes(q)):picker.items;if(!items.length){picker.list.innerHTML=`<div class="bvp-empty">${esc(q?"No voices found":picker.empty)}</div>`;return}picker.list.innerHTML=items.map(item=>{const meta=metaFor(item)||{},stats=statsFor(item),sub=[meta.lang||meta.language,meta.file_type].filter(Boolean).join(" \xB7 ");return`<button class="bvp-item${item.id===picker.select.value?" selected":""}" type="button" data-value="${esc(item.id)}">
${avatarHtml(item,28)}
<span class="bvp-item-main"><span class="bvp-item-name">${esc(item.label||item.id)}</span>${sub?`<span class="bvp-item-sub">${esc(sub)}</span>`:""}</span>
${stats?`<span class="bvp-item-stats">${esc(stats)}</span>`:""}
</button>`}).join("")}function close(picker){picker&&(picker.drop.hidden=!0,picker.drop.style.display="none",picker.trigger.classList.remove("open"))}function closeAll(except){Object.values(pickers).forEach(p=>{p!==except&&close(p)})}function open(picker){closeAll(picker),picker.drop.hidden=!1,picker.drop.style.display="",picker.trigger.classList.add("open"),picker.search.value="",renderList(picker,""),picker.search.focus(),requestAnimationFrame(()=>{var _a2;return(_a2=picker.list.querySelector(".bvp-item.selected"))==null?void 0:_a2.scrollIntoView({block:"nearest"})})}function setValue(picker,value,dispatch){picker.select.value=value||"",renderTrigger(picker),renderList(picker,picker.search.value),close(picker),dispatch!==!1&&picker.select.dispatchEvent(new Event("change",{bubbles:!0})),picker.trigger.focus()}function upgrade(selectId,options){var _a2;const select=document.getElementById(selectId);if(!select)return null;if(pickers[selectId])return pickers[selectId];const picker={select,items:[],placeholder:(options==null?void 0:options.placeholder)||((_a2=Array.from(select.options).find(o=>!o.value))==null?void 0:_a2.textContent)||"\u2014 select after fetch \u2014",empty:(options==null?void 0:options.empty)||"No voices"};select.dataset.bvpUpgraded="1",select.style.display="none";const root=document.createElement("div");return root.className="bvp-root",root.dataset.bvpFor=selectId,select.parentNode.insertBefore(root,select),root.innerHTML=`<button class="bvp-trigger" type="button">
<span class="bvp-trigger-content"><span class="bvp-placeholder">${esc(picker.placeholder)}</span></span>
<span class="mdi mdi-chevron-down bvp-chevron"></span>
</button>
<div class="bvp-drop" hidden>
<div class="bvp-search-wrap"><span class="mdi mdi-magnify bvp-search-icon"></span><input class="bvp-search" type="search" placeholder="Search voices..." autocomplete="off" spellcheck="false"></div>
<div class="bvp-list"></div>
</div>`,Object.assign(picker,{root,trigger:root.querySelector(".bvp-trigger"),drop:root.querySelector(".bvp-drop"),search:root.querySelector(".bvp-search"),list:root.querySelector(".bvp-list")}),picker.trigger.addEventListener("click",e=>{e.preventDefault(),picker.drop.hidden?open(picker):close(picker)}),picker.trigger.addEventListener("keydown",e=>{var _a3;(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),open(picker)),e.key==="ArrowDown"&&(e.preventDefault(),open(picker),(_a3=picker.list.querySelector(".bvp-item"))==null||_a3.focus())}),picker.search.addEventListener("input",()=>renderList(picker,picker.search.value)),picker.search.addEventListener("keydown",e=>{var _a3;e.key==="Escape"&&(e.preventDefault(),close(picker),picker.trigger.focus()),e.key==="ArrowDown"&&(e.preventDefault(),(_a3=picker.list.querySelector(".bvp-item"))==null||_a3.focus())}),picker.list.addEventListener("click",e=>{const item=e.target.closest(".bvp-item");item&&setValue(picker,item.dataset.value,!0)}),picker.list.addEventListener("keydown",e=>{const item=e.target.closest(".bvp-item");item&&((e.key==="Enter"||e.key===" ")&&(e.preventDefault(),setValue(picker,item.dataset.value,!0)),e.key==="ArrowDown"&&(e.preventDefault(),(item.nextElementSibling||item).focus()),e.key==="ArrowUp"&&(e.preventDefault(),(item.previousElementSibling||picker.search).focus()),e.key==="Escape"&&(e.preventDefault(),close(picker),picker.trigger.focus()))}),select.addEventListener("change",()=>{renderTrigger(picker),close(picker)}),document.addEventListener("click",e=>{root.contains(e.target)||close(picker)},!0),pickers[selectId]=picker,syncFromSelect(selectId),picker}function populate(selectId,values,options){var _a2;const picker=upgrade(selectId,options||{});if(!picker)return;picker.placeholder=(options==null?void 0:options.placeholder)||picker.placeholder,picker.empty=(options==null?void 0:options.empty)||picker.empty;const previous=picker.select.value;picker.items=(values||[]).map(norm).filter(item=>item.id),picker.select.innerHTML=`<option value="">${esc(picker.placeholder)}</option>`+picker.items.map(item=>`<option value="${esc(item.id)}">${esc(optionLabel(item))}</option>`).join(""),previous&&picker.items.some(item=>item.id===previous)&&(picker.select.value=previous),renderTrigger(picker),renderList(picker,((_a2=picker.search)==null?void 0:_a2.value)||"")}function syncFromSelect(selectId){const picker=pickers[selectId];picker&&(picker.items=Array.from(picker.select.options).filter(o=>o.value).map(o=>({id:o.value,label:o.textContent||o.value,meta:libraryVoice(o.value)||{}})),renderTrigger(picker),renderList(picker,""))}function getValue(selectId){var _a2;return((_a2=document.getElementById(selectId))==null?void 0:_a2.value)||""}function set(selectId,value){const picker=pickers[selectId]||upgrade(selectId);picker&&setValue(picker,value,!0)}window.BenchmarkVoicePicker={upgrade,populate,syncFromSelect,getValue,set}}();let _selectedVoiceWrap=null;function selectVoice(wrap){var _a2;const inspector=document.getElementById("voices-inspector");if(!inspector)return;function restoreToRow(targetWrap){(targetWrap._extracted||[]).forEach(({el,target})=>{el&&target&&target.appendChild(el)}),targetWrap._extracted=[],inspector.querySelectorAll(".vr-main-row,.vr-optimizer").forEach(el=>targetWrap.appendChild(el))}if(_selectedVoiceWrap&&_selectedVoiceWrap!==wrap&&(_selectedVoiceWrap.classList.remove("vr-selected"),restoreToRow(_selectedVoiceWrap)),_selectedVoiceWrap===wrap){restoreToRow(wrap),wrap.classList.remove("vr-selected"),_selectedVoiceWrap=null,inspector.innerHTML='<div class="inspector-placeholder"><span><span class="mdi mdi-account-voice"></span></span><p>Pick a voice on the left<br>to edit it here</p></div>',typeof window.onMobileInspectorClose=="function"&&window.onMobileInspectorClose();return}_selectedVoiceWrap=wrap,wrap.classList.add("vr-selected"),typeof window.onMobileInspectorOpen=="function"&&window.onMobileInspectorOpen();const voiceId=wrap.dataset.id||"",color=wrap.dataset.color||"#9575CD",isClone=wrap.dataset.hasRef==="true",dbfs=wrap.dataset.dbfs||"-",hasPicture=wrap.dataset.hasPicture==="true",v=(_voices||[]).find(vv=>vv.id===voiceId)||{},langCode=v.lang||wrap.dataset.lang||"",flagCc=v.flag||wrap.dataset.flagCc||langCode,rating=v.rating||0,curGender=v.gender||"N",nameParts=voiceId.split("_"),dispName=v.name&&String(v.name).trim()?String(v.name).trim():nameParts.length>1?nameParts[nameParts.length-1]:voiceId,initial=(dispName[0]||voiceId[0]||"?").toUpperCase(),picSrcInsp=hasPicture?`/api/voice/picture/${encodeURIComponent(voiceId)}`:null,inspFlagCc=flagCc||langCode,inspFlagIcon=inspFlagCc?`<span class="fi fi-${inspFlagCc.toLowerCase()}" role="img" aria-label="${langCode}"></span>`:null,silHtml=`<span class="mdi ${curGender==="M"?"mdi-face-man":curGender==="F"?"mdi-face-woman":"mdi-account"}" style="color:#fff;font-size:32px;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:${color}"></span>`,inspIconHtml=window.voiceAvatarIcon?window.voiceAvatarIcon(v.avatar,48):null,avatarHtml=picSrcInsp?`<img src="${picSrcInsp}" alt="" class="insp-avatar-img">`:inspIconHtml||silHtml,avatarBgStyle=picSrcInsp||inspIconHtml?"":`style="background:${color}"`,flagIconHtml=inspFlagIcon||"",makeStars=n=>[1,2,3,4,5].map(i=>`<span class="insp-star${i<=n?" on":""}" data-val="${i}"><span class="mdi mdi-star"></span></span>`).join(""),inspBenchText=typeof fmtBenchmark=="function"?fmtBenchmark(v):"-",inspBenchTitle=typeof benchmarkTitle=="function"?benchmarkTitle(v):"",inspBenchCls=typeof benchmarkClass=="function"?benchmarkClass(v):"",LANGS=["EN","DE","IT","ES","FR","PT","NL","PL","ZH","JA","KO","AR","RU","TR","HI","SV","DA","FI","NB","HU","CS","RO","UK"],_gM={F:"\u2640",M:"\u2642",N:"\u26A5","":"?"},_gL={F:"Female",M:"Male",N:"Diverse","":"\u2014"},_gC={F:"g-f",M:"g-m",N:"g-n","":"g-n"},genderLabelHtml=`${_gM[curGender]||"?"} ${_gL[curGender]||"\u2014"}`;inspector.innerHTML=`
<div class="inspector-header">
<div class="insp-hd-top">
<div class="inspector-avatar${picSrcInsp?" insp-avatar-photo":""} insp-avatar-clickable" ${avatarBgStyle} title="Click to change photo">${avatarHtml}</div>
<div class="insp-title-stack">
<div class="insp-hd-row1">
<h3 class="insp-disp-name" title="Double-click to rename the display name">${escHtml(dispName)}</h3>
<input class="insp-name-edit" value="${escHtml(voiceId)}" spellcheck="false" style="display:none" placeholder="Voice ID">
<div class="insp-actions-save"></div>
</div>
<div class="insp-hd-row2">
<div class="insp-id-block">
<span class="insp-full-id" title="This is also the voice's filename on disk \u2014 click to rename it (updates the .wav, .json and .txt files too)">${escHtml(voiceId)}</span>
<button class="insp-edit-id-btn" type="button" title="Rename this voice's files (.wav, .json, .txt) \u2014 the voice ID is its filename"><span class="mdi mdi-pencil-outline"></span> rename filename</button>
<button class="insp-copy-id-btn" type="button" title="Copy filename">copy filename</button>
</div>
<div class="insp-hd-row2-right" style="display:flex; flex-direction:column; align-items:flex-end; gap:4px;">
<div class="insp-actions-active"></div>
${v.seed!==void 0&&v.seed!==null?`<div class="insp-pinned-seed" style="color:#d32f2f; font-weight:600; font-size:12px;">Pin Seed # ${v.seed}</div>`:'<div class="insp-pinned-seed" style="color:#d32f2f; font-weight:600; font-size:12px; display:none;"></div>'}
</div>
</div>
<div class="insp-hd-divider"></div>
<div class="insp-subtitle">
<span class="insp-flag" title="Click to change accent flag">
<span class="insp-flag-icon">${flagIconHtml}</span>
</span>
<span class="insp-lang-wrap" title="Click to change language">
<span class="insp-lang-code">${escHtml(langCode)}</span>
</span>
<span class="insp-gender-label" title="Click to cycle gender">${escHtml(genderLabelHtml)}</span>
<span class="vl-type-label ${isClone?"vl-type-clone":"vl-type-design"}">${isClone?"Clone":"Design"}</span>
${inspBenchText!=="-"?`<span class="insp-bench-chip ${escHtml(inspBenchCls)}" title="${escHtml(inspBenchTitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(inspBenchText)}</span>`:""}
<span class="insp-stars-wrap">
<span class="insp-stars">${makeStars(rating)}</span>
<span class="insp-rating-label">${rating}/5</span>
</span>
<input class="insp-tag-input" type="text" placeholder="tag1, tag2\u2026" value="${escHtml(v.tag||"")}" autocomplete="off">
</div>
<div class="insp-note-delete-row">
<div class="insp-note-slot"></div>
<div class="insp-actions-delete"></div>
</div>
</div>
</div>
</div>
<div class="inspector-body"></div>
`,window.innerWidth<=767&&typeof window.onMobileInspectorOpen=="function"&&window.onMobileInspectorOpen();const saveSlot=inspector.querySelector(".insp-actions-save"),activeSlot=inspector.querySelector(".insp-actions-active"),deleteSlot=inspector.querySelector(".insp-actions-delete"),body=inspector.querySelector(".inspector-body"),saveBtn=document.createElement("button");saveBtn.className="btn-primary insp-save-btn",saveBtn.textContent="Save changes",saveBtn.addEventListener("click",()=>{var _a3;return(_a3=body.querySelector(".opt-save-text"))==null?void 0:_a3.click()}),saveSlot.appendChild(saveBtn);const detailRow=wrap.querySelector(".vr-detail-row"),activeEl=detailRow==null?void 0:detailRow.querySelector(".vr-detail-active"),deleteEl=detailRow==null?void 0:detailRow.querySelector(".vr-detail-delete");if(activeEl&&activeSlot.appendChild(activeEl),deleteEl){deleteSlot.appendChild(deleteEl);const _dBtn=deleteEl.querySelector(".delete-btn"),_dCnl=deleteEl.querySelector(".delete-confirm-cancel");_dBtn==null||_dBtn.addEventListener("click",()=>deleteEl.classList.add("delete-pending")),_dCnl==null||_dCnl.addEventListener("click",()=>deleteEl.classList.remove("delete-pending"))}const inspAvatar=inspector.querySelector(".inspector-avatar");inspAvatar.addEventListener("click",()=>{var _a3;(_a3=body.querySelector(".photo-input"))==null||_a3.click()}),inspAvatar.addEventListener("dragenter",e=>{e.preventDefault(),inspAvatar.classList.add("drag-over")}),inspAvatar.addEventListener("dragover",e=>{e.preventDefault(),inspAvatar.classList.add("drag-over")}),inspAvatar.addEventListener("dragleave",()=>inspAvatar.classList.remove("drag-over")),inspAvatar.addEventListener("drop",async e=>{if(e.preventDefault(),inspAvatar.classList.remove("drag-over"),e.dataTransfer.files&&e.dataTransfer.files.length>0){const photoInput=body.querySelector(".photo-input");photoInput&&(photoInput.files=e.dataTransfer.files,photoInput.dispatchEvent(new Event("change")));return}let url=e.dataTransfer.getData("text/uri-list");if(!url){const html=e.dataTransfer.getData("text/html");if(html){const match=html.match(/src=["'](.*?)["']/);match&&(url=match[1])}}if(url||(url=e.dataTransfer.getData("text/plain")),url&&/^https?:\/\//i.test(url)){typeof status=="function"&&status("Downloading picture from URL...");try{const r=await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,image_url:url})});if(!r.ok)throw new Error((await r.json()).detail);const photoCell=body.querySelector(".vr-photo");photoCell&&photoCell.dispatchEvent(new Event("update-photo")),typeof toast=="function"&&toast("Photo saved from URL","success"),typeof status=="function"&&status("Photo saved successfully")}catch(err){typeof toast=="function"&&toast("Photo URL download failed: "+err.message,"error"),typeof status=="function"&&status("Photo URL download failed")}}}),inspector.querySelector(".insp-copy-id-btn").addEventListener("click",()=>{copyText(voiceId).then(()=>toast("Copied: "+voiceId))});const dispNameEl=inspector.querySelector(".insp-disp-name"),nameEditEl=inspector.querySelector(".insp-name-edit"),fullIdEl=inspector.querySelector(".insp-full-id");let _editMode=null,_editCancelled=!1;const showEditor=(mode,val)=>{_editMode=mode,_editCancelled=!1,dispNameEl.style.display="none",fullIdEl.style.display="none",nameEditEl.style.display="block",nameEditEl.placeholder=mode==="name"?"Display name":"Voice ID",nameEditEl.value=val,nameEditEl.focus(),nameEditEl.select()},startNameEdit=()=>showEditor("name",dispName),startInspRename=()=>showEditor("id",voiceId),commitEdit=async()=>{const mode=_editMode;if(_editMode=null,dispNameEl.style.display="",fullIdEl.style.display="",nameEditEl.style.display="none",_editCancelled||!mode)return;const val=nameEditEl.value.trim();if(mode==="id"){if(!val||val===voiceId)return;const rowInput=wrap.querySelector(".vr-name-input"),rowOk=wrap.querySelector(".rename-ok");rowInput&&rowOk&&(rowInput.value=val,rowOk.click());return}if(val!==dispName)try{const r=await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,name:val})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}v.name=val,dispNameEl.textContent=val||voiceId.split("_").pop(),typeof toast=="function"&&toast("Display name updated","success"),typeof renderVoiceList=="function"&&renderVoiceList()}catch(e){typeof toast=="function"&&toast("Rename failed: "+(e.message||e),"error")}};dispNameEl.addEventListener("dblclick",startNameEdit),fullIdEl.addEventListener("dblclick",startInspRename),fullIdEl.addEventListener("click",startInspRename),(_a2=inspector.querySelector(".insp-edit-id-btn"))==null||_a2.addEventListener("click",startInspRename),nameEditEl.addEventListener("blur",commitEdit),nameEditEl.addEventListener("keydown",e=>{e.key==="Enter"&&nameEditEl.blur(),e.key==="Escape"&&(_editCancelled=!0,nameEditEl.blur())});const flagSpan=inspector.querySelector(".insp-flag"),flagIconEl=inspector.querySelector(".insp-flag-icon"),applyFlag=async cc=>{v.flag=cc,wrap.dataset.flagCc=cc;const fi=`<span class="fi fi-${cc.toLowerCase()}" role="img" aria-label="${cc}"></span>`;flagIconEl&&(flagIconEl.innerHTML=fi),await saveMeta(voiceId,{flag:cc})};flagSpan.addEventListener("click",()=>{const langList=FLAG_OPTIONS[langCode.toUpperCase()]||[],items=uniqueFlagOptions([langList,ALL_FLAGS]).map(([cc,name])=>[cc.toLowerCase(),name]);createSearchablePicker(flagSpan,items,applyFlag,{placeholder:"Country or accent\u2026",renderItem:(cc,name)=>`<span class="fi fi-${cc}" style="width:20px;height:14px;background-size:cover;border-radius:2px;flex-shrink:0;display:inline-block"></span><span>${escHtml(name)}</span>`})});const langCodeEl=inspector.querySelector(".insp-lang-code"),langWrap=inspector.querySelector(".insp-lang-wrap"),applyLang=async newLang=>{v.lang=newLang,wrap.dataset.lang=newLang,langCodeEl&&(langCodeEl.textContent=newLang),await saveMeta(voiceId,{lang:newLang})};langWrap==null||langWrap.addEventListener("click",()=>{const items=LANGS.map(l=>[l,LANGUAGE_LABELS[l]?`${LANGUAGE_LABELS[l]} (${l})`:l]);createSearchablePicker(langWrap,items,applyLang,{placeholder:"Language\u2026",renderItem:(l,label)=>`<span class="ipi-code">${escHtml(l)}</span><span>${escHtml(LANGUAGE_LABELS[l]||l)}</span>`})});const genderLabelEl=inspector.querySelector(".insp-gender-label"),genderSel=inspector.querySelector(".insp-gender-sel"),applyGender=async ng=>{v.gender=ng,genderLabelEl&&(genderLabelEl.textContent=`${_gM[ng]||"?"} ${_gL[ng]||"\u2014"}`),genderSel&&(genderSel.value=ng);const gBadge=wrap.querySelector(".gender-badge");gBadge&&(gBadge.innerHTML=`<span class="gender-sym">${_gM[ng]||"?"}</span><span class="gender-txt">${_gL[ng]||"\u2014"}</span>`,gBadge.className="gender-badge "+(_gC[ng]||"g-n")),await saveMeta(voiceId,{gender:ng})};genderLabelEl==null||genderLabelEl.addEventListener("click",()=>{const cycle=["F","M","N"];applyGender(cycle[(cycle.indexOf(v.gender||"N")+1)%3])}),genderSel==null||genderSel.addEventListener("change",()=>applyGender(genderSel.value));const tagInput=inspector.querySelector(".insp-tag-input"),getAllKnownTags=()=>{const fromDb=(_voices||[]).flatMap(vv=>(vv.tag||"").split(",").map(t=>t.trim()).filter(Boolean)),fromStorage=getStoredTags();return[...new Set([...fromStorage,...fromDb])].sort((a,b)=>a.localeCompare(b))},tagLastToken=val=>val.split(",").pop().trimStart(),tagReplaceLastToken=(val,rep)=>{const parts=val.split(",");return parts[parts.length-1]=parts.length>1?" "+rep:rep,parts.join(",")};let _tagDrop=null;const hideTagDrop=()=>{_tagDrop==null||_tagDrop.remove(),_tagDrop=null},showTagDrop=()=>{hideTagDrop();const token=tagLastToken(tagInput.value),matches=getAllKnownTags().filter(t=>t.toLowerCase().startsWith(token.toLowerCase())&&t.toLowerCase()!==token.toLowerCase());if(!matches.length)return;_tagDrop=document.createElement("div"),_tagDrop.className="tag-suggest",matches.slice(0,10).forEach(tag=>{const btn=document.createElement("button");btn.type="button",btn.className="tag-suggest-item",btn.textContent=tag,btn.addEventListener("mousedown",e=>{e.preventDefault(),tagInput.value=tagReplaceLastToken(tagInput.value,tag),tagInput.dispatchEvent(new Event("input")),hideTagDrop(),tagInput.focus()}),_tagDrop.appendChild(btn)}),document.body.appendChild(_tagDrop);const r=tagInput.getBoundingClientRect();_tagDrop.style.top=r.bottom+3+"px",_tagDrop.style.left=r.left+"px",_tagDrop.style.minWidth=Math.max(r.width,140)+"px"};tagInput.addEventListener("input",()=>showTagDrop()),tagInput.addEventListener("focus",()=>showTagDrop()),tagInput.addEventListener("blur",()=>setTimeout(hideTagDrop,150)),tagInput.addEventListener("keydown",e=>{e.key==="Escape"&&hideTagDrop(),e.key===","&&_tagDrop&&setTimeout(showTagDrop,10)});const saveTagValue=debounce(async()=>{tagInput.value.split(",").map(t=>t.trim()).filter(Boolean).forEach(addStoredTag),v.tag=tagInput.value,await saveMeta(voiceId,{tag:tagInput.value})},700);tagInput.addEventListener("input",saveTagValue),tagInput.addEventListener("change",saveTagValue);const updateRating=async newRating=>{v.rating=newRating,wrap.dataset.rating=newRating,inspector.querySelectorAll(".insp-star").forEach(s=>s.classList.toggle("on",parseInt(s.dataset.val)<=newRating)),inspector.querySelectorAll(".insp-rating-label, .insp-meta-rating-label").forEach(el=>el.textContent=`${newRating}/5`),wrap.querySelectorAll(".vr-rating .star").forEach((s,i)=>s.classList.toggle("on",i<newRating)),await saveMeta(voiceId,{rating:newRating})};[".insp-stars",".insp-meta-stars"].forEach(sel=>{const stars=[...inspector.querySelectorAll(`${sel} .insp-star`)];stars.forEach(s=>{s.addEventListener("click",()=>{const val=parseInt(s.dataset.val);updateRating(val===(v.rating||0)?0:val)}),s.addEventListener("mouseenter",()=>{const val=parseInt(s.dataset.val);stars.forEach(ss=>ss.classList.toggle("on",parseInt(ss.dataset.val)<=val))}),s.addEventListener("mouseleave",()=>stars.forEach(ss=>ss.classList.toggle("on",parseInt(ss.dataset.val)<=(v.rating||0))))})});const noteEl=detailRow==null?void 0:detailRow.querySelector(".vr-note"),noteRowEl=inspector.querySelector(".insp-note-slot");noteEl&&noteRowEl&&noteRowEl.appendChild(noteEl);const mainRow=wrap.querySelector(".vr-main-row"),optimizer=wrap.querySelector(".vr-optimizer");mainRow&&body.appendChild(mainRow),optimizer&&body.appendChild(optimizer),wrap._loadOptimizer&&wrap._loadOptimizer().catch(e=>console.warn("Auto-load waveform failed:",e));const personaVal=v.persona||"",personaPanel=document.createElement("div");personaPanel.className="insp-persona-panel",personaPanel.innerHTML=`
<div class="insp-persona-head">
<span class="insp-persona-title">Character persona</span>
<span class="insp-persona-hint">Used by LLM to rewrite text in this voice's style. Leave empty to skip.</span>
</div>
<textarea class="insp-persona-input" rows="3" placeholder="e.g. calm British narrator with a warm tone, measured pace, formal vocabulary\u2026">${escHtml(personaVal)}</textarea>
<div class="insp-persona-actions">
<button class="btn-primary btn-sm insp-persona-save-btn">Save persona</button>
<span class="insp-persona-status"></span>
</div>
`,body.appendChild(personaPanel);const personaTextarea=personaPanel.querySelector(".insp-persona-input"),personaStatus=personaPanel.querySelector(".insp-persona-status");personaPanel.querySelector(".insp-persona-save-btn").addEventListener("click",async()=>{const pText=personaTextarea.value.trim();try{await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,persona:pText})}),v.persona=pText,personaStatus.textContent="Saved.",setTimeout(()=>{personaStatus.textContent=""},2e3),toast("Persona saved","success")}catch(e){toast("Save failed: "+e.message,"error")}});const maintTitle=body.querySelector(".opt-maintenance .opt-group-title");maintTitle&&(maintTitle.innerHTML=`<span class="opt-chevron"></span><span class="opt-title-text">Loudness <span class="opt-group-meta">Current ${escHtml(dbfs)} dBFS</span></span>`),body.querySelectorAll(".opt-group").forEach(group=>{const title=group.querySelector(":scope > .opt-group-title");title&&title.addEventListener("click",()=>{group.classList.toggle("open")})}),typeof attachSeedFinder=="function"&&attachSeedFinder(voiceId,body),wrap._extracted=[activeEl?{el:activeEl,target:detailRow}:null,deleteEl?{el:deleteEl,target:detailRow}:null,noteEl?{el:noteEl,target:detailRow}:null].filter(Boolean)}const SEED_FINDER_TEXT_DE="Die dreitausendf\xFCnfhundertsiebenundsechzig neuen Ger\xE4te f\xFCr das Update ben\xF6tigten eine au\xDFergew\xF6hnlich starke K\xFChlung und regelm\xE4\xDFige Wartung, um die Leistung bei gro\xDFer Last zu gew\xE4hrleisten. Notiere dir den einundzwanzigsten Juni um vierzehn Uhr. Es ist reine Zeitverschwendung, etwas Mittelm\xE4\xDFiges zu tun! Tr\xE4ume beginnen mit einem positiven Mindset.",SEED_FINDER_TEXT_EN="The new devices for the update required exceptionally strong cooling and regular maintenance to guarantee performance under heavy load. Make a note for the twenty-first of June at two in the afternoon. It is a genuine waste of time to do something mediocre! Dreams begin with a positive mindset.",SEED_FINDER_TEXT_TORTURE="Die 3.567 neuen High-End Ger\xE4te f\xFCr das Server-Update ben\xF6tigen eine au\xDFergew\xF6hnlich starke K\xFChlung und regelm\xE4\xDFige Ma\xDFnahmen, um die Performance bei gro\xDFer Last zu gew\xE4hrleisten. - The system administrator successfully configured the customized Docker stacks and benchmarked the inference engines at exactly 8:45 AM. - Notiere dir an Midsummer 21.06. um 14 Uhr - Es ist reine Zeitverschwendung, etwas Mittelm\xE4\xDFiges zu tun! Sch\xF6ne Gr\xFC\xDFe! \u2014 Madonna - Tr\xE4ume beginnt mit einem positiven Mindset.";function _seedFinderDefaultText(voiceId){if(window._appSettings&&window._appSettings.seed_finder_text)return window._appSettings.seed_finder_text;const v=(window._voices||[]).find(x=>x.id===voiceId);if(v&&v.transcript&&v.transcript.trim())return v.transcript.trim();const lc=(voiceId||"").toLowerCase();return lc.startsWith("de_")?SEED_FINDER_TEXT_DE:lc.startsWith("en_")||lc.startsWith("gb_")?SEED_FINDER_TEXT_EN:SEED_FINDER_TEXT_DE}const SF_DB="seed-finder",SF_STORE="samples",SF_CACHE_VERSION="wav-seed-v2";function _sfHash(s){let h=0;for(let i=0;i<s.length;i++)h=h*31+s.charCodeAt(i)>>>0;return h.toString(36)}function _sfDbOpen(){return new Promise((res,rej)=>{const r=indexedDB.open(SF_DB,1);r.onupgradeneeded=e=>{const db=e.target.result;db.objectStoreNames.contains(SF_STORE)||db.createObjectStore(SF_STORE,{keyPath:"key"})},r.onsuccess=e=>res(e.target.result),r.onerror=e=>rej(e.target.error)})}async function _sfDbGet(key){try{const db=await _sfDbOpen();return await new Promise((res,rej)=>{const r=db.transaction(SF_STORE,"readonly").objectStore(SF_STORE).get(key);r.onsuccess=e=>res(e.target.result||null),r.onerror=e=>rej(e.target.error)})}catch{return null}}async function _sfDbPut(rec){try{const db=await _sfDbOpen();await new Promise((res,rej)=>{const r=db.transaction(SF_STORE,"readwrite").objectStore(SF_STORE).put(rec);r.onsuccess=()=>res(),r.onerror=e=>rej(e.target.error)})}catch{}}async function _sfDbAllForVoice(voiceId){try{const db=await _sfDbOpen();return await new Promise((res,rej)=>{const out=[],cur=db.transaction(SF_STORE,"readonly").objectStore(SF_STORE).openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&out.push(c.value),c.continue()):res(out)},cur.onerror=e=>rej(e.target.error)})}catch{return[]}}async function _sfDbClearVoice(voiceId){try{const db=await _sfDbOpen();await new Promise((res,rej)=>{const store=db.transaction(SF_STORE,"readwrite").objectStore(SF_STORE),cur=store.openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&store.delete(c.primaryKey),c.continue()):res()},cur.onerror=e=>rej(e.target.error)})}catch{}}function attachSeedFinder(voiceId,body){const panel=document.createElement("div");panel.className="opt-group seed-finder-group",panel.innerHTML=`
<div class="opt-group-title">
<span class="opt-chevron"></span>
<span class="opt-title-text">\u{1F3B2} Seed Finder</span>
<span class="opt-group-meta">Find the most natural-sounding voice</span>
</div>
<div class="opt-group-subtitle">
Generates audio for each seed so you can listen and pick the best one.
Click "Use this seed" to save it to the TTS server.
</div>
<div class="opt-group-body seed-finder-body">
<div class="seed-finder-config">
<div class="seed-finder-row">
<label class="seed-finder-label">Test sentence</label>
<textarea class="seed-finder-text" rows="3" spellcheck="false"></textarea>
</div>
<div class="seed-finder-row seed-finder-params">
<div class="seed-finder-field">
<label>Seeds: from</label>
<input class="seed-finder-from" type="number" min="0" max="9999" value="1" step="1">
<label>to</label>
<input class="seed-finder-to" type="number" min="1" max="9999" value="15" step="1">
</div>
<div class="seed-finder-field">
<label>Backend</label>
<select class="seed-finder-backend">
<option value="voice_clone" selected>Voice Clone</option>
<option value="streaming">Streaming</option>
<option value="voice_design">Voice Design</option>
</select>
</div>
</div>
<div class="seed-finder-actions">
<button class="btn-primary seed-finder-run-btn">\u25B6 Run Seed Finder</button>
<button class="btn-secondary seed-finder-cancel-btn" style="display:none">\u2715 Cancel</button>
<button class="btn-secondary seed-finder-clear-btn" style="display:none" title="Delete the saved samples cached for this voice">\u{1F5D1} Clear saved</button>
<span class="seed-finder-status"></span>
</div>
<div class="seed-finder-row seed-finder-pin">
<label class="seed-finder-label">Or fix this voice to a specific seed</label>
<div class="seed-finder-pin-controls">
<input class="seed-finder-pin-input" type="number" min="0" max="9999" step="1" placeholder="seed #">
<button class="btn-secondary seed-finder-pin-btn">\u2605 Pin seed</button>
<button class="btn-secondary seed-finder-pin-clear" title="Use a random seed each time (unpin)">Unpin</button>
<span class="seed-finder-pin-status"></span>
</div>
</div>
</div>
<div class="seed-finder-progress" style="display:none">
<div class="seed-finder-progress-head">
<span class="seed-finder-progress-label">Generating\u2026</span>
<span class="seed-finder-progress-count">0 / 0</span>
</div>
<div class="benchmark-progress-track">
<div class="seed-finder-bar" style="width:0%"></div>
</div>
</div>
<div class="seed-finder-results"></div>
</div>
`,body.appendChild(panel);let _batchLoaded=!1;panel.querySelector(".opt-group-title").addEventListener("click",()=>{panel.classList.toggle("open"),panel.classList.contains("open")&&!_batchLoaded&&(_batchLoaded=!0,_loadBatchSamples())});const textEl=panel.querySelector(".seed-finder-text"),fromEl=panel.querySelector(".seed-finder-from"),toEl=panel.querySelector(".seed-finder-to"),backendEl=panel.querySelector(".seed-finder-backend"),runBtn=panel.querySelector(".seed-finder-run-btn"),cancelBtn=panel.querySelector(".seed-finder-cancel-btn"),clearBtn=panel.querySelector(".seed-finder-clear-btn"),statusEl=panel.querySelector(".seed-finder-status"),progressEl=panel.querySelector(".seed-finder-progress"),progLabel=panel.querySelector(".seed-finder-progress-label"),progCount=panel.querySelector(".seed-finder-progress-count"),progBar=panel.querySelector(".seed-finder-bar"),resultsEl=panel.querySelector(".seed-finder-results");textEl.value=_seedFinderDefaultText(voiceId);const pinInput=panel.querySelector(".seed-finder-pin-input"),pinBtn=panel.querySelector(".seed-finder-pin-btn"),pinClear=panel.querySelector(".seed-finder-pin-clear"),pinStatus=panel.querySelector(".seed-finder-pin-status"),voiceObj=(window._voices||[]).find(v=>v.id===voiceId)||{};voiceObj.seed!==void 0&&voiceObj.seed!==null&&(pinInput.value=voiceObj.seed,pinStatus.textContent=`\u2713 Pinned seed ${voiceObj.seed}`),voiceObj.has_ref||(backendEl.value="voice_design");let _cancelled=!1,_currentAudio=null;async function _loadBatchSamples(){try{const resp=await fetch(`/api/seed-samples/${encodeURIComponent(voiceId)}`);if(!resp.ok)return;const{seeds}=await resp.json();if(!seeds||seeds.length===0)return;resultsEl.innerHTML="";const header=document.createElement("div");header.className="sfr-batch-header",header.textContent=`${seeds.length} pre-generated sample${seeds.length!==1?"s":""} from batch run \u2014 click Play to listen`,resultsEl.appendChild(header);for(const seed of seeds){const row=document.createElement("div");row.className="seed-finder-result-row sfr-batch",row.dataset.seed=seed,row.innerHTML=`
<span class="sfr-seed">Seed ${seed}</span>
<span class="sfr-dur sfr-batch-tag">pre-generated</span>
<button class="btn-secondary sfr-play-btn"><span class="mdi mdi-play"></span> Play</button>
<button class="btn-primary sfr-use-btn">\u2605 Use seed ${seed}</button>
<span class="sfr-saved"></span>
`;let _cachedBlob=null;row.querySelector(".sfr-play-btn").addEventListener("click",async()=>{if(!_cachedBlob){const r=await fetch(`/api/seed-sample/${encodeURIComponent(voiceId)}/${seed}`);if(!r.ok){typeof toast=="function"&&toast("Failed to load sample","error");return}_cachedBlob=await r.blob()}_playBlob(_cachedBlob)}),row.querySelector(".sfr-use-btn").addEventListener("click",async()=>{try{await _saveSeed(seed),resultsEl.querySelectorAll(".sfr-saved").forEach(el=>el.textContent=""),resultsEl.querySelectorAll(".seed-finder-result-row").forEach(r=>r.classList.remove("sfr-active")),row.classList.add("sfr-active"),row.querySelector(".sfr-saved").textContent="\u2713 Saved",pinInput&&(pinInput.value=seed),typeof toast=="function"&&toast(`Seed ${seed} saved for ${voiceId}`,"success")}catch(e){typeof toast=="function"&&toast("Save failed: "+e.message,"error")}}),resultsEl.appendChild(row)}}catch{}}async function _saveSeed(seed){const resp=await fetch("/api/tts-voice-seed",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice:voiceId,seed})});if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.detail||resp.statusText)}const v=(window._voices||[]).find(vv=>vv.id===voiceId);v&&(v.seed=seed);const pinnedSeedEl=document.querySelector(".insp-pinned-seed");pinnedSeedEl&&(seed!=null?(pinnedSeedEl.textContent=`Pin Seed # ${seed}`,pinnedSeedEl.style.display=""):(pinnedSeedEl.style.display="none",pinnedSeedEl.textContent=""))}async function _fetchWithRetry(fn,tries=2,delay=1200){let lastErr;for(let t=0;t<=tries;t++){if(_cancelled)throw new Error("cancelled");try{return await fn()}catch(e){lastErr=e,t<tries&&await new Promise(r=>setTimeout(r,delay))}}throw lastErr}function _playBlob(blob){_currentAudio&&(_currentAudio.pause(),_currentAudio=null);const url=URL.createObjectURL(blob),audio=new Audio(url);_currentAudio=audio,audio.play().catch(()=>{}),audio.addEventListener("ended",()=>{URL.revokeObjectURL(url),_currentAudio=null})}const seedRows=new Map,_keyFor=(seed,text,backend)=>`${SF_CACHE_VERSION}|${voiceId}|${backend}|${_sfHash(text)}|${seed}`;async function _useSeed(seed,row){_currentAudio&&(_currentAudio.pause(),_currentAudio=null);try{await _saveSeed(seed),resultsEl.querySelectorAll(".sfr-saved").forEach(el=>el.textContent=""),resultsEl.querySelectorAll(".seed-finder-result-row").forEach(r=>r.classList.remove("sfr-active")),row.classList.add("sfr-active"),row.querySelector(".sfr-saved").textContent="\u2713 Saved",pinInput&&(pinInput.value=seed),typeof toast=="function"&&toast(`Seed ${seed} saved for ${voiceId}`,"success")}catch(e){typeof toast=="function"&&toast("Save failed: "+e.message,"error")}}function _seedMetricText(data){const parts=[];return data.dur&&parts.push(`audio ${data.dur.toFixed(1)}s${data.clipped?" clipped":""}`),data.genSec&&parts.push(`gen ${data.genSec.toFixed(1)}s`),data.rtf&&parts.push(`${data.rtf.toFixed(2)}x`),data.cached&&parts.push("saved"),parts.join(" \xB7 ")}function _renderSeed(seed,data){const row=document.createElement("div");if(row.className="seed-finder-result-row"+(data.err?" seed-finder-result-error":""),row.dataset.seed=seed,data.err)row.innerHTML=`<span class="sfr-seed">Seed ${seed}</span>
<span class="sfr-error">Failed: ${escHtml(String(data.err))}</span>
<button class="btn-secondary sfr-retry-btn"><span class="mdi mdi-refresh"></span> Retry</button>`,row.querySelector(".sfr-retry-btn").addEventListener("click",()=>_retrySeed(seed));else{const durStr=_seedMetricText(data);row.innerHTML=`<span class="sfr-seed">Seed ${seed}</span>
<span class="sfr-dur">${escHtml(durStr)}</span>
<button class="btn-secondary sfr-play-btn"><span class="mdi mdi-play"></span> Play</button>
<button class="btn-primary sfr-use-btn">\u2605 Use seed ${seed}</button>
<span class="sfr-saved"></span>`,row.querySelector(".sfr-play-btn").addEventListener("click",()=>_playBlob(data.blob)),row.querySelector(".sfr-use-btn").addEventListener("click",()=>_useSeed(seed,row))}const existing=seedRows.get(seed);existing?existing.replaceWith(row):resultsEl.appendChild(row),seedRows.set(seed,row)}async function _fetchSeedSample(seed,text,backend){const body2={text,voice:voiceId,response_format:"wav",instruct:"",backend,seed,seed_finder:!0},t0=performance.now(),r=await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body2)}),genSec=Math.max(0,(performance.now()-t0)/1e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const blob=await r.blob(),headerDur=parseFloat(r.headers.get("X-TTS-Audio-Duration")||""),dur=Number.isFinite(headerDur)&&headerDur>0?headerDur:Math.max(0,(blob.size-44)/(24e3*2)),clipped=String(r.headers.get("X-TTS-Audio-Clipped")||"").toLowerCase()==="true",rtf=dur>0?genSec/dur:0;return{blob,dur,genSec,rtf,clipped}}async function _generateSeed(seed,text,backend,force){const key=_keyFor(seed,text,backend);if(!force){const c=await _sfDbGet(key);if(c&&c.blob)return{blob:c.blob,dur:c.dur||0,genSec:c.genSec||0,rtf:c.rtf||0,clipped:!!c.clipped,cached:!0}}const data=await _fetchWithRetry(()=>_fetchSeedSample(seed,text,backend));return _sfDbPut({key,cacheVersion:SF_CACHE_VERSION,voiceId,backend,textHash:_sfHash(text),seed,...data,ts:Date.now()}),{...data,cached:!1}}async function _retrySeed(seed){const text=textEl.value.trim(),backend=backendEl.value,row=seedRows.get(seed),err=row&&row.querySelector(".sfr-error");err&&(err.textContent="Retrying\u2026");try{_renderSeed(seed,await _generateSeed(seed,text,backend,!0))}catch(e){_renderSeed(seed,{err:e.message||String(e)})}}async function _loadCached(){const text=textEl.value.trim(),backend=backendEl.value,th=_sfHash(text),all=(await _sfDbAllForVoice(voiceId)).filter(r=>r.cacheVersion===SF_CACHE_VERSION&&r.backend===backend&&r.textHash===th&&r.blob);if(!all.length){clearBtn.style.display="none";return}all.sort((a,b)=>a.seed-b.seed),resultsEl.innerHTML="",seedRows.clear(),all.forEach(r=>_renderSeed(r.seed,{blob:r.blob,dur:r.dur||0,genSec:r.genSec||0,rtf:r.rtf||0,clipped:!!r.clipped,cached:!0})),clearBtn.style.display="",statusEl.textContent=`${all.length} saved sample${all.length!==1?"s":""} loaded \u2014 Run to fill the rest.`}async function _runFinder(){const text=textEl.value.trim();if(!text){statusEl.textContent="Enter a test sentence.";return}const seedFrom=Math.max(0,parseInt(fromEl.value,10)||1),seedTo=Math.max(seedFrom,parseInt(toEl.value,10)||15),backend=backendEl.value,seeds=[];for(let s=seedFrom;s<=seedTo;s++)seeds.push(s);const total=seeds.length;_cancelled=!1,resultsEl.innerHTML="",seedRows.clear(),progressEl.style.display="",runBtn.style.display="none",cancelBtn.style.display="",clearBtn.style.display="none",statusEl.textContent="",progBar.style.width="0%",progCount.textContent=`0 / ${total}`,progLabel.textContent="Generating\u2026";let i=0,cached=0;for(const seed of seeds){if(_cancelled)break;progLabel.textContent=`Seed ${seed}\u2026`,progCount.textContent=`${i} / ${total}`,progBar.style.width=`${Math.round(i/total*100)}%`;try{const d=await _generateSeed(seed,text,backend,!1);d.cached&&cached++,_renderSeed(seed,d)}catch(e){if(_cancelled)break;_renderSeed(seed,{err:e.message||String(e)})}i++}progBar.style.width="100%",progCount.textContent=`${i} / ${total}`,progLabel.textContent=_cancelled?"Cancelled.":`Done \u2014 listen and pick your seed.${cached?` (${cached} from cache)`:""}`,runBtn.style.display="",cancelBtn.style.display="none",seedRows.size&&(clearBtn.style.display="")}runBtn.addEventListener("click",_runFinder),cancelBtn.addEventListener("click",()=>{_cancelled=!0}),clearBtn.addEventListener("click",async()=>{confirm("Delete the cached seed samples for this voice?")&&(await _sfDbClearVoice(voiceId),resultsEl.innerHTML="",seedRows.clear(),clearBtn.style.display="none",statusEl.textContent="Saved samples cleared.")}),_loadCached(),pinBtn.addEventListener("click",async()=>{const seed=parseInt(pinInput.value,10);if(isNaN(seed)||seed<0){pinStatus.textContent="Enter a seed number.";return}pinBtn.disabled=!0,pinStatus.textContent="Saving\u2026";try{await _saveSeed(seed),pinStatus.textContent=`\u2713 Pinned seed ${seed}`,typeof toast=="function"&&toast(`Voice ${voiceId} fixed to seed ${seed}`,"success")}catch(e){pinStatus.textContent="",typeof toast=="function"&&toast("Pin failed: "+e.message,"error")}finally{pinBtn.disabled=!1}}),pinClear.addEventListener("click",async()=>{pinClear.disabled=!0,pinStatus.textContent="Clearing\u2026";try{await _saveSeed(null),pinInput.value="",pinStatus.textContent="\u2713 Unpinned (random seed)",resultsEl.querySelectorAll(".sfr-saved").forEach(el=>el.textContent=""),resultsEl.querySelectorAll(".seed-finder-result-row").forEach(r=>r.classList.remove("sfr-active")),typeof toast=="function"&&toast(`Voice ${voiceId} unpinned \u2014 random seed`,"success")}catch(e){pinStatus.textContent="",typeof toast=="function"&&toast("Unpin failed: "+e.message,"error")}finally{pinClear.disabled=!1}})}function seedFinderBatchAll(){var _a2;const _sfUseSelected=typeof _bulkSelected!="undefined"&&_bulkSelected.size>0,ids=_sfUseSelected?[..._bulkSelected]:typeof activeVoiceIds=="function"?activeVoiceIds():(window._voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id);if(!ids.length){typeof toast=="function"&&toast("No voices selected","error");return}(_a2=document.getElementById("seedbatch-overlay"))==null||_a2.remove();const ov=document.createElement("div");ov.className="audiobook-overlay",ov.id="seedbatch-overlay",ov.innerHTML=`<div class="audiobook-box">
<div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div>
<div class="audiobook-msg">Generate &amp; cache Seed Finder samples for <b>${ids.length}</b> ${_sfUseSelected?"selected":"active"} voice${ids.length!==1?"s":""}. Already-cached seeds are skipped, so it's resumable; you can cancel anytime.</div>
<div class="seed-finder-row seed-finder-params" style="margin:12px 0">
<div class="seed-finder-field">
<label>Seeds: from</label>
<input class="sb-from" type="number" min="0" max="9999" value="1" step="1">
<label>to</label>
<input class="sb-to" type="number" min="1" max="9999" value="8" step="1">
</div>
<span class="sb-estimate seed-finder-status"></span>
</div>
<div class="audiobook-actions">
<button class="btn-secondary btn-sm" id="sb-cancel-cfg">Cancel</button>
<button class="btn-primary btn-sm" id="sb-start"><span class="mdi mdi-play"></span> Start</button>
</div>
</div>`,document.body.appendChild(ov);const box=ov.querySelector(".audiobook-box"),fromI=ov.querySelector(".sb-from"),toI=ov.querySelector(".sb-to"),est=ov.querySelector(".sb-estimate"),updEst=()=>{const f=Math.max(0,parseInt(fromI.value,10)||0),t=Math.max(f,parseInt(toI.value,10)||f);est.textContent=`${ids.length} voices \xD7 ${t-f+1} seeds = up to ${ids.length*(t-f+1)} samples`};fromI.addEventListener("input",updEst),toI.addEventListener("input",updEst),updEst(),ov.querySelector("#sb-cancel-cfg").addEventListener("click",()=>ov.remove()),ov.querySelector("#sb-start").addEventListener("click",()=>{const from=Math.max(0,parseInt(fromI.value,10)||1),to=Math.max(from,parseInt(toI.value,10)||8);_seedBatchRun(ids,from,to,box,ov)})}async function _seedBatchRun(ids,from,to,box,ov){const totalJobs=ids.length*(to-from+1);box.innerHTML=`<div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div>
<div class="audiobook-msg" id="sb-msg">Starting\u2026</div>
<div class="reader-synth-track"><div class="reader-synth-fill" id="sb-fill"></div></div>
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="sb-cancel">Cancel</button></div>`;let cancel=!1;box.querySelector("#sb-cancel").addEventListener("click",()=>{cancel=!0});const fill=box.querySelector("#sb-fill"),msg=box.querySelector("#sb-msg");let done=0,made=0,cached=0,failed=0;for(let vi=0;vi<ids.length&&!cancel;vi++){const voiceId=ids[vi],text=_seedFinderDefaultText(voiceId),th=_sfHash(text),backend=typeof _ttsBackendForVoice=="function"?_ttsBackendForVoice(voiceId,"voice_clone"):"voice_clone";for(let seed=from;seed<=to&&!cancel;seed++){msg.textContent=`Voice ${vi+1}/${ids.length} \xB7 seed ${seed} \u2014 ${voiceId}`;const key=`${SF_CACHE_VERSION}|${voiceId}|${backend}|${th}|${seed}`,existing=await _sfDbGet(key);if(existing&&existing.blob){cached++,done++,fill.style.width=done/totalJobs*100+"%";continue}try{const body={text,voice:voiceId,response_format:"wav",instruct:"",backend,seed,seed_finder:!0},t0=performance.now(),resp=await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}),genSec=Math.max(0,(performance.now()-t0)/1e3);if(!resp.ok)throw new Error(resp.statusText);const blob=await resp.blob(),headerDur=parseFloat(resp.headers.get("X-TTS-Audio-Duration")||""),dur=Number.isFinite(headerDur)&&headerDur>0?headerDur:Math.max(0,(blob.size-44)/(24e3*2)),clipped=String(resp.headers.get("X-TTS-Audio-Clipped")||"").toLowerCase()==="true",rtf=dur>0?genSec/dur:0;await _sfDbPut({key,cacheVersion:SF_CACHE_VERSION,voiceId,backend,textHash:th,seed,blob,dur,genSec,rtf,clipped,ts:Date.now()}),made++}catch{failed++}done++,fill.style.width=done/totalJobs*100+"%"}}ov.remove(),typeof toast=="function"&&toast(cancel?`Stopped \u2014 ${made} generated, ${cached} already cached`:`Batch done \u2014 ${made} generated, ${cached} cached${failed?`, ${failed} failed`:""}`,cancel?"error":"success")}typeof $=="function"&&((_a=$("seed-batch-all-btn"))==null||_a.addEventListener("click",seedFinderBatchAll));const DEFAULT_VOICE_SOURCE_URLS=["https://aiartes.com/voiceai","https://sample-files.com/downloads/audio/wav/voice-sample.wav","https://freesound.org/people/Scott%20Simpson/","https://lanceblairvo.com/raw-voiceover-samples/","https://github.com/yaph/tts-samples/tree/main/mp3","https://github.com/jim-schwoebel/voice_datasets"],VOICE_SOURCE_STORAGE_KEY="ttsvc-getvoices-sources";let _voiceSourcePayload=null,_voiceSourceItems=[];function sourceTextareaValueFromDefaults(){return DEFAULT_VOICE_SOURCE_URLS.join(`
`)}function initGetVoiceSourcesEditor(){const box=$("getvoices-sources");!box||box.dataset.ready||(box.value=localStorage.getItem(VOICE_SOURCE_STORAGE_KEY)||sourceTextareaValueFromDefaults(),box.dataset.ready="1",box.addEventListener("input",()=>{localStorage.setItem(VOICE_SOURCE_STORAGE_KEY,box.value),_voiceSourcePayload=null,$("getvoices-status").textContent="Source list changed."}))}function getEditableVoiceSourceUrls(){var _a2;return initGetVoiceSourcesEditor(),(((_a2=$("getvoices-sources"))==null?void 0:_a2.value)||"").split(/\r?\n/).map(line=>line.trim()).filter(line=>line&&!line.startsWith("#"))}function getVoiceSourceItems(){return(_voiceSourcePayload&&_voiceSourcePayload.sources||[]).flatMap(src=>(src.items||[]).map(item=>({...item,_sourceName:src.name,_sourceHomepage:src.homepage})))}function voiceSourceSearchText(item){return[item.name,item.kind,item.category,item.language,item.gender,item.description,item.source,item._sourceName].join(" ").toLowerCase()}function setOptions(selectId,values,allLabel){const sel=$(selectId);if(!sel)return;const current=sel.value||"all";sel.innerHTML=`<option value="all">${escHtml(allLabel)}</option>`+values.map(value=>`<option value="${escHtml(value)}">${escHtml(value)}</option>`).join(""),sel.value=values.includes(current)?current:"all"}function renderGetVoices(){var _a2,_b2,_c2,_d2,_e2,_f2;initGetVoiceSourcesEditor();const list=$("getvoices-list"),summary=$("getvoices-summary");if(!list||!summary)return;const payload=_voiceSourcePayload||{sources:[],total:0,direct_audio:0,errors:[]},sources=payload.sources||[],sourceFilter=((_a2=$("getvoices-source-filter"))==null?void 0:_a2.value)||"all",languageFilter=((_b2=$("getvoices-language-filter"))==null?void 0:_b2.value)||"all",genderFilter=((_c2=$("getvoices-gender-filter"))==null?void 0:_c2.value)||"all",filetypeFilter=((_d2=$("getvoices-filetype-filter"))==null?void 0:_d2.value)||"all",q=(((_e2=$("getvoices-search"))==null?void 0:_e2.value)||"").trim().toLowerCase(),directOnly=!!((_f2=$("getvoices-direct-only"))!=null&&_f2.checked);_voiceSourceItems=getVoiceSourceItems(),summary.innerHTML=[[`${payload.total||0}`,"Items found"],[`${payload.direct_audio||0}`,"Direct audio"],[`${sources.length}`,"Sources OK"],[`${(payload.errors||[]).length}`,"Errors"]].map(([value,label])=>`<div class="insight"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join(""),setOptions("getvoices-source-filter",sources.map(src=>src.id).filter(Boolean),"Source: all");const sourceSelect=$("getvoices-source-filter");sourceSelect&&[...sourceSelect.options].forEach(option=>{if(option.value==="all")return;const src=sources.find(s=>s.id===option.value);src&&(option.textContent=src.name||src.id)});const languages=[...new Set(_voiceSourceItems.map(item=>item.language||"Unknown"))].sort((a,b)=>a.localeCompare(b)),genders=[...new Set(_voiceSourceItems.map(item=>item.gender||"Unknown"))].sort((a,b)=>a.localeCompare(b)),filetypes=[...new Set(_voiceSourceItems.map(item=>(item.file_type||(item.direct_audio?"audio":"page")).toUpperCase()))].sort((a,b)=>a.localeCompare(b));setOptions("getvoices-language-filter",languages,"Language: all"),setOptions("getvoices-gender-filter",genders,"Sex: all"),setOptions("getvoices-filetype-filter",filetypes,"Filetype: all");let items=_voiceSourceItems.filter(item=>{if(sourceFilter!=="all"&&item.source_id!==sourceFilter||languageFilter!=="all"&&(item.language||"Unknown")!==languageFilter||genderFilter!=="all"&&(item.gender||"Unknown")!==genderFilter)return!1;const itemFiletype=(item.file_type||(item.direct_audio?"audio":"page")).toUpperCase();return!(filetypeFilter!=="all"&&itemFiletype!==filetypeFilter||directOnly&&!item.direct_audio||q&&!voiceSourceSearchText(item).includes(q))});const shown=items.slice(0,240),more=items.length-shown.length;if(!shown.length){const errorText=(payload.errors||[]).map(e=>`${e.source}: ${e.detail}`).join(" | ");list.innerHTML=`<div class="card"><p class="note">No matching sources found.${errorText?" Source errors: "+escHtml(errorText):""}</p></div>`;return}list.innerHTML=shown.map(item=>{const thumb=item.image_url?`<img class="voice-source-thumb" src="${escHtml(item.image_url)}" alt="" loading="lazy" decoding="async">`:'<div class="voice-source-thumb"></div>',audio=item.audio_url?`<audio controls preload="none" src="${escHtml(item.audio_url)}"></audio>`:"",audioLink=item.audio_url?`<a class="btn-secondary" href="${escHtml(item.audio_url)}" target="_blank" rel="noopener">Open audio</a>`:"",canGetVoice=!!(item.import_url||item.audio_url),importUrl=item.import_url||item.audio_url,getVoice=canGetVoice?`<button class="btn-primary get-source-voice" data-url="${escHtml(importUrl)}" data-name="${escHtml(item.name||"")}" data-image="${escHtml(item.image_url||"")}" data-language="${escHtml(item.language||"")}" data-gender="${escHtml(item.gender||"")}" data-kind="${escHtml(item.kind||"")}" data-description="${escHtml(item.description||"")}" data-page="${escHtml(item.page_url||item._sourceHomepage||"")}">Import this voice</button>`:"",type=item.file_type?`<span class="voice-source-pill">${escHtml(String(item.file_type).toUpperCase())}</span>`:"",language=item.language?`<span class="voice-source-pill">${escHtml(item.language)}</span>`:"",gender=item.gender?`<span class="voice-source-pill">${escHtml(item.gender)}</span>`:"";return`<div class="voice-source-card">
<div class="voice-source-head">
${thumb}
<div class="voice-source-title">
<strong title="${escHtml(item.name||"")}">${escHtml(item.name||"Untitled")}</strong>
<span>${escHtml(item._sourceName||item.source||"")}${item.category?" \xB7 "+escHtml(item.category):""}</span>
</div>
</div>
<div class="voice-source-desc">${escHtml(item.kind||"")}${item.description?" - "+escHtml(item.description):""}</div>
${audio}
<div class="voice-source-actions">
${type}${language}${gender}
${audioLink}
<a class="btn-secondary" href="${escHtml(item.page_url||item._sourceHomepage||"#")}" target="_blank" rel="noopener">Source page</a>
<button class="btn-secondary copy-source-url" data-url="${escHtml(item.audio_url||item.page_url||"")}">Copy URL</button>
</div>
${getVoice?`<div class="vs-import-footer">${getVoice}</div>`:""}
</div>`}).join("")+(more>0?`<div class="card"><p class="note">${more} more matches. Narrow the search or filters to see them.</p></div>`:"")}async function loadGetVoices(force=!1){if(initGetVoiceSourcesEditor(),_voiceSourcePayload&&!force){renderGetVoices();return}const urls=getEditableVoiceSourceUrls(),list=$("getvoices-list");if(!urls.length){list&&(list.innerHTML='<div class="card"><p class="note">Add at least one source URL, then scrape again.</p></div>'),$("getvoices-status").textContent="No sources.";return}list&&(list.innerHTML=loadingMarkup("Scraping voice sources",`Fetching ${urls.length} source${urls.length===1?"":"s"} from the editable list.`,6)),$("getvoices-status").textContent="Scraping...",$("getvoices-refresh-btn").disabled=!0;try{const r=await fetch("/api/voice-sources",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({urls})});if(!r.ok){const text=await r.text().catch(()=>"");let message=r.statusText||`HTTP ${r.status}`;try{message=JSON.parse(text).detail||message}catch{text&&(message=text.slice(0,160))}throw new Error(message)}_voiceSourcePayload=await r.json(),renderGetVoices();const errors=(_voiceSourcePayload.errors||[]).length;$("getvoices-status").textContent=`${_voiceSourcePayload.total||0} found${errors?`, ${errors} source errors`:""}`}catch(e){list&&(list.innerHTML=`<div class="card"><p style="color:var(--red)">Scrape failed: ${escHtml(e.message)}</p><p class="note">Check that the TTS Voice Creator backend is restarted and that at least one source URL is reachable.</p></div>`),$("getvoices-status").textContent="Scrape failed",toast("Voice source scrape failed: "+e.message,"error")}finally{$("getvoices-refresh-btn").disabled=!1}}["getvoices-search","getvoices-source-filter","getvoices-language-filter","getvoices-gender-filter","getvoices-filetype-filter","getvoices-direct-only"].forEach(id=>{const el=$(id);el&&el.addEventListener(id==="getvoices-search"?"input":"change",renderGetVoices)}),(_b=$("getvoices-refresh-btn"))==null||_b.addEventListener("click",()=>loadGetVoices(!0)),(_c=$("getvoices-reset-sources-btn"))==null||_c.addEventListener("click",()=>{const box=$("getvoices-sources");box&&(box.value=sourceTextareaValueFromDefaults(),localStorage.setItem(VOICE_SOURCE_STORAGE_KEY,box.value),_voiceSourcePayload=null,renderGetVoices(),$("getvoices-status").textContent="Source list reset.")});const SOURCE_LANGUAGE_CODES={english:"EN",german:"DE",deutsch:"DE",french:"FR",spanish:"ES",japanese:"JA",korean:"KO",italian:"IT",portuguese:"PT",russian:"RU",arabic:"AR",polish:"PL",dutch:"NL",swedish:"SV",turkish:"TR",hindi:"HI",chinese:"ZH"};function sourceLanguageCode(language){const raw=String(language||"").trim();return/^[A-Z]{2}$/.test(raw)?raw:SOURCE_LANGUAGE_CODES[raw.toLowerCase()]||"EN"}function sourceGenderCode(gender,name=""){const text=`${gender||""} ${name||""}`.toLowerCase();return/female|woman|girl|\bf\b/.test(text)?"F":/male|man|boy|\bm\b/.test(text)?"M":"N"}function suggestedVoiceIdFromSourceName(name,language="EN",gender="N"){const base=String(name||"SourceVoice").normalize("NFKD").replace(/[\u0300-\u036f]/g,"").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,48)||"SourceVoice";return`${language||"EN"}_${gender||"N"}_${base}`}function setLibAddSourcePreview(meta={}){const sourceBox=document.querySelector(".lib-add-source-box");if(!sourceBox)return;let preview=$("lib-add-source-preview");if(preview||(preview=document.createElement("div"),preview.id="lib-add-source-preview",preview.className="lib-add-source-preview",sourceBox.appendChild(preview)),!meta.name&&!meta.imageUrl){preview.classList.remove("open"),preview.innerHTML="";return}const image=meta.imageUrl?`<img src="${escHtml(meta.imageUrl)}" alt="">`:'<div class="voice-source-thumb"></div>';preview.innerHTML=`${image}<div style="min-width:0"><strong>${escHtml(meta.name||"Source voice")}</strong><span>${escHtml([meta.language,meta.gender,meta.kind].filter(Boolean).join(" \xB7 ")||"Source metadata will be saved with the voice")}</span></div>`,preview.classList.add("open")}async function getSourceVoiceInLibrary(meta){if(!meta.url){toast("This source has no direct audio URL","error");return}const lang=sourceLanguageCode(meta.language),gender=sourceGenderCode(meta.gender,meta.name),voiceId=suggestedVoiceIdFromSourceName(meta.name,lang,gender);toast(`Importing ${meta.name||"voice"}\u2026`,"info");try{const r=await fetch("/api/quick-import-voice",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audio_url:meta.url,voice_id:voiceId,transcript:""})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();let srcTag="online";try{meta.pageUrl&&(srcTag=new URL(meta.pageUrl).hostname.replace(/^www\./,"").split(".")[0]||"online")}catch{}typeof saveMeta=="function"&&await saveMeta(d.voice_id,{name:meta.name||d.voice_id,tag:srcTag,group:srcTag,origin:"cloned",gender:(gender||"").toUpperCase(),note:(meta.description||"").slice(0,180)}).catch(()=>{}),meta.imageUrl&&await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:d.voice_id,image_url:meta.imageUrl})}).catch(()=>{}),_pendingSelectId=d.voice_id,navTo("s-voices"),_libraryLoadPromise=null,await loadVoiceLibrary(),toast(`Saved as ${d.voice_id}`,"success")}catch(e){toast("Import failed: "+e.message,"error")}}(_d=$("getvoices-list"))==null||_d.addEventListener("click",async e=>{const getBtn=e.target.closest(".get-source-voice");if(getBtn){await getSourceVoiceInLibrary({url:getBtn.dataset.url||"",name:getBtn.dataset.name||"",imageUrl:getBtn.dataset.image||"",language:getBtn.dataset.language||"",gender:getBtn.dataset.gender||"",kind:getBtn.dataset.kind||"",description:getBtn.dataset.description||"",pageUrl:getBtn.dataset.page||""});return}const btn=e.target.closest(".copy-source-url");btn&&(await copyText(btn.dataset.url||""),toast("Source URL copied","success"))}),function(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l2;const $2=id=>document.getElementById(id),grid=$2("fa-grid");if(!grid)return;let _page=1,_audio=null,_playingCard=null;const esc=s=>String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function fmtCount(n){return n=+n||0,n>=1e6?(n/1e6).toFixed(1).replace(/\.0$/,"")+"M":n>=1e3?(n/1e3).toFixed(1).replace(/\.0$/,"")+"K":String(n)}const FLAG={EN:"\u{1F1EC}\u{1F1E7}",DE:"\u{1F1E9}\u{1F1EA}",FR:"\u{1F1EB}\u{1F1F7}",ES:"\u{1F1EA}\u{1F1F8}",IT:"\u{1F1EE}\u{1F1F9}",PT:"\u{1F1F5}\u{1F1F9}",NL:"\u{1F1F3}\u{1F1F1}",JA:"\u{1F1EF}\u{1F1F5}",ZH:"\u{1F1E8}\u{1F1F3}",KO:"\u{1F1F0}\u{1F1F7}",RU:"\u{1F1F7}\u{1F1FA}",AR:"\u{1F1F8}\u{1F1E6}",PL:"\u{1F1F5}\u{1F1F1}"};function status2(msg,show=!0){const bar=$2("fa-status-bar"),txt=$2("fa-status-text");bar&&(bar.hidden=!show),txt&&(txt.textContent=msg||"")}function activeVals(group){return[...document.querySelectorAll(`#fa-filter-pop .fa-pchips[data-group="${group}"] .fa-pchip.active`)].map(c=>c.dataset.val).filter(Boolean)}function filterCount(){var _a3;return activeVals("gender").length+activeVals("age").length+activeVals("tag").length+((_a3=$2("fa-tag"))!=null&&_a3.value.trim()?1:0)}function updateFilterBadge(){const n=filterCount(),b=$2("fa-filter-badge"),btn=$2("fa-filter-btn");b&&(b.textContent=String(n),b.hidden=n===0),btn==null||btn.classList.toggle("active",n>0)}function params(){var _a3,_b3,_c3,_d3;const p=new URLSearchParams({page:String(_page),page_size:"24"}),s=(_a3=$2("fa-search"))==null?void 0:_a3.value.trim();s&&p.set("search",s);const l=(_b3=$2("fa-lang"))==null?void 0:_b3.value;l&&p.set("language",l);const sort=(_c3=$2("fa-sort"))==null?void 0:_c3.value;sort&&p.set("sort_by",sort);const g=activeVals("gender")[0];g&&p.set("gender",g);const a=activeVals("age")[0];a&&p.set("age",a),activeVals("tag").forEach(t2=>p.append("tag",t2));const t=(_d3=$2("fa-tag"))==null?void 0:_d3.value.trim();return t&&p.append("tag",t),p}function card(v){const can=!!v.sample_audio,meta=[v.language?`<span class="fa-meta-chip">${FLAG[v.language]||"\u{1F310}"} ${esc(v.language)}</span>`:"",v.gender?`<span class="fa-meta-chip">${esc(v.gender)}</span>`:"",v.age?`<span class="fa-meta-chip">${esc(v.age)}</span>`:""].join(""),avatar=v.image?`<img class="fa-avatar" src="${esc(v.image)}" alt="" loading="lazy" onerror="this.parentElement.classList.add('fa-avatar-fallback');this.remove();">`:"";return`<div class="fa-card${can?"":" fa-card-disabled"}" data-id="${esc(v.id)}">
<div class="fa-card-avatar${v.image?"":" fa-avatar-fallback"}">
${avatar}
${can?'<button class="fa-play" title="Preview"><span class="mdi mdi-play"></span></button>':""}
</div>
<div class="fa-card-body">
<div class="fa-card-top">
<span class="fa-card-name" title="${esc(v.title)}">${esc(v.title)||"Untitled"}</span>
${v.author?`<span class="fa-card-author">\xB7 ${esc(v.author)}</span>`:""}
</div>
<div class="fa-card-desc">${esc(v.description||v.sample_text||"")}</div>
<div class="fa-card-meta">${meta}</div>
<div class="fa-card-foot">
<span class="fa-stat" title="Generations"><span class="mdi mdi-poll"></span> ${fmtCount(v.plays)}</span>
<span class="fa-stat" title="Likes"><span class="mdi mdi-heart-outline"></span> ${fmtCount(v.likes)}</span>
${can?'<button class="btn-primary btn-sm fa-import" title="Import as a clonable voice"><span class="mdi mdi-download"></span> Use</button>':""}
</div>
</div>
</div>`}function render(data){const items=data.items||[];items.length?(grid.innerHTML=items.map(card).join(""),grid.querySelectorAll(".fa-card").forEach((el,i)=>{var _a3,_b3;const v=items[i];(_a3=el.querySelector(".fa-play"))==null||_a3.addEventListener("click",e=>{e.stopPropagation(),preview(v,el)}),(_b3=el.querySelector(".fa-import"))==null||_b3.addEventListener("click",e=>{e.stopPropagation(),importVoice(v,el)})})):grid.innerHTML='<div class="el-idle">No voices found \u2014 try a different search, language or filter.</div>';const pager=$2("fa-pager");if(pager){pager.hidden=!1,$2("fa-prev").disabled=_page<=1;const pages=Math.max(1,Math.ceil((data.total||0)/(data.page_size||24)));$2("fa-next").disabled=_page>=pages,$2("fa-pager-info").textContent=`Page ${_page} \xB7 ${(data.total||0).toLocaleString()} voices`}}async function browse(){status2("Loading voices from fish.audio\u2026"),grid.innerHTML='<div class="el-idle"><span class="reh-imsdb-spinner"></span> Loading\u2026</div>';try{const d=await fetch("/api/fishaudio/voices?"+params().toString()).then(r=>{if(!r.ok)throw new Error("HTTP "+r.status);return r.json()});render(d),status2(d.offline?"\u26A0 fish.audio unreachable \u2014 showing cached results":`${(d.total||0).toLocaleString()} matching voices`,!!d.offline)}catch(e){grid.innerHTML=`<div class="el-idle">Failed to load: ${esc(e.message)}</div>`,status2("")}}function stopPreview(){_audio&&(_audio.pause(),_audio=null),_playingCard==null||_playingCard.classList.remove("fa-card-playing"),_playingCard=null}function preview(v,el){if(_playingCard===el){stopPreview();return}stopPreview(),v.sample_audio&&(_audio=new Audio(v.sample_audio),_playingCard=el,el.classList.add("fa-card-playing"),_audio.addEventListener("ended",stopPreview),_audio.play().catch(()=>{stopPreview(),typeof toast=="function"&&toast("Could not play preview","error")}))}async function importVoice(v,el){const btn=el.querySelector(".fa-import"),orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span>';const lang=(v.language||"EN").slice(0,2).toUpperCase(),base=(typeof _umlautSafe=="function"?_umlautSafe(v.title||"fishaudio"):v.title||"fishaudio").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,40)||"Voice",voiceId=`${lang}_${base}`;try{const r=await fetch("/api/quick-import-voice",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,audio_url:v.sample_audio,transcript:v.sample_text||v.default_text||""})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();typeof saveMeta=="function"&&await saveMeta(d.voice_id,{name:v.title||base,tag:"fish-audio",group:"fish-audio",origin:"cloned",gender:(v.gender||"").charAt(0).toUpperCase(),note:(v.description||"").slice(0,180)}).catch(()=>{}),v.image&&await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:d.voice_id,image_url:v.image})}).catch(()=>{}),btn.innerHTML='<span class="mdi mdi-check"></span> Imported',el.classList.add("fa-card-imported"),typeof toast=="function"&&toast(`Imported "${v.title}" as ${d.voice_id}`,"success"),typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().catch(()=>{})}catch(e){btn.disabled=!1,btn.innerHTML=orig,typeof toast=="function"&&toast("Import failed: "+e.message,"error")}}(_a2=$2("fa-fetch"))==null||_a2.addEventListener("click",()=>{_page=1,browse()}),(_b2=$2("fa-search"))==null||_b2.addEventListener("keydown",e=>{e.key==="Enter"&&(_page=1,browse())}),(_c2=$2("fa-tag"))==null||_c2.addEventListener("keydown",e=>{e.key==="Enter"&&(_page=1,browse(),applyFilters())}),(_d2=$2("fa-lang"))==null||_d2.addEventListener("change",()=>{_page=1,browse()}),(_e2=$2("fa-sort"))==null||_e2.addEventListener("change",()=>{_page=1,browse()}),(_f2=$2("fa-prev"))==null||_f2.addEventListener("click",()=>{_page>1&&(_page--,browse())}),(_g2=$2("fa-next"))==null||_g2.addEventListener("click",()=>{_page++,browse()});const pop=$2("fa-filter-pop");function positionPop(){const btn=$2("fa-filter-btn");if(!pop||!btn||pop.hidden)return;const r=btn.getBoundingClientRect(),w=pop.offsetWidth||320,margin=8;let left=Math.min(r.right-w,window.innerWidth-w-margin);left<margin&&(left=margin),pop.style.position="fixed",pop.style.right="auto",pop.style.left=left+"px",pop.style.top=r.bottom+8+"px",pop.style.maxHeight=window.innerHeight-r.bottom-16+"px"}function openPop(show){pop&&(pop.hidden=show===void 0?!pop.hidden:!show,pop.hidden||positionPop())}(_h2=$2("fa-filter-btn"))==null||_h2.addEventListener("click",e=>{e.stopPropagation(),openPop()}),(_i2=$2("fa-filter-close"))==null||_i2.addEventListener("click",e=>{e.stopPropagation(),openPop(!1)}),pop==null||pop.addEventListener("click",e=>e.stopPropagation()),document.addEventListener("click",()=>openPop(!1)),document.addEventListener("keydown",e=>{e.key==="Escape"&&openPop(!1)}),window.addEventListener("resize",positionPop),window.addEventListener("scroll",positionPop,!0),document.querySelectorAll("#fa-filter-pop .fa-pchip").forEach(chip=>{chip.addEventListener("click",()=>{const wrap=chip.closest(".fa-pchips");wrap.classList.contains("fa-pchips-multi")?chip.classList.toggle("active"):(wrap.querySelectorAll(".fa-pchip").forEach(c=>c.classList.remove("active")),chip.classList.add("active")),updateFilterBadge()})}),(_j2=$2("fa-tag"))==null||_j2.addEventListener("input",updateFilterBadge);function applyFilters(){_page=1,openPop(!1),browse()}(_k2=$2("fa-filter-apply"))==null||_k2.addEventListener("click",applyFilters),(_l2=$2("fa-filter-reset"))==null||_l2.addEventListener("click",()=>{document.querySelectorAll('#fa-filter-pop .fa-pchips[data-group="tag"] .fa-pchip').forEach(c=>c.classList.remove("active")),["gender","age"].forEach(g=>{const wrap=document.querySelector(`#fa-filter-pop .fa-pchips[data-group="${g}"]`);wrap==null||wrap.querySelectorAll(".fa-pchip").forEach((c,i)=>c.classList.toggle("active",i===0))}),$2("fa-tag")&&($2("fa-tag").value=""),updateFilterBadge(),applyFilters()})}(),function(){const tabs=document.getElementById("gvo-tabs");if(!tabs)return;const map={direct:"tab-getvoices",fish:"fa-browser-card",eleven:"el-browser-card"};function show(src){map[src]||(src="direct"),Object.entries(map).forEach(([k,id])=>{const el=document.getElementById(id);el&&(k===src?el.style.removeProperty("display"):el.style.setProperty("display","none","important"))}),tabs.querySelectorAll(".gvo-tab").forEach(t=>t.classList.toggle("active",t.dataset.src===src));try{localStorage.setItem("gvo-src",src)}catch{}}tabs.querySelectorAll(".gvo-tab").forEach(t=>t.addEventListener("click",()=>show(t.dataset.src)));let saved="direct";try{saved=localStorage.getItem("gvo-src")||"direct"}catch{}show(saved)}();function cleanBaseUrl(url){return String(url||"").trim().replace(/\/+$/,"")}function getTtsBaseUrl(){var _a2;return cleanBaseUrl((_a2=$("s-tts-url"))==null?void 0:_a2.value)||"http://localhost:8020"}function getTtsV1Url(){const base=getTtsBaseUrl();return base.endsWith("/v1")?base:base+"/v1"}function getTtsStreamBaseUrl(){var _a2;return cleanBaseUrl(((_a2=$("s-tts-stream-url"))==null?void 0:_a2.value)||_appSettings.tts_stream_url)||"http://localhost:8023"}function getTtsStreamV1Url(){const base=getTtsStreamBaseUrl();return base.endsWith("/v1")?base:base+"/v1"}function getCreatorV1Url(){const loc=window.location,protocol=loc.protocol||"http:",port=loc.port?":"+loc.port:"",host=loc.hostname==="0.0.0.0"?"localhost":loc.hostname;return`${protocol}//${host}${port}/v1`}function updateCreatorUrlHints(){const warning=$("routing-url-warning"),badBindHost=window.location.hostname==="0.0.0.0";warning&&warning.classList.toggle("show",badBindHost)}function activeVoiceIds(){return(_voices||[]).filter(v=>v.enabled!==!1).slice().sort((a,b)=>a.id.localeCompare(b.id)).map(v=>v.id)}function integrationVoiceExample(){return activeVoiceIds()[0]||"EN_F_ExampleVoice"}function integrationVoiceList(){const ids=activeVoiceIds();return ids.length?ids.join(", "):"EN_F_ExampleVoice, DE_M_ExampleVoice"}function virtualDesignVoiceIds(){return Object.keys(loadDesignPresets?loadDesignPresets():{}).sort((a,b)=>a.localeCompare(b)).map(name=>"vd_"+(typeof _umlautSafe=="function"?_umlautSafe(name):name).replace(/[^A-Za-z0-9_.-]+/g,"_").replace(/^_+|_+$/g,""))}function renderIntegrationSnippets(){if(!$("snippet-sillytavern"))return;const base=getTtsBaseUrl(),v1=getTtsV1Url(),streamV1=getTtsStreamV1Url(),proxyV1=getCreatorV1Url(),proxyBase=proxyV1.replace(/\/v1$/,"");updateCreatorUrlHints();const voice=integrationVoiceExample(),voices=integrationVoiceList(),vdVoices=virtualDesignVoiceIds(),vdVoice=vdVoices[0]||"vd_EN_F_Warm_Narrator";$("integration-url-label").textContent="TTS backend: "+base,$("snippet-sillytavern").textContent=`Provider: OpenAI compatible TTS
API base URL: ${v1}
API key: dummy
Model: qwen3-tts
Voice: ${voice}
Custom voices: ${voices}
Streaming backend, if your SillyTavern TTS extension supports progressive playback:
API base URL: ${streamV1}
Endpoint: /audio/speech
Format: wav`,$("snippet-streaming-howto").textContent=`Direct streaming backend, no creator routing:
API base URL: ${streamV1}
Endpoint: /audio/speech
Model: tts-1
Voice: ${voice}
Response format: wav
Requirement: the app must start playback while the HTTP response is still arriving.
Current 8023 streaming service ignores per-request instruct/style text.
Streaming through TTS Voice Creator routing:
API base URL: ${proxyV1}
Voice: default or another incoming route voice
Routing tab: set Backend = Streaming for the matching rule
Response format: wav
Avoid before/after sounds for true streaming; route sounds and MP3 require buffering.
SillyTavern note:
Use OpenAI-compatible TTS if your extension supports progressive audio responses. If it waits for the whole file before playing, streaming works technically but will feel like buffered TTS.`,$("snippet-open-webui").textContent=`Admin settings -> Audio -> Text-to-Speech
Engine/provider: OpenAI compatible
API base URL: ${proxyV1}
API key: dummy
Model: tts-1
Voice: default
Routing tab example:
default + EN -> ${voice}
default + DE -> DE_M_YourGermanVoice`,$("snippet-home-assistant").textContent=`Home Assistant OpenAI TTS agent:
Base URL: ${proxyV1}
API key: dummy
Model: tts-1
Voice: default
Extra JSON payload: {"app":"Home Assistant"}
Audio format: mp3 or wav
REST command example using routing:
rest_command:
routed_tts:
url: "${proxyV1}/audio/speech"
method: POST
content_type: "application/json"
headers:
Authorization: "Bearer dummy"
payload: >
{"model":"tts-1","voice":"default","input":"{{ text }}","response_format":"mp3","app":"Home Assistant"}
Important: use this Creator proxy URL, not the direct Qwen3 backend URL ${v1}. Routing only runs through the Creator proxy.`,$("snippet-curl").textContent=`curl -s "${v1}/models"
curl -s "${v1}/audio/speech" \\
-H "Authorization: Bearer dummy" \\
-H "Content-Type: application/json" \\
-d '{"model":"qwen3-tts","voice":"${voice}","input":"Hello from Qwen3 TTS","response_format":"wav"}' \\
--output qwen3-tts-test.wav`,$("snippet-voice-design-proxy").textContent=`Virtual VoiceDesign mode - no WAV export needed
Use this app as the OpenAI-compatible TTS endpoint:
API base URL: ${proxyV1}
API key: dummy
Model: tts-1
Voice: ${vdVoice}
Available virtual voices:
${vdVoices.length?vdVoices.join(", "):"Save a Voice Design prompt preset first."}
curl -s "${proxyV1}/models"
curl -s "${proxyV1}/audio/speech" \\
-H "Authorization: Bearer dummy" \\
-H "Content-Type: application/json" \\
-d '{"model":"tts-1","voice":"${vdVoice}","input":"This line is generated through the VoiceDesign container.","response_format":"wav"}' \\
--output voicedesign-virtual.wav`,$("snippet-mcp-claude-cmd")&&($("snippet-mcp-claude-cmd").textContent=`claude mcp add voice-creator \\
--transport http \\
--url ${proxyBase}/mcp \\
--header "X-Voice-Creator-Client-Id: claude-code"`),$("snippet-mcp-claude-config")&&($("snippet-mcp-claude-config").textContent=`{
"mcpServers": {
"voice-creator": {
"url": "${proxyBase}/mcp",
"headers": { "X-Voice-Creator-Client-Id": "my-agent" }
}
}
}`),$("snippet-hotkey")&&($("snippet-hotkey").textContent=`pip install pynput sounddevice soundfile pyperclip requests
python hotkey_daemon.py --server ${proxyBase}`),$("snippet-speak")&&($("snippet-speak").textContent=`# Bind your client ID to a voice once (persisted in Settings)
curl -X PUT ${proxyBase}/speak/bindings/my-script \\
-H "Content-Type: application/json" \\
-d '{"voice":"${voice}"}'
# Generate speech \u2014 voice from binding, persona rewrite optional
curl -X POST ${proxyBase}/speak \\
-H "Content-Type: application/json" \\
-H "X-Voice-Creator-Client-Id: my-script" \\
-d '{"text":"Hello world","apply_persona":true}' \\
--output speech.wav
# Or specify voice directly
curl -X POST ${proxyBase}/speak \\
-H "Content-Type: application/json" \\
-d '{"text":"Hello world","voice":"${voice}"}' \\
--output speech.wav`)}document.querySelectorAll(".copy-snippet").forEach(btn=>btn.addEventListener("click",async()=>{const el=$(btn.dataset.snippet);el&&(await copyText(el.textContent),toast("Snippet copied","success"))})),(_e=$("show-api-btn"))==null||_e.addEventListener("click",()=>{window.open("/docs","_blank","noopener")}),(_f=$("integration-refresh-btn"))==null||_f.addEventListener("click",()=>{renderIntegrationSnippets(),toast("Integration examples refreshed","success")}),(_g=$("copy-active-voices-btn-integrations"))==null||_g.addEventListener("click",async()=>{const active=activeVoiceIds();if(!active.length){toast("No active voices to copy","error");return}await copyText(active.join(", ")),toast("Copied "+active.length+" active voices","success"),status("Copied active voices to clipboard")});let _ttsRoutes=[];const ROUTE_LANGS=[["*","Any"],["AUTO","Auto"],["EN","English"],["DE","German"],["FR","French"],["ES","Spanish"],["IT","Italian"],["PT","Portuguese"],["NL","Dutch"],["PL","Polish"]];function routeSelectOptions(value){return ROUTE_LANGS.map(([code,label])=>`<option value="${code}" ${code===value?"selected":""}>${escHtml(label)}</option>`).join("")}const ROUTE_BACKENDS=[["voice_clone","Voice Clone"],["streaming","Streaming"],["voice_design","Voice Design"],["nvidia_magpie","NVIDIA Magpie"],["nvidia_zeroshot","NVIDIA Zeroshot"],["nvidia_flow","NVIDIA Flow"]];let _routeSounds=[];function routeSoundOptions(value=""){const current=String(value||""),listed=new Set(_routeSounds.map(s=>String(s.path||""))),options=['<option value="">Browse sounds</option>'];return current&&!listed.has(current)&&options.push(`<option value="${escHtml(current)}" selected>${escHtml(current)}</option>`),_routeSounds.forEach(sound=>{const path=String(sound.path||"");if(!path)return;const duration=sound.duration!=null?` \xB7 ${Number(sound.duration).toFixed(1)}s`:"";options.push(`<option value="${escHtml(path)}" ${path===current?"selected":""}>${escHtml(path+duration)}</option>`)}),options.join("")}async function loadRouteSounds(){try{const r=await fetch("/api/route-sounds");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_routeSounds=Array.isArray(d.sounds)?d.sounds:[]}catch{_routeSounds=[]}}let _routeSoundPickerTarget=null,_routeSoundPlayingButton=null;function routeSoundUrl(path){return"/api/route-sounds/file/"+String(path||"").split("/").map(encodeURIComponent).join("/")}function setRouteSoundField(row,target,path){const field=row==null?void 0:row.querySelector(target==="before"?".route-before-sound":".route-after-sound");field&&(field.value=path||"",readRoutingForm())}function renderRouteSoundBrowser(){var _a2;const list=$("routing-sound-list");if(!list)return;const query=String(((_a2=$("routing-sound-search"))==null?void 0:_a2.value)||"").trim().toLowerCase(),sounds=query?_routeSounds.filter(sound=>String(sound.path||"").toLowerCase().includes(query)||String(sound.name||"").toLowerCase().includes(query)):_routeSounds;if(!_routeSounds.length){list.innerHTML='<div class="routing-log-empty">No sounds found yet. Upload one in a route row; only the selected file is imported.</div>';return}if(!sounds.length){list.innerHTML='<div class="routing-log-empty">No sounds match this search.</div>';return}list.innerHTML=sounds.map(sound=>{const path=String(sound.path||""),size=sound.size!=null?Math.max(1,Math.round(Number(sound.size)/1024))+" KB":"",duration=sound.duration!=null?Number(sound.duration).toFixed(1)+"s":size,type=sound.type?String(sound.type).toUpperCase():"",targetLabel=(_routeSoundPickerTarget==null?void 0:_routeSoundPickerTarget.target)==="after"?"Use after":"Use before";return`
<div class="routing-sound-item" data-path="${escHtml(path)}">
<button class="btn-secondary sound-play" type="button" title="Preview sound">\u25B6</button>
<div class="sound-path" title="${escHtml(path)}">${escHtml(path)}</div>
<div class="sound-meta">${escHtml(duration||"-")} ${type?"\xB7 "+escHtml(type):""}</div>
<button class="btn-secondary sound-use-current" type="button">${escHtml(targetLabel)}</button>
</div>`}).join("")}async function openRouteSoundBrowser(row,target){_routeSoundPickerTarget={row,target},await loadRouteSounds(),renderRouteSoundBrowser();const panel=$("routing-sound-browser");panel&&(panel.hidden=!1,panel.scrollIntoView({block:"nearest",behavior:"smooth"}));const label=target==="before"?"before sound":"after sound",note=$("routing-sound-browser-note");note&&(note.textContent=`Preview uploaded route sounds, then choose one for this ${label}. Upload imports only the selected file; this list also shows sounds already present in the sounds folders. ${_routeSounds.length} sounds available.`)}function closeRouteSoundBrowser(){const panel=$("routing-sound-browser");panel&&(panel.hidden=!0);const audio=$("routing-sound-preview");audio&&(audio.pause(),audio.hidden=!0,audio.removeAttribute("src")),_routeSoundPlayingButton&&(_routeSoundPlayingButton.textContent="\u25B6"),_routeSoundPlayingButton=null,_routeSoundPickerTarget=null}function playRouteSound(path,btn){const audio=$("routing-sound-preview");!audio||!path||(_routeSoundPlayingButton&&_routeSoundPlayingButton!==btn&&(_routeSoundPlayingButton.textContent="\u25B6"),_routeSoundPlayingButton=btn,btn.textContent="\u275A\u275A",audio.hidden=!1,audio.playbackRate=1,audio.src=routeSoundUrl(path),audio.onended=()=>{btn.textContent="\u25B6"},audio.onpause=()=>{_routeSoundPlayingButton===btn&&(btn.textContent="\u25B6")},audio.onplay=()=>{btn.textContent="\u275A\u275A"},audio.play().catch(e=>{btn.textContent="\u25B6",toast("Sound preview failed: "+e.message,"error")}))}const ROUTE_SPEED_PREVIEW_TEXT={EN:"This is a quick playback speed test.",DE:"Das ist ein kurzer Test der Wiedergabegeschwindigkeit.",FR:"Ceci est un test rapide de la vitesse de lecture.",ES:"Esta es una prueba r\xE1pida de la velocidad de reproducci\xF3n.",IT:"Questo \xE8 un rapido test della velocit\xE0 di riproduzione.",PT:"Este \xE9 um teste r\xE1pido da velocidade de reprodu\xE7\xE3o.",NL:"Dit is een snelle test van de afspeelsnelheid.",PL:"To jest kr\xF3tki test szybko\u015Bci odtwarzania."};async function previewRouteSpeed(row,btn){var _a2,_b2,_c2,_d2,_e2;const outputVoice=(_a2=row.querySelector(".route-output"))==null?void 0:_a2.value.trim();if(!outputVoice){toast("Set an output voice first","error");return}const backend=((_b2=row.querySelector(".route-backend"))==null?void 0:_b2.value)||"voice_clone",speed=Math.max(.5,Math.min(2,parseFloat((_c2=row.querySelector(".route-speed"))==null?void 0:_c2.value)||1)),lang=(((_d2=row.querySelector(".route-lang"))==null?void 0:_d2.value)||"EN").toUpperCase(),text=ROUTE_SPEED_PREVIEW_TEXT[lang]||ROUTE_SPEED_PREVIEW_TEXT.EN,audio=$("routing-sound-preview");if(!audio)return;_routeSoundPlayingButton&&_routeSoundPlayingButton!==btn&&(_routeSoundPlayingButton.textContent="\u25B6"),_routeSpeedPreviewButton&&_routeSpeedPreviewButton!==btn&&((_e2=_routeSpeedPreviewButton.querySelector(".mdi"))==null||_e2.classList.replace("mdi-pause","mdi-play")),_routeSpeedPreviewButton=btn;const icon=btn.querySelector(".mdi");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(outputVoice,text,"wav","",backend);audio.hidden=!1,audio.src=URL.createObjectURL(blob),audio.playbackRate=speed,icon==null||icon.classList.replace("mdi-play","mdi-pause"),audio.onended=()=>icon==null?void 0:icon.classList.replace("mdi-pause","mdi-play"),audio.onpause=()=>{_routeSpeedPreviewButton===btn&&(icon==null||icon.classList.replace("mdi-pause","mdi-play"))},await audio.play()}catch(e){icon==null||icon.classList.replace("mdi-pause","mdi-play"),toast("Preview failed: "+e.message,"error")}finally{btn.disabled=!1}}let _routeSpeedPreviewButton=null;function useRouteSound(path,target){const selected=_routeSoundPickerTarget||{},row=selected.row||document.querySelector(".routing-row"),useTarget=target||selected.target||"before";setRouteSoundField(row,useTarget,path),toast(`${useTarget==="before"?"Before":"After"} sound selected`,"success")}function routeBackendOptions(value){const current=value||"voice_clone";return ROUTE_BACKENDS.map(([code,label])=>`<option value="${code}" ${code===current?"selected":""}>${escHtml(label)}</option>`).join("")}function refreshRoutingVoiceOptions(){const dl=$("routing-voice-options");if(dl){const ids=[...activeVoiceIds(),...virtualDesignVoiceIds()];dl.innerHTML=[...new Set(ids)].map(id=>`<option value="${escHtml(id)}"></option>`).join("")}const soundsDl=$("routing-sound-options");soundsDl&&(soundsDl.innerHTML=_routeSounds.map(sound=>`<option value="${escHtml(sound.path||"")}"></option>`).join(""))}function newRoute(app="Open WebUI",inputVoice="default",language="*",outputVoice=""){return{id:"route_"+Date.now().toString(36)+"_"+Math.random().toString(36).slice(2,6),enabled:!0,app,input_voice:inputVoice,language,backend:"voice_clone",output_voice:outputVoice,before_sound:"",after_sound:""}}function renderRoutingList(){if($("routing-list")){if(refreshRoutingVoiceOptions(),$("routing-proxy-url").textContent=getCreatorV1Url(),updateCreatorUrlHints(),$("routing-status").textContent=_ttsRoutes.length?`${_ttsRoutes.length} route${_ttsRoutes.length===1?"":"s"}`:"No routes yet.",!_ttsRoutes.length){$("routing-list").innerHTML='<div class="note" style="padding:10px">No routing rules yet. Add a route or add the Open WebUI default examples.</div>';return}$("routing-list").innerHTML=_ttsRoutes.map((r,i)=>`
<div class="routing-grid routing-row" data-index="${i}">
<label class="toggle" title="Enable route">
<input type="checkbox" class="route-enabled" aria-label="Enable route" ${r.enabled!==!1?"checked":""}>
<span class="t-slider"></span>
</label>
<input class="route-app" aria-label="App / client" value="${escHtml(r.app||"Open WebUI")}" placeholder="Open WebUI, SillyTavern, Home Assistant, or *" title="Matches app/client JSON, X-TTS-App header, or detected client name. Use * for any app.">
<input class="route-input" aria-label="Input voice" value="${escHtml(r.input_voice||"default")}" placeholder="default">
<select class="route-lang" aria-label="Route language">${routeSelectOptions(String(r.language||"*").toUpperCase())}</select>
<select class="route-backend" aria-label="Route backend" title="Voice Clone uses the normal TTS URL, Streaming uses the streaming URL, Voice Design uses vd_ presets, NVIDIA Magpie uses fixed NVIDIA voices, NVIDIA Zeroshot/Flow use saved library WAVs as audio prompts.">${routeBackendOptions(r.backend||"voice_clone")}</select>
<span class="route-output-wrap"><input class="route-output" value="${escHtml(r.output_voice||"")}" list="routing-voice-options" placeholder="EN_F_VoiceName or vd_Preset"></span>
<div class="route-speed-cell">
<input class="route-speed" type="number" min="0.5" max="2" step="0.05" value="${Number(r.speed)>0?r.speed:1}" title="Playback speed multiplier (0.5x-2x). 1 = normal speed.">
<button type="button" class="btn-secondary route-speed-preview" title="Preview how this voice sounds at this speed"><span class="mdi mdi-play"></span></button>
</div>
<div class="route-sound-cell">
<input class="route-before-sound" value="${escHtml(r.before_sound||"")}" placeholder="sounds/start.wav" list="routing-sound-options">
<button class="btn-secondary route-sound-pick" data-target="before" title="Browse and preview uploaded sounds">Pick</button>
<button class="btn-secondary route-sound-upload" data-target="before" title="Upload one before sound">Upload</button>
</div>
<div class="route-sound-cell">
<input class="route-after-sound" value="${escHtml(r.after_sound||"")}" placeholder="sounds/end.wav" list="routing-sound-options">
<button class="btn-secondary route-sound-pick" data-target="after" title="Browse and preview uploaded sounds">Pick</button>
<button class="btn-secondary route-sound-upload" data-target="after" title="Upload one after sound">Upload</button>
</div>
<button class="btn-secondary routing-delete" title="Delete route">\xD7</button>
</div>
`).join(""),typeof VoicePicker!="undefined"&&document.querySelectorAll(".route-output").forEach(inp=>VoicePicker.attachTextPicker(inp))}}function readRoutingForm(){_ttsRoutes=[...document.querySelectorAll(".routing-row")].map((row,i)=>({id:(_ttsRoutes[Number(row.dataset.index)]||{}).id||`route_${i+1}`,enabled:row.querySelector(".route-enabled").checked,app:row.querySelector(".route-app").value.trim()||"*",input_voice:row.querySelector(".route-input").value.trim()||"default",language:row.querySelector(".route-lang").value||"*",backend:row.querySelector(".route-backend").value||"voice_clone",output_voice:row.querySelector(".route-output").value.trim(),speed:Math.max(.5,Math.min(2,parseFloat(row.querySelector(".route-speed").value)||1)),before_sound:row.querySelector(".route-before-sound").value.trim(),after_sound:row.querySelector(".route-after-sound").value.trim()}))}async function loadRoutingTab(){if($("routing-list")){$("routing-proxy-url").textContent=getCreatorV1Url(),updateCreatorUrlHints(),$("routing-status").textContent="Loading routing\u2026",$("routing-list").innerHTML=loadingMarkup("Loading routing","Loading active voices and routing rules for the proxy.",5),setBusyButton("routing-refresh-btn",!0),await Promise.allSettled([_voices.length?Promise.resolve():loadVoiceLibrary().catch(()=>{}),loadRouteSounds()]);try{const r=await fetch("/api/tts-routes");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_ttsRoutes=Array.isArray(d.routes)?d.routes:[],renderRoutingList(),status("Routing loaded"),loadRoutingLog()}catch(e){$("routing-status").textContent="Load failed",$("routing-list").innerHTML=`<div style="color:var(--red);padding:10px">Failed to load routes: ${escHtml(e.message)}</div>`,status("Routing load failed")}finally{setBusyButton("routing-refresh-btn",!1)}}}async function saveRoutingTab(){readRoutingForm(),$("routing-save-btn").disabled=!0;try{const r=await fetch("/api/tts-routes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({routes:_ttsRoutes})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}_ttsRoutes=(await r.json()).routes||_ttsRoutes,renderRoutingList(),toast("Routing saved","success"),status("TTS routing saved")}catch(e){toast("Save routes failed: "+e.message,"error")}finally{$("routing-save-btn").disabled=!1}}function renderRouteTestResult(d){const el=$("routing-test-result");if(!el)return;const health=d.voice_health||{},warnings=Array.isArray(health.warnings)?health.warnings:[];el.className="routing-test-result "+(warnings.length?"warn":"ok");const parts=[`${escHtml(d.requested_voice||"")} \u2192 ${escHtml(d.routed_voice||"")}`,`app ${escHtml(d.app||"-")}`,`backend ${escHtml(d.backend||"voice_clone")}`,`language ${escHtml(d.detected_language||"-")}`,d.matched?"matched route":"no route matched"];health.duration&&parts.push(`reference ${health.duration}s`),health.word_count!=null&&parts.push(`${health.word_count} words`),health.words_per_sec&&parts.push(`${health.words_per_sec} words/s`),warnings.length&&parts.push("Warning: "+warnings.map(escHtml).join("; "));const sounds=d.sounds||{};for(const[key,sound]of Object.entries(sounds)){const label=key==="before_sound"?"before sound":"after sound";parts.push(sound.ok?`${label} OK`:`${label}: ${escHtml(sound.error||"not found")}`),sound.ok||(el.className="routing-test-result warn")}el.innerHTML=parts.join(" \xB7 ")}function routingLogTime(ts){if(!ts)return"--:--:--";const d=new Date(ts);return Number.isNaN(d.getTime())?String(ts).slice(11,19)||"--:--:--":d.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}function routingLogBadge(item){const status2=String(item.status||item.kind||"log");return item.kind==="test"&&status2==="matched"?"test ok":item.kind==="test"&&status2==="no_match"?"test miss":status2.replace(/_/g," ")}function routingLogMeta(item){const parts=[];return item.backend&&parts.push(item.backend),item.language&&parts.push("lang "+item.language),item.response_format&&parts.push(item.response_format),item.duration!=null&&parts.push(Number(item.duration).toFixed(2)+"s"),item.bytes!=null&&parts.push(Math.round(Number(item.bytes)/1024)+" KB"),item.sounds&&Array.isArray(item.sounds)&&item.sounds.length&&parts.push("sounds "+item.sounds.join(",")),item.route_id&&parts.push("route "+item.route_id),item.client&&parts.push(item.client),parts.join(" \xB7 ")}let _routingLogItems=[],_routingLogFilter="all";function routingLogPassesFilter(item){const status2=String((item==null?void 0:item.status)||"").toLowerCase(),isError=status2==="error"||!!(item!=null&&item.error),isNoMatch=status2==="no_match"||(item==null?void 0:item.matched)===!1;return _routingLogFilter==="error"?isError:_routingLogFilter==="no_match"?isNoMatch:_routingLogFilter==="attention"?isError||isNoMatch:!0}function renderCurrentRoutingLog(){renderRoutingLog(_routingLogItems.filter(routingLogPassesFilter))}function renderRoutingLog(items=[]){const el=$("routing-log-list");if(el){if(!items.length){const filtered=_routingLogItems.length&&_routingLogFilter!=="all";el.innerHTML=`<div class="routing-log-empty">${filtered?"No routing log entries match this filter.":"No routing log entries yet. Test a route or send a TTS request through the Creator proxy."}</div>`;return}el.innerHTML=items.map(item=>{const status2=String(item.status||"log").replace(/[^a-z0-9_-]/gi,"_"),requested=item.requested_voice||"-",routed=item.routed_voice||"-",voice=requested===routed?requested:`${requested} \u2192 ${routed}`,text=item.error?`Error: ${item.error}`:item.text_preview||"";return`
<div class="routing-log-entry status-${escHtml(status2)}">
<div class="log-time">${escHtml(routingLogTime(item.ts))}</div>
<div class="routing-log-badge">${escHtml(routingLogBadge(item))}</div>
<div class="log-app" title="${escHtml(item.app||"-")}">${escHtml(item.app||"-")}</div>
<div class="log-voice" title="${escHtml(voice)}">${escHtml(voice)}</div>
<div class="log-meta" title="${escHtml(routingLogMeta(item))}">${escHtml(routingLogMeta(item)||"-")}</div>
<div class="log-text" title="${escHtml(text)}">${escHtml(text||"-")}</div>
</div>`}).join("")}}async function loadRoutingLog(){const el=$("routing-log-list");if(el)try{const r=await fetch("/api/tts-routing-log?limit=80");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_routingLogItems=Array.isArray(d.items)?d.items:[],renderCurrentRoutingLog()}catch(e){el.innerHTML=`<div class="routing-log-empty" style="color:var(--red)">Routing log unavailable: ${escHtml(e.message)}</div>`}}async function clearRoutingLog(){const btn=$("routing-log-clear-btn");btn&&(btn.disabled=!0);try{const r=await fetch("/api/tts-routing-log",{method:"DELETE"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}_routingLogItems=[],renderRoutingLog([]),toast("Routing log cleared","success")}catch(e){toast("Clear log failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}async function testRouting(){readRoutingForm();const btn=$("routing-test-btn"),el=$("routing-test-result");btn.disabled=!0,el.className="routing-test-result",el.textContent="Testing route...";try{const r=await fetch("/api/tts-route-test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app:$("routing-test-app").value.trim()||"Open WebUI",voice:$("routing-test-voice").value.trim()||"default",input:$("routing-test-text").value.trim()})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}renderRouteTestResult(await r.json()),loadRoutingLog()}catch(e){el.className="routing-test-result warn",el.textContent="Route test failed: "+e.message}finally{btn.disabled=!1}}async function uploadRouteSoundForRow(row,target){const input=document.createElement("input");input.type="file",input.accept="audio/*",input.multiple=!1,input.onchange=async()=>{if(!input.files||!input.files.length)return;const btn=row.querySelector(`.route-sound-upload[data-target="${target}"]`),field=row.querySelector(target==="before"?".route-before-sound":".route-after-sound");btn&&(btn.disabled=!0);try{const fd=new FormData;fd.append("file",input.files[0]);const r=await fetch("/api/route-sounds/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();field.value=d.path||"",await loadRouteSounds(),readRoutingForm(),renderRoutingList(),toast(`${target==="before"?"Before":"After"} sound uploaded`,"success"),status(`Uploaded route sound: ${d.path}`)}catch(e){toast("Sound upload failed: "+e.message,"error"),status("Sound upload failed")}finally{btn&&(btn.disabled=!1)}},input.click()}(_h=$("routing-refresh-btn"))==null||_h.addEventListener("click",loadRoutingTab),(_i=$("routing-add-btn"))==null||_i.addEventListener("click",()=>{readRoutingForm(),_ttsRoutes.push(newRoute()),renderRoutingList()}),(_j=$("routing-add-openwebui-btn"))==null||_j.addEventListener("click",()=>{readRoutingForm();const voices=activeVoiceIds(),firstByLang=lang=>voices.find(v=>v.toUpperCase().startsWith(lang+"_"))||"";_ttsRoutes.push(newRoute("Open WebUI","default","EN",firstByLang("EN"))),_ttsRoutes.push(newRoute("Open WebUI","default","DE",firstByLang("DE"))),renderRoutingList()}),(_k=$("routing-save-btn"))==null||_k.addEventListener("click",saveRoutingTab),(_l=$("routing-test-btn"))==null||_l.addEventListener("click",testRouting),(_m=$("routing-log-refresh-btn"))==null||_m.addEventListener("click",loadRoutingLog),(_n=$("routing-log-clear-btn"))==null||_n.addEventListener("click",clearRoutingLog),(_o=$("routing-log-filter"))==null||_o.addEventListener("change",e=>{_routingLogFilter=e.target.value||"all",renderCurrentRoutingLog()}),(_p=$("routing-list"))==null||_p.addEventListener("change",e=>{const picker=e.target.closest(".route-sound-picker");if(!picker)return;const field=picker.closest(".routing-row").querySelector(picker.dataset.target==="before"?".route-before-sound":".route-after-sound");field&&(field.value=picker.value||""),readRoutingForm()}),(_q=$("routing-list"))==null||_q.addEventListener("click",e=>{const speedPreviewBtn=e.target.closest(".route-speed-preview");if(speedPreviewBtn){previewRouteSpeed(speedPreviewBtn.closest(".routing-row"),speedPreviewBtn);return}const pickBtn=e.target.closest(".route-sound-pick");if(pickBtn){const row2=pickBtn.closest(".routing-row");openRouteSoundBrowser(row2,pickBtn.dataset.target);return}const uploadBtn=e.target.closest(".route-sound-upload");if(uploadBtn){const row2=uploadBtn.closest(".routing-row");uploadRouteSoundForRow(row2,uploadBtn.dataset.target);return}const btn=e.target.closest(".routing-delete");if(!btn)return;readRoutingForm();const row=btn.closest(".routing-row");_ttsRoutes.splice(Number(row.dataset.index),1),renderRoutingList()}),(_r=$("routing-sound-search"))==null||_r.addEventListener("input",debounce(renderRouteSoundBrowser,120)),(_s=$("routing-sound-refresh-btn"))==null||_s.addEventListener("click",async()=>{await loadRouteSounds(),renderRouteSoundBrowser()}),(_t=$("routing-sound-close-btn"))==null||_t.addEventListener("click",closeRouteSoundBrowser),(_u=$("routing-sound-list"))==null||_u.addEventListener("click",e=>{const item=e.target.closest(".routing-sound-item");if(!item)return;const path=item.dataset.path||"",playBtn=e.target.closest(".sound-play");if(playBtn){playRouteSound(path,playBtn);return}e.target.closest(".sound-use-current")&&useRouteSound(path,(_routeSoundPickerTarget==null?void 0:_routeSoundPickerTarget.target)||"before")});const CLONE_SAMPLE_TEXTS={EN:"Hello! My name is Sam, and this is my voice. I can speak softly or with great strength. The crisp winter air, warm firelight, and the gentle sound of rain \u2014 these are the things I love. Can you hear how clearly I speak?",DE:"Hallo! Ich hei\xDFe Alex und das ist meine Stimme. Ich kann leise fl\xFCstern oder mit voller Kraft sprechen. Klare Winterluft, warmes Kerzenlicht und der Klang des Regens am Fenster \u2014 das liebe ich. H\xF6rst du, wie deutlich ich spreche?",IT:"Ciao! Mi chiamo Marco e questa \xE8 la mia voce. Posso parlare dolcemente o con grande forza. L'aria fresca d'inverno, la luce calda del fuoco e il suono della pioggia \u2014 queste sono le cose che amo. Senti come parlo chiaramente?",ES:"\xA1Hola! Me llamo Carlos y esta es mi voz. Puedo hablar suavemente o con gran fuerza. El aire fr\xEDo del invierno, la c\xE1lida luz del fuego y el suave sonido de la lluvia \u2014 estas son las cosas que amo. \xBFPuedes o\xEDr lo claramente que hablo?",FR:"Bonjour ! Je m'appelle Sophie et voici ma voix. Je peux parler doucement ou avec grande force. L'air vif de l'hiver, la douce lumi\xE8re du feu et le son de la pluie \u2014 voil\xE0 ce que j'aime. Entends-tu comme je parle clairement ?",PT:"Ol\xE1! Meu nome \xE9 Ana e esta \xE9 a minha voz. Posso falar suavemente ou com grande for\xE7a. O ar fresco do inverno, a luz quente do fogo e o som da chuva \u2014 estas s\xE3o as coisas que amo. Consegues ouvir como falo claramente?",NL:"Hoi! Mijn naam is Laura en dit is mijn stem. Ik kan zacht fluisteren of met volle kracht spreken. De frisse winterlucht, het warme kaarslicht en het geluid van de regen \u2014 dat zijn de dingen die ik liefheb. Hoor je hoe helder ik spreek?",PL:"Cze\u015B\u0107! Mam na imi\u0119 Anna i to jest m\xF3j g\u0142os. Mog\u0119 m\xF3wi\u0107 cicho lub z ca\u0142\u0105 moc\u0105. Mro\u017Ane zimowe powietrze, ciep\u0142e \u015Bwiat\u0142o ognia i d\u017Awi\u0119k deszczu za oknem \u2014 to s\u0105 rzeczy, kt\xF3re kocham. Czy s\u0142yszysz, jak wyra\u017Anie m\xF3wi\u0119?"},CLONE_SAMPLE_NAMES={EN:"Sam",DE:"Alex",IT:"Marco",ES:"Carlos",FR:"Sophie",PT:"Ana",NL:"Laura",PL:"Anna"};function cloneSampleForLang(lang){var _a2;const txt=$("clone-sample-text"),def=CLONE_SAMPLE_NAMES[lang]||"";let t=CLONE_SAMPLE_TEXTS[lang]||CLONE_SAMPLE_TEXTS.EN;const name=(((_a2=$("clone-your-name"))==null?void 0:_a2.value)||"").trim();name&&def&&(t=t.replace(new RegExp("\\b"+def.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b"),name)),txt&&(txt.value=t,txt.dataset.sampleName=name||def)}window.initCloneSampleText=function(){const sel=$("clone-sample-lang"),txt=$("clone-sample-text");!sel||!txt||(cloneSampleForLang(sel.value),sel._cloneSampleBound||(sel.addEventListener("change",()=>cloneSampleForLang(sel.value)),sel._cloneSampleBound=!0))},initCloneSampleText();let ws=null,wsRegions=null,currentFileId=null,trimmedFileId=null,designedFileId=null,editingVoiceId=null,editingVoicePath=null;function initWaveSurfer(){ws&&(ws.destroy(),ws=null,wsRegions=null),wsRegions=WaveSurfer.Regions.create(),ws=WaveSurfer.create({container:"#waveform",waveColor:"#45475a",progressColor:"#89b4fa",cursorColor:"#cba6f7",height:90,normalize:!0,plugins:[wsRegions]}),ws.on("ready",()=>{const dur=ws.getDuration();$("trim-end").value=dur.toFixed(2),$("trim-end").max=dur.toFixed(2),$("trim-start").max=dur.toFixed(2),updateRegion()}),wsRegions.on("region-updated",r=>{$("trim-start").value=r.start.toFixed(2),$("trim-end").value=r.end.toFixed(2),updateDurationLabel()})}function updateRegion(){wsRegions.clearRegions();const s=parseFloat($("trim-start").value)||0,e=parseFloat($("trim-end").value)||(ws?ws.getDuration():0);wsRegions.addRegion({start:s,end:e,color:"rgba(137,180,250,0.25)",drag:!0,resize:!0}),updateDurationLabel()}function updateDurationLabel(){const d=Math.max(0,(parseFloat($("trim-end").value)||0)-(parseFloat($("trim-start").value)||0)),el=$("trim-duration");el.textContent=d.toFixed(1)+" s",el.className=d>=5&&d<=20?"dur-ok":d>20?"dur-warn":"dur-bad"}["trim-start","trim-end"].forEach(id=>$(id).addEventListener("input",()=>{ws&&updateRegion()})),$("play-btn").addEventListener("click",()=>{ws&&ws.playPause()}),$("play-selection-btn").addEventListener("click",()=>{ws&&ws.play(parseFloat($("trim-start").value)||0,parseFloat($("trim-end").value)||ws.getDuration())}),$("auto-trim-btn").addEventListener("click",async()=>{if(!currentFileId){toast("No audio loaded","error");return}$("auto-trim-btn").disabled=!0,status("Finding best TTS reference segment\u2026");try{const r=await fetch("/api/auto-trim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:currentFileId})});let d;if(r.ok)d=await r.json();else if(r.status===404||r.status===405)status("Backend auto trim unavailable; analysing audio in browser\u2026"),d=await clientAutoTrimBounds(currentFileId);else{const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText||"Auto trim failed")}$("trim-start").value=Number(d.start).toFixed(2),$("trim-end").value=Number(d.end).toFixed(2),ws&&updateRegion(),toast("Auto trim set: "+Number(d.duration).toFixed(1)+" s","success"),status(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error"),status("Auto trim failed")}finally{$("auto-trim-btn").disabled=!1}});function loadAudioId(id,dur,opts={}){currentFileId=id,trimmedFileId=null,designedFileId=null,editingVoiceId=opts.editingVoiceId||null,editingVoicePath=opts.editingVoicePath||null,$("trim-start").value="0",$("trim-end").value=dur.toFixed(2),$("waveform-card").style.display="",initWaveSurfer(),ws.load("/api/audio/"+id),$("save-result").style.display="none",$("trim-audio").style.display="none",$("no-audio-hint").style.display="",editingVoiceId&&($("voice-id-input").value=editingVoiceId,$("voice-id-input").dispatchEvent(new Event("input")),$("transcript-area").value=opts.transcript||"",status("Editing existing voice: "+editingVoiceId))}const dropZone=$("drop-zone"),fileInput=$("file-input");dropZone.addEventListener("click",()=>fileInput.click()),dropZone.addEventListener("dragover",e=>{e.preventDefault(),dropZone.classList.add("drag-over")}),dropZone.addEventListener("dragleave",()=>dropZone.classList.remove("drag-over")),dropZone.addEventListener("drop",e=>{e.preventDefault(),dropZone.classList.remove("drag-over"),e.dataTransfer.files.length&&uploadFile(e.dataTransfer.files[0])}),fileInput.addEventListener("change",()=>{fileInput.files.length&&uploadFile(fileInput.files[0])});async function uploadFile(file){status("Uploading "+file.name+"\u2026");const fd=new FormData;fd.append("file",file);try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();loadAudioId(d.id,d.duration),status("Loaded: "+file.name+" ("+d.duration.toFixed(1)+" s)"),toast("File loaded","success")}catch(e){toast("Upload failed: "+e.message,"error"),status("Upload failed")}}async function loadLibraryVoiceAudio(v){const audioResp=await fetch(voiceFileUrl(v),{cache:"no-store"});if(!audioResp.ok){const e=await audioResp.json().catch(()=>({}));throw new Error(e.detail||audioResp.statusText)}const blob=await audioResp.blob(),ext=(v.file_type||"wav").toLowerCase(),fd=new FormData;fd.append("file",new File([blob],`${v.id}.${ext}`,{type:blob.type||"audio/wav"}));const upload=await fetch("/api/upload",{method:"POST",body:fd});if(!upload.ok){const e=await upload.json().catch(()=>({}));throw new Error(e.detail||upload.statusText)}const d=await upload.json();return{id:d.id,voice_id:v.id,duration:d.duration,transcript:v.transcript||"",file_type:ext,path:v.path}}$("yt-btn").addEventListener("click",()=>{const url=$("yt-url").value.trim();if(!url)return;$("yt-btn").disabled=!0,$("yt-progress").textContent="Starting download\u2026";const es=new EventSource("/api/download-yt?url="+encodeURIComponent(url));es.onmessage=e=>{const d=JSON.parse(e.data);d.error?(toast("Download failed: "+d.error,"error"),$("yt-progress").textContent=d.error,$("yt-btn").disabled=!1,es.close()):d.done?(es.close(),$("yt-btn").disabled=!1,$("yt-progress").textContent="Done!",loadAudioId(d.id,d.duration),toast("YouTube audio loaded","success")):($("yt-progress").textContent=d.msg||"",d.pct&&status("Downloading\u2026 "+d.pct+"%"))},es.onerror=()=>{es.close(),$("yt-btn").disabled=!1}});const RAW_MIC_CONSTRAINTS={echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1};async function visibleMicrophoneCount(){var _a2;if(!((_a2=navigator.mediaDevices)!=null&&_a2.enumerateDevices))return null;try{return(await navigator.mediaDevices.enumerateDevices()).filter(device=>device.kind==="audioinput").length}catch{return null}}async function microphoneErrorMessage(error){const name=(error==null?void 0:error.name)||"",message=(error==null?void 0:error.message)||"",lowerMessage=message.toLowerCase(),micCount=await visibleMicrophoneCount();return name==="NotFoundError"||lowerMessage.includes("requested device not found")?micCount===0?"No microphone is visible to this browser. Connect or enable an input device in your OS/browser settings, then reload.":"The browser can see a microphone, but cannot open the selected/default input. Check the site permission and OS input selection, then reload.":name==="NotAllowedError"||name==="PermissionDeniedError"?"Microphone permission is blocked for this site. Allow microphone access in the address bar, then reload.":name==="NotReadableError"?"The microphone is busy or unavailable. Close other apps using it, then try again.":name==="SecurityError"?"Microphone access requires localhost or HTTPS.":message||"Microphone failed."}async function requestMicrophoneStream(options={}){var _a2;if(!((_a2=navigator.mediaDevices)!=null&&_a2.getUserMedia))throw new Error("Microphone requires HTTPS. Open the app via https://... or access it on localhost.");if(!options.raw)return navigator.mediaDevices.getUserMedia({audio:!0});try{return await navigator.mediaDevices.getUserMedia({audio:RAW_MIC_CONSTRAINTS})}catch(e){if((e==null?void 0:e.name)==="OverconstrainedError"||(e==null?void 0:e.name)==="NotFoundError")return navigator.mediaDevices.getUserMedia({audio:!0});throw e}}let _cloneMonState={stream:null,recordStream:null,audioCtx:null,sourceNode:null,gainNode:null,analyser:null,meterRaf:null,waveRing:null,monitoring:!1};function _cloneRenderMeter(level=0,db=-1/0,clipped=!1){const meter=$("clone-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<active&&(bar.classList.add("on"),db>-12&&i>11&&bar.classList.add("hot"),clipped&&i>14&&bar.classList.add("clip"))});const el=$("clone-db-readout");el&&(el.textContent=Number.isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB")}function _cloneStartMeter(){if(!_cloneMonState.analyser)return;_cloneMonState.meterRaf&&cancelAnimationFrame(_cloneMonState.meterRaf);const data=new Float32Array(_cloneMonState.analyser.fftSize),canvas=$("clone-live-wave"),RING=300,ADD=10;_cloneMonState.waveRing=new Float32Array(RING);const tick=()=>{_cloneMonState.analyser.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,level=Number.isFinite(db)?(db+60)/60:0;if(_cloneRenderMeter(level,db,peak>.98),canvas&&_cloneMonState.waveRing){const ring=_cloneMonState.waveRing;ring.copyWithin(0,ADD);for(let i=0;i<ADD;i++)ring[RING-ADD+i]=data[Math.floor(i*data.length/ADD)];const ctx=canvas.getContext("2d"),w=canvas.width,h=canvas.height;ctx.clearRect(0,0,w,h),ctx.beginPath(),ctx.strokeStyle=peak>.98?"#f38ba8":db>-12?"#f9e2af":"#a6e3a1",ctx.lineWidth=1.5;const mid=h/2;for(let i=0;i<RING;i++){const x=i/RING*w,y=mid-ring[i]*mid*.85;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y)}ctx.stroke()}_cloneMonState.meterRaf=requestAnimationFrame(tick)};tick()}async function _cloneStartMonitor(){var _a2;if(_cloneMonState.recordStream)return;const AudioCtx=window.AudioContext||window.webkitAudioContext;if(_cloneMonState.stream=await requestMicrophoneStream({raw:!0}),AudioCtx){_cloneMonState.audioCtx=new AudioCtx,_cloneMonState.sourceNode=_cloneMonState.audioCtx.createMediaStreamSource(_cloneMonState.stream),_cloneMonState.gainNode=_cloneMonState.audioCtx.createGain(),_cloneMonState.analyser=_cloneMonState.audioCtx.createAnalyser(),_cloneMonState.analyser.fftSize=1024;const dest=_cloneMonState.audioCtx.createMediaStreamDestination(),gainVal=parseFloat((_a2=$("clone-mic-gain"))==null?void 0:_a2.value)||1;_cloneMonState.gainNode.gain.value=gainVal,_cloneMonState.sourceNode.connect(_cloneMonState.gainNode),_cloneMonState.gainNode.connect(_cloneMonState.analyser),_cloneMonState.gainNode.connect(dest),_cloneMonState.recordStream=dest.stream,_cloneStartMeter()}else _cloneMonState.recordStream=_cloneMonState.stream;_cloneMonState.monitoring=!0,$("clone-monitor-btn")&&($("clone-monitor-btn").disabled=!0),$("clone-monitor-stop")&&($("clone-monitor-stop").disabled=!1)}function _cloneStopMonitor(){_cloneMonState.meterRaf&&cancelAnimationFrame(_cloneMonState.meterRaf),_cloneMonState.meterRaf=null,[_cloneMonState.sourceNode,_cloneMonState.gainNode,_cloneMonState.analyser].forEach(n=>{try{n&&n.disconnect()}catch{}}),_cloneMonState.stream&&_cloneMonState.stream.getTracks().forEach(t=>t.stop()),_cloneMonState.recordStream&&_cloneMonState.recordStream.getTracks().forEach(t=>t.stop()),_cloneMonState.audioCtx&&_cloneMonState.audioCtx.close().catch(()=>{}),Object.assign(_cloneMonState,{stream:null,recordStream:null,sourceNode:null,gainNode:null,analyser:null,audioCtx:null,monitoring:!1,waveRing:null}),_cloneRenderMeter(0,-1/0,!1);const wc=$("clone-live-wave");wc&&wc.getContext("2d").clearRect(0,0,wc.width,wc.height),$("clone-monitor-btn")&&($("clone-monitor-btn").disabled=!1),$("clone-monitor-stop")&&($("clone-monitor-stop").disabled=!0)}(_v=$("clone-monitor-btn"))==null||_v.addEventListener("click",async()=>{try{await _cloneStartMonitor(),status("Mic level monitor active")}catch(e){const m=await microphoneErrorMessage(e);toast(m,"error")}}),(_w=$("clone-monitor-stop"))==null||_w.addEventListener("click",()=>{_cloneStopMonitor(),status("Mic level monitor stopped")}),(_x=$("clone-mic-gain"))==null||_x.addEventListener("input",()=>{const v=parseFloat($("clone-mic-gain").value)||0;$("clone-mic-gain-value")&&($("clone-mic-gain-value").textContent=v.toFixed(2)+"x"),_cloneMonState.gainNode&&(_cloneMonState.gainNode.gain.value=v)}),_cloneRenderMeter();let mediaRec=null,recChunks=[],recTimer=null,recSecs=0;$("rec-start-btn").addEventListener("click",async()=>{try{await _cloneStartMonitor(),recChunks=[],recSecs=0,$("rec-time").textContent="0:00",$("rec-indicator").classList.add("active"),$("rec-start-btn").disabled=!0,$("rec-stop-btn").disabled=!1,recTimer=setInterval(()=>{recSecs++,$("rec-time").textContent=Math.floor(recSecs/60)+":"+String(recSecs%60).padStart(2,"0")},1e3),mediaRec=new MediaRecorder(_cloneMonState.recordStream||_cloneMonState.stream,{audioBitsPerSecond:256e3}),mediaRec.ondataavailable=e=>{e.data.size&&recChunks.push(e.data)},mediaRec.onstop=async()=>{clearInterval(recTimer),$("rec-indicator").classList.remove("active");const blob=new Blob(recChunks,{type:mediaRec.mimeType||"audio/webm"}),ext=(mediaRec.mimeType||"").includes("ogg")?".ogg":".webm";_cloneStopMonitor(),await uploadFile(new File([blob],"recording"+ext,{type:blob.type}))},mediaRec.start(100),status("Recording\u2026")}catch(e){_cloneStopMonitor(),toast(await microphoneErrorMessage(e),"error")}}),$("rec-stop-btn").addEventListener("click",()=>{mediaRec&&mediaRec.state!=="inactive"&&mediaRec.stop(),$("rec-start-btn").disabled=!1,$("rec-stop-btn").disabled=!0}),$("trim-btn").addEventListener("click",async()=>{var _a2,_b2,_c2;if(!currentFileId){toast("No audio loaded","error");return}try{const r=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:currentFileId,start:parseFloat($("trim-start").value)||0,end:parseFloat($("trim-end").value)||0})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();trimmedFileId=d.id,designedFileId=null,$("trim-audio").src="/api/audio/"+d.id,$("trim-audio").style.display="",$("no-audio-hint").style.display="none",switchTab("save"),toast("Trim done","success"),(_b2=(_a2=$("transcript-area"))==null?void 0:_a2.closest(".card"))==null||_b2.scrollIntoView({behavior:"smooth",block:"center"}),(_c2=window._cloneAutoTranscribe)==null||_c2.call(window)}catch(e){toast("Trim failed: "+e.message,"error")}});const DESIGN_LANG_CODE={Auto:"EN",English:"EN",Chinese:"ZH",Japanese:"JA",Korean:"KO",German:"DE",French:"FR",Spanish:"ES",Italian:"IT",Portuguese:"PT",Russian:"RU"},DESIGN_GENDER_WORD={F:"female",M:"male",N:"neutral"},DESIGN_PRESET_KEY="vcf-design-presets",DESIGN_PRESET_SEEDED_KEY="vcf-design-presets-seeded-v2",DEFAULT_DESIGN_PRESETS={EN_M_Young_Energetic:{description:"Young adult male voice, clear English, bright and energetic, moderately high pitch, quick but controlled speaking rate, confident and friendly, suitable for tutorials or streaming.",sample_text:"Hey everyone, welcome back. Today we are going to move quickly, keep it clear, and make this setup feel easy.",language:"English",gender:"M"},EN_F_Warm_Narrator:{description:"Adult female English narrator, warm and smooth, medium pitch, calm pace, gentle emotion, clear articulation, suited for audiobooks and voice assistant responses.",sample_text:"The room grew quiet as the morning light touched the window, and for a moment everything felt simple and kind.",language:"English",gender:"F"},DE_M_Elderly_Documentary:{description:"Aeltere maennliche deutsche Stimme, tief und resonant, langsam und gelassen, klar artikuliert, ruhig und dokumentarisch, mit serioeser und vertrauensvoller Praesenz.",sample_text:"Seit vielen Jahren beobachten wir diesen Ort, seine Geschichte und die Menschen, die ihn mit Leben fuellen.",language:"German",gender:"M"},DE_F_Young_Friendly:{description:"Junge weibliche deutsche Stimme, hell und freundlich, natuerliche Sprechgeschwindigkeit, klare Aussprache, leicht optimistisch und nahbar, passend fuer Assistenten und kurze Erklaerungen.",sample_text:"Hallo, schoen dass du da bist. Ich zeige dir kurz, wie alles funktioniert, Schritt fuer Schritt.",language:"German",gender:"F"},EN_N_Old_Wise_Assistant:{description:"Older neutral English voice, gentle and wise, slightly low pitch, slow measured pace, soothing tone, very clear pronunciation, calm personality for guidance and reflective narration.",sample_text:"Take a slow breath. We will look at the facts carefully, choose the next step, and keep moving.",language:"English",gender:"N"}},QWEN_DESIGN_SAMPLES={"qwen-timbre-reuse":{title:"Qwen Timbre Reuse",summary:"Reference clip for designing a reusable teen character timbre.",description:"Male, 17 years old, tenor range, gaining confidence - deeper breath support now, though vowels still tighten when nervous",text:"H-hey! You dropped your... uh... calculus notebook? I mean, I think it's yours? Maybe?",language:"English",gender:"M"},"acoustic-sausage-announcer":{title:"Acoustic Attribute Control - British announcer",summary:"Fast, loud, articulate British male delivery with excitement and performative authority.",description:`gender: Male.
pitch: Low male pitch with significant upward inflections for emphasis and excitement.
speed: Fast-paced delivery with deliberate pauses for dramatic effect.
volume: Loud and projecting, increasing notably during moments of praise and announcements.
age: Young adult to middle-aged adult.
clarity: Highly articulate and distinct pronunciation.
fluency: Very fluent speech with no hesitations.
accent: British English.
texture: Bright and clear vocal texture.
emotion: Enthusiastic and excited, especially when complimenting.
tone: Upbeat, authoritative, and performative.
personality: Confident, extroverted, and engaging.`,text:"Nine different, exciting ways of cooking sausage. Incredible. There were three outstanding deliveries in terms of the sausage being the hero. The first dish that we want to dissect, this individual smartly combined different proteins in their sausage. Great seasoning. The blend was absolutely spot on. Congratulations. Please step forward. Natasha.",language:"English",gender:"M"},"acoustic-character-laugh":{title:"Acoustic Attribute Control - theatrical character",summary:"Artificially high male character voice shifting from loud forced amusement to deliberate resignation.",description:`gender: Male.
pitch: Artificially high-pitched, slightly lowering after the initial laugh.
speed: Rapid during the laugh, then slowing to a deliberate pace.
volume: Loud laugh transitioning to a standard conversational level.
age: Young adult to middle-aged, performing a character voice.
clarity: Clear and distinct articulation.
fluency: Fluent delivery without hesitation.
accent: American English.
texture: Slightly strained and somewhat nasal quality.
emotion: Forced amusement shifting to feigned resignation.
tone: Initially playful, then shifts to a slightly put-upon tone.
personality: Theatrical and expressive.`,text:"Good one. Okay, fine, I'm just gonna leave this sock monkey here. Goodbye.",language:"English",gender:"M"},"age-control-surly-elvis":{title:"Age Control - middle-aged gravel",summary:"Low, resonant, slightly gravelly American male voice with a commanding opening.",description:`gender: Male.
pitch: Low male pitch, generally stable.
speed: Deliberate pace, slowing slightly after the initial exclamation.
volume: Starts loud, then transitions to a projected conversational volume.
age: Middle-aged adult.
clarity: High clarity with distinct pronunciation.
fluency: Highly fluent.
accent: American English.
texture: Resonant and slightly gravelly.
emotion: Initially commanding, shifting to narrative amusement.
tone: Authoritative start, moving to an engaging, descriptive tone.
personality: Confident and performative.`,text:"Older gentleman, 110, maybe 111 years old, sort of a surly Elvis thing happening with him. He smiles like this. Seen him around?",language:"English",gender:"M"},"gradual-control-anger":{title:"Gradual Control - emotional escalation",summary:"Female voice that begins neutral and quickly escalates into sharp anger and accusation.",description:`gender: Female.
pitch: Mid-range female pitch, rising sharply with frustration.
speed: Starts measured, then accelerates rapidly during emotional outburst.
volume: Begins conversational, escalates quickly to loud and forceful.
age: Young adult to middle-aged.
clarity: High clarity and distinct articulation throughout.
fluency: Highly fluent with no significant pauses or fillers.
accent: General American English.
texture: Bright and clear vocal quality.
emotion: Shifts abruptly from neutral acceptance to intense resentment and anger.
tone: Initially accepting, becomes sharply accusatory and confrontational.
personality: Assertive and emotionally expressive when provoked.`,text:"Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you-",language:"English",gender:"F"},"human-likeness-digital-nomad":{title:"Human-likeness - casual self-aware monologue",summary:"Warm male conversational voice with natural laughter, hesitations, and self-deprecating humor.",description:"A relaxed, naturally expressive male voice in his late twenties to early thirties, with a moderately low pitch, casual speaking rate, and conversational volume; deliver lines with a light, self-deprecating tone, breaking into genuine, easygoing laughter at moments of embarrassment, while maintaining clear articulation and an overall warm, approachable clarity.",text:`Yeah, so--uh--I'm a digital nomad, right? So... pretty much all my communication is just, like, texts and messages. And now, you know, there's these AI agents that can, uh... reply for you? Which is--heh--convenient, sure, I guess? But also... kinda delicate, you know?
Like, you'll type something super short--like, "Yep, sounds good"--and it'll turn that into this whole... warm, polished paragraph. Like, way nicer than I'd ever write myself. huh... ha Seriously, I sound like a Hallmark card all of a sudden.
But then... once you outsource that... what's the other person actually hearing? Are they hearing me... or just some... generic, friendly-bot voice? Man, that's weird to even say out loud.`,language:"English",gender:"M"},"background-marcus-cole":{title:"Background Information - Marcus Cole",summary:"Broadcast booth announcer profile with bright, agile, urgent delivery.",description:`Character Name: Marcus Cole
Voice Profile: A bright, agile male voice with a natural upward lift, delivering lines at a brisk, energetic pace. Pitch leans high with spark, volume projects clearly--near-shouting at peaks--to convey urgency and excitement. Speech flows seamlessly, fluently, each word sharply defined, riding a current of dynamic rhythm.
Background: Longtime broadcast booth announcer for national television, specializing in live interstitials and public engagement spots. His voice bridges segments, rallies action, and keeps momentum alive--from voter drives to entertainment news.
Presence: Late 50s, neatly groomed, dressed in a crisp shirt under studio lights. Moves with practiced ease, eyes locked on the script, energy coiled and ready.
Personality: Energetic, precise, inherently engaging. He doesn't just read--he propels. Behind the speed is intent: to inform fast, to move people to act. Whether it's "text VOTE to 5703" or a star-studded tease, he makes it feel immediate, vital.`,text:"Lot being you watching. 1-866-IDLE-03 for JPL. That's 1-866-436-5703. Or text the word VOTE to 5703. Diana DeGarmo's next with more from the movies right after this brief intermission on American Idol.",language:"English",gender:"M"},"timbre-reuse-lucas-mia":{title:"Timbre Reuse - Lucas and Mia",summary:"Two-character teen dialogue using native VoiceDesign speaker-profile switching.",description:`"Lucas": "Male, 17 years old, tenor range, gaining confidence - deeper breath support now, though vowels still tighten when nervous"
"Mia": "Female, 16 years old, mezzo-soprano range, softening - lowering register to intimate speaking voice, consonants softening"`,text:`Lucas:H-hey! You dropped your... uh... calculus notebook? I mean, I think it's yours? Maybe?
Mia:Oh wow, my mortal enemy - Mr. Thompson's problem sets. Thanks for rescuing me from that F.
Lucas:No problem! I actually... kinda finished those already? If you want to compare answers or something...
Mia:Is this your sneaky way of saying you want to study together, Lucas? Because I saw you staring during lab partners sign-up.
Lucas:What? No! I mean yes but not like... I just think you're... your titration technique is really precise!
Mia:That's the nerdiest compliment I've ever gotten. Tell you what - help me survive pre-calc and I'll teach you how to actually flirt.
Lucas:Wow, harsh. And here I thought my titration line was smooth.
Mia:It was adorable. Like when you tripped over your shoelaces in the hall yesterday. Or that time you-
Lucas:Okay okay! I get it, I'm a disaster. So... library after school? I'll bring the graphing calculators?
Mia:Only if you promise not to spill coffee on my notes again... though I guess watching you panic-clean was pretty cute.`,language:"English",gender:"N",dialogue:!0}};let currentDesignSource=null;function loadDesignPresets(){try{return JSON.parse(localStorage.getItem(DESIGN_PRESET_KEY)||"{}")}catch{return{}}}function saveDesignPresets(presets){localStorage.setItem(DESIGN_PRESET_KEY,JSON.stringify(presets))}async function syncDesignPresetsToServer(){try{await fetch("/api/voice-design-presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(loadDesignPresets())}),renderIntegrationSnippets()}catch(e){status("Voice Design preset sync failed: "+e.message)}}function seedDesignPresets(){const presets=loadDesignPresets();let changed=!1;Object.entries(DEFAULT_DESIGN_PRESETS).forEach(([name,preset])=>{if(!presets[name]){presets[name]=preset,changed=!0;return}["description","sample_text","language","gender"].forEach(key=>{!presets[name][key]&&preset[key]&&(presets[name][key]=preset[key],changed=!0)})}),(changed||!localStorage.getItem(DESIGN_PRESET_SEEDED_KEY))&&saveDesignPresets(presets),localStorage.setItem(DESIGN_PRESET_SEEDED_KEY,"1"),changed&&syncDesignPresetsToServer()}function refreshDesignPresetSelect(){const presets=loadDesignPresets(),sel=$("design-preset-select"),prev=sel.value;sel.innerHTML='<option value="">\u2014 preset \u2014</option>',Object.keys(presets).sort((a,b)=>a.localeCompare(b)).forEach(name=>{const opt=document.createElement("option");opt.value=opt.textContent=name,sel.appendChild(opt)}),presets[prev]&&(sel.value=prev),renderDesignPresetLibrary()}function applyDesignPreset(name){const preset=loadDesignPresets()[name];if(!preset){toast("Preset not found","error");return}$("design-instruct").value=preset.description||"",$("design-sample-text").value=preset.sample_text||preset.text||$("design-sample-text").value||"",$("design-language").value=preset.language||"Auto",$("design-gender").value=preset.gender||"N",$("design-preset-name").value=name,$("design-preset-select").value=name,currentDesignSource={name,gender:preset.gender||"N",language:preset.language||"Auto",text:$("design-sample-text").value||"",description:preset.description||""},toast("Preset loaded: "+name,"success")}function renderDesignPresetLibrary(){const lib=$("design-preset-library");if(!lib)return;const presets=loadDesignPresets(),names=Object.keys(presets).sort((a,b)=>a.localeCompare(b));if(!names.length){lib.innerHTML='<div class="note">No saved prompt presets yet.</div>';return}lib.innerHTML="",names.forEach(name=>{const p=presets[name],row=document.createElement("div");row.className="design-preset-row",row.innerHTML=`
<strong>${escHtml(name)}</strong>
<span>${escHtml(p.gender||"N")}</span>
<span>${escHtml(p.language||"Auto")}</span>
<span class="preset-desc" title="${escHtml(p.description||"")}">${escHtml(p.description||"")}</span>
<span class="preset-transcript" title="${escHtml(p.sample_text||p.text||"")}">${escHtml(p.sample_text||p.text||"")}</span>
<span class="preset-actions">
<button class="btn-secondary preset-use">Use</button>
<button class="btn-primary preset-preview">Preview</button>
<button class="btn-secondary preset-delete">Delete</button>
</span>
`,row.querySelector(".preset-use").addEventListener("click",()=>applyDesignPreset(name)),row.querySelector(".preset-delete").addEventListener("click",()=>{const all=loadDesignPresets();delete all[name],saveDesignPresets(all),syncDesignPresetsToServer(),refreshDesignPresetSelect(),toast("Preset deleted: "+name,"success")}),row.querySelector(".preset-preview").addEventListener("click",async e=>{e.currentTarget.disabled=!0;try{const sampleText=p.sample_text||p.text||$("design-sample-text").value,r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(p.description||"",sampleText,p.language||"Auto",p))});if(!r.ok){const err=await r.json().catch(()=>({}));throw new Error(err.detail||r.statusText)}const d=await r.json();$("design-audio").src="/api/audio/"+d.id,$("design-result").style.display="flex",$("design-audio").play().catch(()=>{})}catch(err){toast("Preset preview failed: "+err.message,"error")}finally{e.currentTarget.disabled=!1}}),lib.appendChild(row)})}function renderQwenSampleCards(){const list=$("qwen-sample-list");list&&(list.innerHTML="",Object.entries(QWEN_DESIGN_SAMPLES).forEach(([key,sample])=>{const card=document.createElement("div");card.className="qwen-sample design-sample-grid",card.dataset.qwenSample=key,card.innerHTML=`
<strong title="${escHtml(sample.summary||sample.title||key)}">${escHtml(sample.title||key)}</strong>
<span class="sample-sex">${escHtml(sample.gender||"N")}</span>
<span class="sample-language">${escHtml(sample.language||"Auto")}</span>
<span class="sample-desc" title="${escHtml(sample.description||"")}">${escHtml(sample.description||"")}</span>
<span class="sample-text" title="${escHtml(sample.text||"")}">${escHtml(sample.text||"")}</span>
<div class="qwen-sample-actions">
<button class="btn-primary qwen-preview">Preview</button>
<button class="btn-secondary qwen-use">Use</button>
<span class="note qwen-state"></span>
</div>
<audio controls></audio>
`,list.appendChild(card)}))}function applyQwenSample(sample){$("design-instruct").value=sample.description,$("design-sample-text").value=sample.text,$("design-language").value=sample.language,$("design-gender").value=sample.gender,currentDesignSource=sample,$("design-result").style.display="none",$("design-save-result").style.display="none",$("design-instruct").scrollIntoView({behavior:"smooth",block:"nearest"})}function isDialogueDesign(instruct,text,source=null){if(source&&source.dialogue)return!0;const speakers=new Set;if(String(instruct||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^"?([^":]+)"?\s*:\s*"?(.+?)"?$/);match&&speakers.add(match[1].trim())}),speakers.size<2)return!1;const turnSpeakers=new Set;return String(text||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^([^:]{1,40}):\s*(.+)$/);match&&speakers.has(match[1].trim())&&turnSpeakers.add(match[1].trim())}),turnSpeakers.size>=2}function voiceDesignPayload(instruct,sampleText,language,source=null,gender=null){var _a2;return{instruct,sample_text:sampleText,language,gender:gender||(source==null?void 0:source.gender)||((_a2=$("design-gender"))==null?void 0:_a2.value)||"",dialogue:isDialogueDesign(instruct,sampleText,source)}}let _dVoiceIdManual=!1;function designSafeName(name){const base=name||"VoiceDesign";return(typeof _umlautSafe=="function"?_umlautSafe(base):String(base)).replace(/^[A-Z]{2}_[FMN]_/,"").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,42)||"VoiceDesign"}function voiceIdSafePart(value,fallback="style"){return(typeof _umlautSafe=="function"?_umlautSafe(value||fallback):String(value||fallback)).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,32)||fallback}function suggestedStyleVoiceId(baseId,style){const suffix=voiceIdSafePart(style||"style");return`${baseId}_${suffix}`.slice(0,96)}function _updateDVoiceId(){if(_dVoiceIdManual)return;const lang=$("d-lang").value,gender=$("d-gender").value,name=$("d-name").value.trim();$("d-voice-id").value=name?`${lang}_${gender}_${name}`:""}["d-lang","d-gender"].forEach(id=>$(id).addEventListener("change",_updateDVoiceId)),$("d-name").addEventListener("input",()=>{_dVoiceIdManual=!1,_updateDVoiceId()}),$("d-voice-id").addEventListener("input",()=>{_dVoiceIdManual=!0}),seedDesignPresets(),refreshDesignPresetSelect(),renderQwenSampleCards(),syncDesignPresetsToServer(),$("design-preset-select").addEventListener("change",()=>{$("design-preset-select").value&&applyDesignPreset($("design-preset-select").value)}),$("design-preset-load").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}applyDesignPreset(name)}),$("design-preset-save").addEventListener("click",()=>{const name=$("design-preset-name").value.trim()||$("design-preset-select").value;if(!name){toast("Enter a preset name","error"),$("design-preset-name").focus();return}const presets=loadDesignPresets();presets[name]={description:$("design-instruct").value,sample_text:$("design-sample-text").value,language:$("design-language").value,gender:$("design-gender").value,dialogue:isDialogueDesign($("design-instruct").value,$("design-sample-text").value,currentDesignSource)},saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-select").value=name,toast("Preset saved: "+name,"success")}),$("design-preset-delete").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}const presets=loadDesignPresets();if(!presets[name]){toast("Preset not found","error");return}delete presets[name],saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-name").value="",toast("Preset deleted: "+name,"success")}),["design-instruct","design-sample-text"].forEach(id=>$(id).addEventListener("input",()=>{currentDesignSource=null,id==="design-sample-text"&&($("d-transcript").value=$("design-sample-text").value)})),document.querySelectorAll(".qwen-sample").forEach(card=>{const sample=QWEN_DESIGN_SAMPLES[card.dataset.qwenSample],state=card.querySelector(".qwen-state"),audio=card.querySelector("audio");card.querySelector(".qwen-use").addEventListener("click",()=>{applyQwenSample(sample),toast("Voice Design sample loaded","success")}),card.querySelector(".qwen-preview").addEventListener("click",async e=>{const btn=e.currentTarget;btn.disabled=!0,state.textContent="Generating preview\u2026";try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(sample.description,sample.text,sample.language,sample))});if(!r.ok){const err=await r.json().catch(()=>({}));throw new Error(err.detail||r.statusText)}const d=await r.json();audio.src="/api/audio/"+d.id,audio.style.display="",audio.play().catch(()=>{}),state.textContent="Preview ready"}catch(err){state.textContent="Preview failed",toast("Sample preview failed: "+err.message,"error")}finally{btn.disabled=!1}})});async function runVoiceDesign(){const baseInstruct=$("design-instruct").value.trim(),sample=$("design-sample-text").value.trim(),dialogue=isDialogueDesign(baseInstruct,sample,currentDesignSource),instruct=baseInstruct;if(!instruct){toast("Enter a voice description first","error");return}$("design-generate-btn").disabled=!0,$("design-status").textContent="Generating\u2026",$("design-result").style.display="none",$("design-save-result").style.display="none",status("Generating voice design\u2026");try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(instruct,sample,$("design-language").value,currentDesignSource,$("design-gender").value))});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();designedFileId=d.id,trimmedFileId=null,editingVoiceId=null,$("design-audio").src="/api/audio/"+d.id,$("design-result").style.display="flex",$("design-status").textContent="Done ("+d.duration.toFixed(1)+" s)";const langCode=DESIGN_LANG_CODE[$("design-language").value]||"EN";$("d-lang").value=langCode,$("d-gender").value=$("design-gender").value,$("d-name").value=designSafeName((currentDesignSource==null?void 0:currentDesignSource.title)||(currentDesignSource==null?void 0:currentDesignSource.name)||$("design-preset-name").value||"VoiceDesign"),_dVoiceIdManual=!1,_updateDVoiceId(),$("d-transcript").value=sample,$("trim-audio").src="/api/audio/"+d.id,$("trim-audio").style.display="",$("no-audio-hint").style.display="none",$("transcript-area").value||($("transcript-area").value=sample),$("design-audio").play().catch(()=>{}),$("design-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Voice generated and export fields filled.","success"),status("Voice design ready")}catch(e){$("design-status").textContent="Failed: "+e.message,toast("Voice design failed: "+e.message,"error"),status("Voice design failed")}finally{$("design-generate-btn").disabled=!1}}$("design-generate-btn").addEventListener("click",runVoiceDesign),$("design-retry-btn").addEventListener("click",runVoiceDesign),$("design-save-btn").addEventListener("click",async()=>{if(!designedFileId){toast("No voice generated yet","error");return}const voiceId=$("d-voice-id").value.trim();if(!voiceId){toast("Enter a Voice ID first","error"),$("d-name").focus();return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("design-save-btn").disabled=!0;try{const r=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designedFileId,voice_id:voiceId,transcript:$("d-transcript").value})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const saved=await r.json();await saveMeta(saved.voice_id,{gender:$("d-gender").value,flag:LANG_FLAG_DEFAULT[$("d-lang").value]||void 0,transcript:$("d-transcript").value,note:"Voice Design: "+$("design-instruct").value.slice(0,240),origin:"designed",voice_design_prompt:$("design-instruct").value}).catch(()=>{}),await loadVoiceLibrary().catch(()=>{}),$("design-save-result").style.display="flex",$("design-save-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Exported to Voice Clone Library: "+saved.voice_id,"success"),status("Exported to Voice Clone Library: "+saved.voice_id)}catch(e){toast("Save failed: "+e.message,"error")}finally{$("design-save-btn").disabled=!1}}),$("design-download-btn").addEventListener("click",()=>{if(!designedFileId)return;const a=document.createElement("a");a.href="/api/audio/"+designedFileId,a.download=($("d-voice-id").value.trim()||"voice_design")+".wav",a.click()}),(_y=$("clone-refresh-stt-btn"))==null||_y.addEventListener("click",async()=>{var _a2;$("clone-refresh-stt-btn").disabled=!0;try{await refreshSttBackends((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)}finally{$("clone-refresh-stt-btn").disabled=!1}}),$("transcribe-btn").addEventListener("click",async()=>{var _a2,_b2;const id=trimmedFileId||designedFileId||currentFileId;if(!id){toast("No audio to transcribe","error");return}const btn=$("transcribe-btn"),status2=$("transcribe-status"),area=$("transcript-area"),orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span> Transcribing\u2026',status2&&(status2.className="clone-tr-status working",status2.innerHTML='<span class="reh-imsdb-spinner"></span> Listening to your recording\u2026'),area&&(area.classList.add("transcribing"),area.placeholder="Transcribing your audio \u2014 please wait\u2026");try{const backend=((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)||"configured",r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,backend})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();area&&(area.value=d.text),status2&&(status2.className="clone-tr-status done",status2.innerHTML='<span class="mdi mdi-check-circle"></span> Transcribed'),toast("Transcription complete","success"),(_b2=window._cloneScheduleAutoSave)==null||_b2.call(window)}catch(e){status2&&(status2.className="clone-tr-status error",status2.innerHTML=`<span class="mdi mdi-alert-circle"></span> Failed: ${escHtml(e.message||String(e))}`),toast("Transcription failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,area&&(area.classList.remove("transcribing"),area.placeholder="Type or auto-transcribe the spoken text\u2026")}}),function(){var _a2,_b2,_c2;const g=id=>document.getElementById(id);let _idManual=!1,_prevName="Sam",_autoSaveTimer=null,_lastAutoSaved="";function buildVoiceId(){var _a3,_b3,_c3;if(_idManual){scheduleAutoSave();return}const lang=((_a3=g("lang-select"))==null?void 0:_a3.value)||"EN",gender=((_b3=g("gender-select"))==null?void 0:_b3.value)||"N",name=(((_c3=g("name-input"))==null?void 0:_c3.value)||"").trim().replace(/\s+/g,""),vid=g("voice-id-input");vid&&name&&(vid.value=`${lang}_${gender}_${name}`,vid.dispatchEvent(new Event("input"))),scheduleAutoSave()}const nameField=g("clone-your-name");nameField==null||nameField.addEventListener("input",()=>{const name=nameField.value.trim();if(!name)return;const sample=g("clone-sample-text");if(sample){const prev=sample.dataset.sampleName||_prevName,re=new RegExp("\\b"+prev.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b");re.test(sample.value)&&(sample.value=sample.value.replace(re,name)),sample.dataset.sampleName=name}_prevName=name;const ni=g("name-input");ni&&(ni.value=name.replace(/\s+/g,"")),buildVoiceId()}),["lang-select","gender-select"].forEach(id=>{var _a3;return(_a3=g(id))==null?void 0:_a3.addEventListener("change",buildVoiceId)}),(_a2=g("name-input"))==null||_a2.addEventListener("input",buildVoiceId),(_b2=g("voice-id-input"))==null||_b2.addEventListener("input",e=>{e.isTrusted&&(_idManual=!0),scheduleAutoSave()}),(_c2=g("transcript-area"))==null||_c2.addEventListener("input",scheduleAutoSave),window._cloneAutoTranscribe=function(){var _a3;const ta=g("transcript-area");if(ta&&ta.value.trim()){scheduleAutoSave();return}(_a3=g("transcribe-btn"))==null||_a3.click()};function canAutoSave(){var _a3,_b3;const vid=(((_a3=g("voice-id-input"))==null?void 0:_a3.value)||"").trim(),tr=(((_b3=g("transcript-area"))==null?void 0:_b3.value)||"").trim();return!!((typeof trimmedFileId!="undefined"&&trimmedFileId||typeof designedFileId!="undefined"&&designedFileId)&&vid&&tr&&(typeof validateVoiceId!="function"||validateVoiceId(vid)))}function scheduleAutoSave(){const toggle=g("clone-autosave-toggle");!toggle||!toggle.checked||(clearTimeout(_autoSaveTimer),_autoSaveTimer=setTimeout(()=>{var _a3;if(!canAutoSave())return;const sig=(g("voice-id-input").value+"|"+g("transcript-area").value).trim();sig!==_lastAutoSaved&&(_lastAutoSaved=sig,(_a3=g("save-btn"))==null||_a3.click())},1600))}window._cloneScheduleAutoSave=scheduleAutoSave}(),function(){const picker=document.getElementById("clone-src-picker");if(!picker)return;const cards=[...document.querySelectorAll(".clone-src-card")],tabs=[...picker.querySelectorAll(".clone-src-tab")],KEY="clone-src-choice";function show(src){cards.forEach(c=>{c.hidden=c.dataset.src!==src}),tabs.forEach(t=>t.classList.toggle("active",t.dataset.src===src));try{localStorage.setItem(KEY,src)}catch{}}tabs.forEach(t=>t.addEventListener("click",()=>show(t.dataset.src))),show(localStorage.getItem(KEY)||"mic")}(),$("save-btn").addEventListener("click",async()=>{const id=trimmedFileId||designedFileId||currentFileId;if(!id){toast("No audio ready","error");return}const voiceId=$("voice-id-input").value.trim();if(!voiceId){toast("Enter a Voice ID","error");return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("save-btn").disabled=!0;try{const payload={id,voice_id:voiceId,path:editingVoicePath,transcript:$("transcript-area").value},sendSave=endpoint=>fetch(endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});let fallbackSave=!1,r=await sendSave(editingVoiceId?"/api/voice-replace":"/api/save");if(editingVoiceId&&(r.status===404||r.status===405)&&(fallbackSave=!0,status("Update endpoint unavailable; saving as a regular voice\u2026"),r=await sendSave("/api/save")),!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();$("save-result").style.display="",toast((editingVoiceId?"Voice updated: ":"Voice saved: ")+d.voice_id,"success"),fallbackSave&&editingVoiceId&&voiceId!==editingVoiceId&&((await fetch("/api/voice/"+encodeURIComponent(editingVoiceId),{method:"DELETE"})).ok||status("Saved renamed voice; old library entry may need manual deletion.")),editingVoiceId=null,editingVoicePath=null}catch(e){toast("Save failed: "+e.message,"error")}finally{$("save-btn").disabled=!1}});let _voices=[],_pendingSelectId=null,_sortField="id",_sortDir=1,_libraryIssueFilter="",_activePlayButton=null,_activePlayVoiceId=null,_activePlayUrl=null,_libraryLoadPromise=null;const BENCHMARK_SAMPLE_STORAGE_KEY="vcf-benchmark-sample-text",_VL_CACHE_KEY="ttsvc_vc";function _vlCacheRead(){try{return JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY)||"null")}catch{return null}}function _vlCacheWrite(voices){try{sessionStorage.setItem(_VL_CACHE_KEY,JSON.stringify(voices))}catch{}}function _vlCacheClear(){try{sessionStorage.removeItem(_VL_CACHE_KEY)}catch{}}window._vlCacheClear=_vlCacheClear;const _libraryFilters={text:"",lang:"",sex:"",type:"",rating:""};let _libraryFilterOptionsSig="";const DEFAULT_BENCHMARK_SAMPLE_TEXT="Hello, how are you today? Please read this sample clearly for a fair voice benchmark.",BENCHMARK_PRESETS={de:"Die Welt ist voller Geschichten, die darauf warten, erz\xE4hlt zu werden \u2014 von mutigen Helden und stillen Tr\xE4umern.",en:"The old lighthouse stood firm against the crashing waves, its beam sweeping silently across the dark and restless sea.",de2:"Victor jagt zw\xF6lf Boxk\xE4mpfer quer \xFCber den gro\xDFen Sylter Deich. Im Winter ist es kalt und die Tage sind kurz.",en2:"She sells seashells by the seashore. Peter Piper picked a peck of pickled peppers on a perfectly pleasant afternoon.",reset:DEFAULT_BENCHMARK_SAMPLE_TEXT};function benchmarkSampleText(){const el=$("benchmark-sample-text");return el&&el.value.trim()||DEFAULT_BENCHMARK_SAMPLE_TEXT}function initBenchmarkSampleControls(){var _a2,_b2;const sample=$("benchmark-sample-text");if(!sample)return;sample.value=localStorage.getItem(BENCHMARK_SAMPLE_STORAGE_KEY)||DEFAULT_BENCHMARK_SAMPLE_TEXT,sample.addEventListener("input",debounce(()=>{localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value.trim()),status("Benchmark sample sentence saved")},500)),(_a2=$("benchmark-reset-sample-btn"))==null||_a2.addEventListener("click",()=>{sample.value=DEFAULT_BENCHMARK_SAMPLE_TEXT,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),status("Benchmark sample sentence reset")});const presetSel=$("benchmark-preset-select");presetSel&&presetSel.addEventListener("change",()=>{const key=presetSel.value;if(!key||!BENCHMARK_PRESETS[key]){presetSel.value="";return}sample.value=BENCHMARK_PRESETS[key],localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),presetSel.value="",status("Benchmark sample sentence loaded")}),(_b2=$("benchmark-use-preview-btn"))==null||_b2.addEventListener("click",()=>{var _a3;const text=(_a3=$("preview-text-area"))==null?void 0:_a3.value.trim();if(!text){toast("Preview text is empty","error");return}sample.value=text,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,text),status("Benchmark sample sentence copied from TTS preview")})}function _displaySource(v){if(v.origin)return v.origin;if(v.note){const m=v.note.match(/^Rehearser\s*·\s*(.+?)\s*·/);if(m)return m[1].trim()}if(v.tag){const tags=v.tag.split(",").map(t=>t.trim().toLowerCase());if(tags.includes("fish-audio")||tags.includes("fishaudio"))return"fish-audio"}return""}function getSortValue(v,field){var _a2,_b2,_c2,_d2,_e2,_f2;switch(field){case"has_picture":return v.has_picture?1:0;case"flag":return(v.flag||"").toLowerCase();case"gender":return(_a2={F:0,M:1,N:2}[v.gender])!=null?_a2:3;case"id":return v.id.toLowerCase();case"file_type":return voiceFileType(v);case"duration":return v.duration||0;case"dbfs":return(_b2=voiceDbfs(v))!=null?_b2:-999;case"benchmark":case"factor":return-((_c2=voiceFactor(v))!=null?_c2:-999);case"elapsed":return(_d2=voiceBenchmarkElapsed(v))!=null?_d2:999;case"bench_audio":return(_e2=voiceBenchmarkAudioSec(v))!=null?_e2:999;case"wpm":return(_f2=voiceWpm(v))!=null?_f2:-1;case"transcript":return(v.transcript||"").toLowerCase();case"note":return(v.note||"").toLowerCase();case"source":return(_displaySource(v)||"").toLowerCase();case"seed":return v.seed!=null?v.seed:9999999;case"tag":return(v.tag||"").toLowerCase();case"rating":return v.rating||0;case"enabled":return v.enabled===!1?0:1;default:return""}}function setSort(field){_sortDir=_sortField===field?_sortDir*-1:1,_sortField=field,syncSortHeaders(),renderVoiceList()}function toggleSortDir(){_sortDir*=-1,syncSortHeaders(),renderVoiceList()}function syncSortHeaders(){document.querySelectorAll(".vl-header [data-sort], .vl-table-header [data-sort]").forEach(el=>{el.classList.remove("sort-asc","sort-desc"),el.dataset.sort===_sortField&&el.classList.add(_sortDir===1?"sort-asc":"sort-desc")});const sel=document.getElementById("voice-sort-field");sel&&sel.value!==_sortField&&(sel.value=_sortField);const dirBtn=document.getElementById("voice-sort-dir");if(dirBtn){const icon=dirBtn.querySelector(".mdi");icon&&(icon.className=_sortDir===1?"mdi mdi-arrow-up":"mdi mdi-arrow-down"),dirBtn.title=_sortDir===1?"Ascending \u2014 click to reverse":"Descending \u2014 click to reverse"}}document.addEventListener("click",e=>{var _a2,_b2,_c2;if(e.target.closest("#voice-sort-dir")&&toggleSortDir(),e.target.closest("#voice-group-tag-btn")){window._voiceGroupByTag=!window._voiceGroupByTag;try{localStorage.setItem("vl-group-by-tag",window._voiceGroupByTag?"1":"0")}catch{}const btn=document.getElementById("voice-group-tag-btn");btn==null||btn.classList.toggle("active",window._voiceGroupByTag);const icon=btn==null?void 0:btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline"),renderVoiceList()}else if(e.target.closest("#voice-table-view-btn")){window._voiceTableView=!window._voiceTableView,window._voiceTableView||(window._voiceTableEditMode=!1);try{localStorage.setItem("vl-table-view",window._voiceTableView?"1":"0")}catch{}const btn=document.getElementById("voice-table-view-btn");btn==null||btn.classList.toggle("active",window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");if(editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode)),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",window._voiceTableView),(_b2=document.querySelector(".voices-workbench"))==null||_b2.classList.toggle("table-edit-mode",!!window._voiceTableEditMode),window._voiceTableView){document.querySelectorAll(".edit-open").forEach(r=>r.classList.remove("edit-open"));const inspector=document.getElementById("voices-inspector");inspector&&(inspector.innerHTML='<div class="inspector-placeholder"><span><span class="mdi mdi-table"></span></span><p>Table View Mode<br>Click a row to exit table view and edit.</p></div>')}renderVoiceList()}else if(e.target.closest("#voice-table-edit-btn")){window._voiceTableEditMode=!window._voiceTableEditMode;const editBtn=document.getElementById("voice-table-edit-btn");editBtn==null||editBtn.classList.toggle("active",window._voiceTableEditMode),(_c2=document.querySelector(".voices-workbench"))==null||_c2.classList.toggle("table-edit-mode",window._voiceTableEditMode),renderVoiceList()}});try{window._voiceGroupByTag=localStorage.getItem("vl-group-by-tag")==="1"}catch{}try{window._voiceTableView=localStorage.getItem("vl-table-view")==="1"}catch{}document.addEventListener("click",e=>{const th=e.target.closest(".vl-th-sortable");if(th){const field=th.dataset.sort;if(field){const sel=document.getElementById("voice-sort-field");sel&&(sel.value=field),setSort(field)}}}),document.addEventListener("change",e=>{e.target.id==="voice-sort-field"&&setSort(e.target.value)}),document.addEventListener("change",async e=>{if(e.target.classList.contains("vl-inline-edit")){const row=e.target.closest(".vl-row");if(!row)return;const voiceId=row.dataset.id,field=e.target.dataset.field;let value=e.target.type==="checkbox"?e.target.checked:e.target.value;field==="rating"&&(value=parseInt(value)||0);const payload={};payload[field]=value;try{await saveMeta(voiceId,payload);const v=_voices.find(vv=>vv.id===voiceId);v&&(v[field]=value),typeof toast=="function"&&toast("Saved "+field,"success")}catch{typeof toast=="function"&&toast("Failed to save "+field,"error")}}});const FLAG_LANGUAGE_CANDIDATES={GB:["EN"],US:["EN"],AU:["EN"],NZ:["EN"],IE:["EN"],ZA:["EN"],NG:["EN"],KE:["EN"],GH:["EN"],JM:["EN"],TT:["EN"],CA:["EN","FR"],IN:["EN","HI"],SG:["EN","ZH"],PH:["EN","FIL"],MT:["EN","MT"],DE:["DE"],AT:["DE"],CH:["DE","FR","IT"],FR:["FR"],BE:["FR","NL"],LU:["FR","DE"],ES:["ES"],MX:["ES"],AR:["ES"],CO:["ES"],CL:["ES"],PE:["ES"],VE:["ES"],UY:["ES"],EC:["ES"],BO:["ES"],CR:["ES"],CU:["ES"],DO:["ES"],PT:["PT"],BR:["PT"],IT:["IT"],NL:["NL"],PL:["PL"],SE:["SV"],DK:["DA"],NO:["NO"],FI:["FI"],IS:["IS"],GR:["EL"],CY:["EL","TR"],CZ:["CS"],SK:["SK"],HU:["HU"],RO:["RO"],BG:["BG"],HR:["HR"],SI:["SL"],RS:["SR"],BA:["BS"],ME:["SR"],MK:["MK"],AL:["SQ"],EE:["ET"],LV:["LV"],LT:["LT"],UA:["UK"],RU:["RU"],BY:["RU"],MD:["RO"],TR:["TR"],CN:["ZH"],TW:["ZH"],HK:["ZH"],MO:["ZH"],JP:["JA"],KR:["KO"],VN:["VI"],TH:["TH"],ID:["ID"],MY:["MS"],PK:["UR"],BD:["BN"],LK:["SI"],NP:["NE"],SA:["AR"],EG:["AR"],AE:["AR"],MA:["AR"],QA:["AR"],KW:["AR"],OM:["AR"],JO:["AR"],LB:["AR"],IQ:["AR"],IR:["FA"],IL:["HE"]},FLAG_LANGUAGE=Object.fromEntries(Object.entries(FLAG_LANGUAGE_CANDIDATES).map(([cc,langs])=>[cc,langs[0]])),LANGUAGE_LABELS={EN:"English",DE:"German",FR:"French",ES:"Spanish",PT:"Portuguese",IT:"Italian",NL:"Dutch",PL:"Polish",SV:"Swedish",DA:"Danish",NO:"Norwegian",FI:"Finnish",IS:"Icelandic",EL:"Greek",MT:"Maltese",CS:"Czech",SK:"Slovak",HU:"Hungarian",RO:"Romanian",BG:"Bulgarian",HR:"Croatian",SL:"Slovenian",SR:"Serbian",BS:"Bosnian",MK:"Macedonian",SQ:"Albanian",ET:"Estonian",LV:"Latvian",LT:"Lithuanian",UK:"Ukrainian",RU:"Russian",ZH:"Chinese",JA:"Japanese",KO:"Korean",VI:"Vietnamese",TH:"Thai",ID:"Indonesian",MS:"Malay",FIL:"Filipino",HI:"Hindi",UR:"Urdu",BN:"Bengali",SI:"Sinhala",NE:"Nepali",AR:"Arabic",FA:"Persian",HE:"Hebrew",TR:"Turkish"},SEX_FILTER_LABELS={F:"\u2640 Female",M:"\u2642 Male",N:"\u26A5 Diverse / neutral"};function voiceLangFromName(v){return(v.lang||String(v.id||"").split("_")[0]||"").toUpperCase()}function libraryVoiceLang(v){const fromName=voiceLangFromName(v),candidates=FLAG_LANGUAGE_CANDIDATES[String(v.flag||"").toUpperCase()];return candidates!=null&&candidates.length?candidates.includes(fromName)?fromName:candidates[0]:fromName}function libraryLanguageLabel(code){return LANGUAGE_LABELS[code]||code}function populateLibraryFilters(){const langSel=$("library-filter-lang"),sexSel=$("library-filter-sex"),typeSel=$("library-filter-type"),tagSel=$("library-filter-tag"),groupSel=$("library-filter-group");if(!langSel||!sexSel||!typeSel)return;const langSet=new Set,sexSet=new Set,typeSet=new Set,tagSet=new Set,groupSet=new Set;(_voices||[]).forEach(v=>{const lang=libraryVoiceLang(v);lang&&langSet.add(lang),v.gender&&sexSet.add(v.gender);const type=voiceFileType(v);type&&typeSet.add(type),String(v.tag||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>tagSet.add(t));const g=(v.group||"").trim();g&&groupSet.add(g)});const langs=[...langSet].sort((a,b)=>libraryLanguageLabel(a).localeCompare(libraryLanguageLabel(b))),sexOrder=["F","M","N"],sexes=[...sexSet].sort((a,b)=>(sexOrder.indexOf(a)<0?99:sexOrder.indexOf(a))-(sexOrder.indexOf(b)<0?99:sexOrder.indexOf(b))),types=[...typeSet].sort(),tags=[...tagSet].sort((a,b)=>a.localeCompare(b)),groups=[...groupSet].sort((a,b)=>a.localeCompare(b)),sig=JSON.stringify([langs,sexes,types,tags,groups]);if(sig===_libraryFilterOptionsSig)return;_libraryFilterOptionsSig=sig;const keep={lang:langSel.value,sex:sexSel.value,type:typeSel.value,tag:tagSel==null?void 0:tagSel.value,group:groupSel==null?void 0:groupSel.value};langSel.innerHTML='<option value="">All languages</option>'+langs.map(x=>`<option value="${escHtml(x)}">${escHtml(libraryLanguageLabel(x))} (${escHtml(x)})</option>`).join(""),sexSel.innerHTML='<option value="">All</option>'+sexes.map(x=>`<option value="${escHtml(x)}">${escHtml(SEX_FILTER_LABELS[x]||x)}</option>`).join(""),typeSel.innerHTML='<option value="">All</option>'+types.map(x=>`<option value="${escHtml(x)}">${escHtml(x.toUpperCase())}</option>`).join(""),tagSel&&(tagSel.innerHTML='<option value="">All tags</option>'+tags.map(x=>`<option value="${escHtml(x)}">${escHtml(x)}</option>`).join("")),groupSel&&(groupSel.innerHTML='<option value="">All groups</option>'+groups.map(x=>`<option value="${escHtml(x)}">${escHtml(x)}</option>`).join("")),langSel.value=langs.includes(keep.lang)?keep.lang:"",sexSel.value=sexes.includes(keep.sex)?keep.sex:"",typeSel.value=types.includes(keep.type)?keep.type:"",tagSel&&(tagSel.value=tags.includes(keep.tag)?keep.tag:""),groupSel&&(groupSel.value=groups.includes(keep.group)?keep.group:"")}function readLibraryFilters(){var _a2,_b2,_c2,_d2,_e2;_libraryFilters.text=(((_a2=$("library-filter-text"))==null?void 0:_a2.value)||"").trim().toLowerCase(),_libraryFilters.lang=((_b2=$("library-filter-lang"))==null?void 0:_b2.value)||"",_libraryFilters.sex=((_c2=$("library-filter-sex"))==null?void 0:_c2.value)||"",_libraryFilters.type=((_d2=$("library-filter-type"))==null?void 0:_d2.value)||"",_libraryFilters.rating=((_e2=$("library-filter-rating"))==null?void 0:_e2.value)||""}function libraryFilterMatch(v){const f=_libraryFilters;if(f.lang&&libraryVoiceLang(v)!==f.lang||f.sex&&(v.gender||"")!==f.sex||f.type&&voiceFileType(v)!==f.type)return!1;if(f.rating){const r=Number(v.rating||0),wanted=Number(f.rating);if(wanted===0&&r!==0||wanted===1&&r<1||wanted>1&&r<wanted)return!1}return!(f.text&&![v.id,v.transcript,v.note,v.file_type,v.flag,v.gender,v.group,v.origin].map(x=>String(x||"").toLowerCase()).join(" ").includes(f.text))}function clearLibraryFilters(){["library-filter-text","library-filter-lang","library-filter-sex","library-filter-type","library-filter-rating"].forEach(id=>{const el=$(id);el&&(el.value="")}),readLibraryFilters(),renderVoiceList()}function libraryTtsBackend(){var _a2;return((_a2=$("library-tts-backend-select"))==null?void 0:_a2.value)||"voice_clone"}function needsDuration(v){return v.duration==null||Number.isNaN(Number(v.duration))}function voiceFileType(v){if(v.file_type)return String(v.file_type).replace(/^\./,"").toLowerCase();const match=String(v.path||v.filename||"").match(/\.([A-Za-z0-9]+)(?:$|[?#])/);return match?match[1].toLowerCase():"wav"}function voiceDbfs(v){var _a2;const value=v.loudness&&((_a2=v.loudness.dbfs)!=null?_a2:v.loudness.after_dbfs);return value==null||Number.isNaN(Number(value))?null:Number(value)}function fmtDbfs(v){const db=voiceDbfs(v);return db==null?"-":db.toFixed(1)}function voiceBenchmark(v){return v.benchmark&&typeof v.benchmark=="object"&&Object.keys(v.benchmark).length>0?v.benchmark:null}function voiceBenchmarkElapsed(v){const b=voiceBenchmark(v),value=b&&b.elapsed_sec;return value==null||Number.isNaN(Number(value))?null:Number(value)}function voiceFactor(v){const b=voiceBenchmark(v);return b&&b.ok&&b.speed!=null?Number(b.speed):null}function fmtFactor(v){const f=voiceFactor(v);return f!=null?f.toFixed(2)+"x":"-"}function fmtElapsed(v){const e=voiceBenchmarkElapsed(v),b=voiceBenchmark(v);return!b||!b.ok?b&&!b.ok?"ERR":"-":e!=null?e.toFixed(1)+"s":"-"}function fmtBenchmark(v){const elapsed=fmtElapsed(v),factor=fmtFactor(v);return elapsed==="-"&&factor==="-"?"-":[elapsed,factor].filter(x=>x!=="-").join(" \xB7 ")}function benchmarkClass(v){const b=voiceBenchmark(v);if(!b)return"";if(!b.ok||b.clipped||b.realtime_ok===!1)return"bench-bad";const elapsed=voiceBenchmarkElapsed(v);return elapsed!=null&&elapsed<=4?"bench-ok":"bench-warn"}function voiceBenchmarkAudioSec(v){const b=voiceBenchmark(v);return b&&b.ok&&b.audio_sec!=null?Number(b.audio_sec):null}function fmtBenchmarkAudio(v){const sec=voiceBenchmarkAudioSec(v);return sec!=null?sec.toFixed(1)+"s":"-"}function voiceWpm(v){const b=voiceBenchmark(v);if(!b||!b.ok||!b.audio_sec||!b.text)return null;const words=b.text.trim().split(/\s+/).length;return Math.round(words/(b.audio_sec/60))}function fmtWpm(v){const wpm=voiceWpm(v);return wpm!=null?wpm+" wpm":"-"}function voiceFileUrl(v){const bust=v._audioVersion||v.updated_at||v.benchmarked_at||""||Date.now();return`/api/voice-file?path=${encodeURIComponent(v.path)}&v=${encodeURIComponent(bust)}`}function markVoiceAudioChanged(v){v._audioVersion=Date.now()}function benchmarkTitle(v){const b=voiceBenchmark(v);if(!b)return"Not benchmarked yet";const parts=[];return b.ok?(parts.push(`total ${Number(b.elapsed_sec||0).toFixed(2)}s`),b.ttfa_ms!=null&&parts.push(`TTFA ${Number(b.ttfa_ms).toFixed(0)}ms`),b.audio_sec!=null&&parts.push(`audio ${Number(b.audio_sec).toFixed(2)}s`),b.rtf!=null&&parts.push(`RTF ${Number(b.rtf).toFixed(2)}`),b.speed!=null&&parts.push(`speed ${Number(b.speed).toFixed(2)}x real-time`),b.clipped&&parts.push("output clipped")):(parts.push("benchmark failed"),b.error&&parts.push(b.error)),Array.isArray(b.advice)&&b.advice.length&&parts.push(b.advice.join(" | ")),b.benchmarked_at&&parts.push(`saved ${b.benchmarked_at}`),parts.join(" \xB7 ")}async function clientVoiceLoudness(v){if(!v.path)throw new Error("No audio path");const resp=await fetch(voiceFileUrl(v),{cache:"no-store"});if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const audioData=await resp.arrayBuffer(),buffer=await new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(audioData.slice(0));let sum=0,peak=0,count=0;for(let ch=0;ch<buffer.numberOfChannels;ch++){const data=buffer.getChannelData(ch);count+=data.length;for(let i=0;i<data.length;i++){const sample=data[i];sum+=sample*sample,peak=Math.max(peak,Math.abs(sample))}}const rms=Math.sqrt(sum/Math.max(1,count)),dbfs=rms>0?20*Math.log10(rms):null,peakDbfs=peak>0?20*Math.log10(peak):null;return{dbfs:dbfs==null?null:Number(dbfs.toFixed(2)),peak_dbfs:peakDbfs==null?null:Number(peakDbfs.toFixed(2))}}async function clientCalculateVoiceDb(){const voices=_bulkSelected&&_bulkSelected.size>0?(_voices||[]).filter(v=>_bulkSelected.has(v.id)):visibleLibraryVoices(),errors=[];let calculated=0;const stats={startedAt:Date.now(),ok:0,slow:0,errors:0,middleLabel:"Skipped"};setBenchmarkProgress(0,voices.length,"Preparing dB scan...",stats);for(const v of voices){setBenchmarkProgress(calculated+errors.length,voices.length,`Calculating dB: ${v.id}`,stats);try{v.loudness=await clientVoiceLoudness(v),await saveMeta(v.id,{loudness:v.loudness}).catch(()=>{}),calculated++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs(v)} dBFS`,status(`Calculated dB: ${calculated} / ${voices.length}`)}catch(e){errors.push({voice_id:v.id,detail:e.message}),stats.errors++,stats.last=`${v.id}: ${e.message}`}setBenchmarkProgress(calculated+errors.length,voices.length,`Calculating dB: ${v.id}`,stats),await new Promise(resolve=>setTimeout(resolve,0))}return setBenchmarkProgress(voices.length,voices.length,"dB scan complete",stats),{calculated,errors,voices:voices.map(v=>({voice_id:v.id,loudness:v.loudness}))}}async function hydrateVoiceDuration(v,el){if(!(!v.path||!needsDuration(v)||v._durationLoading)){v._durationLoading=!0;try{const audio=new Audio;audio.preload="metadata",audio.src=voiceFileUrl(v),await new Promise((resolve,reject)=>{audio.onloadedmetadata=resolve,audio.onerror=()=>reject(new Error("Could not read duration"))}),Number.isFinite(audio.duration)&&audio.duration>0&&(v.duration=audio.duration,el&&document.body.contains(el)&&(el.textContent=fmtDuration(v.duration),el.title=String(v.duration.toFixed(2)))),audio.removeAttribute("src"),audio.load()}catch(e){el&&document.body.contains(el)&&(el.title=e.message)}finally{v._durationLoading=!1}}}document.querySelectorAll(".vl-header [data-sort]").forEach(el=>el.addEventListener("click",()=>setSort(el.dataset.sort)));function dominantLanguages(limit=3){const counts=new Map;return(_voices||[]).forEach(v=>{const lang=(v.lang||String(v.id||"").split("_")[0]||"?").toUpperCase();counts.set(lang,(counts.get(lang)||0)+1)}),[...counts.entries()].sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0])).slice(0,limit).map(([lang,count])=>`${lang} ${count}`).join(" \xB7 ")||"-"}function updateLibraryInsights(state="ready"){const el=$("library-insights");if(!el)return;if(state==="loading"){el.innerHTML=[["\u2026","Loading"],["\u2026","Active"],["\u2026","Languages"],["\u2026","Benchmarks"],["\u2026","Quality"],["\u2026","Actions"]].map(([value,label])=>`<div class="insight"><strong>${value}</strong><span>${label}</span></div>`).join("");return}if(state==="error"){el.innerHTML='<div class="insight"><strong>Failed</strong><span>Library load</span></div>';return}const total=_voices.length,active=_voices.filter(v=>v.enabled!==!1).length,hidden=total-active,bench=_voices.map(voiceBenchmark).filter(Boolean),slow=bench.filter(b=>b&&b.ok&&b.realtime_ok===!1).length,dbValues=_voices.map(voiceDbfs).filter(v=>v!=null),avgDb=dbValues.length?(dbValues.reduce((a,b)=>a+b,0)/dbValues.length).toFixed(1):"-",missingRef=_voices.filter(v=>!v.transcript).length,restart=_voices.filter(v=>v.needs_tts_restart).length,tiles=[{value:`${_voices.filter(v=>$("show-disabled-cb").checked||v.enabled!==!1).length}/${total}`,label:"Visible"},{value:`${active} on`,label:hidden?`${hidden} hidden`:"Active"},{value:dominantLanguages(),label:"Languages"},{value:bench.length?`${bench.length} done`:"-",label:slow?`${slow} slow`:"Benchmarks",filter:slow?"slow":"",title:slow?describeIssueVoices("slow"):"No slow voices"},{value:avgDb==="-"?"-":`${avgDb} dB`,label:missingRef?`${missingRef} no text`:"Avg loudness",filter:missingRef?"no_text":"",title:missingRef?describeIssueVoices("no_text"):"All visible voices have reference text"},{value:restart||"-",label:restart?"Need restart":"Restart flags",filter:restart?"restart":"",title:restart?describeIssueVoices("restart"):"No voices need restart"}];el.innerHTML=tiles.map(item=>{const filter=item.filter?` data-filter="${escHtml(item.filter)}" role="button" tabindex="0"`:"",activeCls=item.filter&&item.filter===_libraryIssueFilter?" active":"",title=item.title?` title="${escHtml(item.title)}"`:"";return`<div class="insight${activeCls}"${filter}${title}><strong>${escHtml(item.value)}</strong><span>${escHtml(item.label)}</span></div>`}).join(""),el.querySelectorAll("[data-filter]").forEach(tile=>{const activate=()=>setLibraryIssueFilter(tile.dataset.filter||"");tile.addEventListener("click",activate),tile.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),activate())})})}function shouldRenderVoiceLibrary(){const section=$("s-voices");return!section||section.classList.contains("is-active")}async function loadVoiceLibrary(options={}){const forceRefresh=!!(options&&options.refresh);return _libraryLoadPromise?_libraryLoadPromise.then(()=>{shouldRenderVoiceLibrary()&&_voices.length&&renderVoiceList()}):(_libraryLoadPromise=(async()=>{setBusyButton("refresh-voices-btn",!0);const list=$("voice-list"),renderVisibleList=shouldRenderVoiceLibrary(),cached=_voices.length===0?_vlCacheRead():null;cached&&Array.isArray(cached)&&cached.length&&(_voices=cached,window._voices=_voices,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices),renderVisibleList&&renderVoiceList(),updatePreviewVoiceMatchPanel(),status(`Loaded ${_voices.length} voices`));const silent=_voices.length>0;list&&!silent&&renderVisibleList&&(list.innerHTML=loadingMarkup("Loading voice library","Scanning voices, reference text, metadata, ratings, and benchmark results.",8)),!silent&&renderVisibleList&&($("voice-count").textContent="Loading voices\u2026",updateLibraryInsights("loading"),status("Loading voice library\u2026"));try{const r=await fetch("/api/voices"+(forceRefresh?"?refresh=1":""));if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const fresh=await r.json();_vlCacheWrite(fresh),_voices=fresh,window._voices=_voices,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices),shouldRenderVoiceLibrary()&&renderVoiceList(),updatePreviewVoiceMatchPanel(),typeof renderPerfHistory=="function"&&renderPerfHistory(),status(`Loaded ${_voices.length} voices`)}catch(e){if(!silent&&renderVisibleList&&(list&&(list.innerHTML='<div style="color:var(--red);padding:8px;font-size:13px">Failed to load voices: '+(e.message||String(e))+"</div>"),$("voice-count").textContent="Load failed",updateLibraryInsights("error")),status("Voice library load failed: "+e.message),!silent)throw e}finally{setBusyButton("refresh-voices-btn",!1),_libraryLoadPromise=null}})(),_libraryLoadPromise)}$("refresh-voices-btn").addEventListener("click",()=>{_vlCacheClear(),loadVoiceLibrary({refresh:!0})}),$("sync-voice-folders-btn").addEventListener("click",async()=>{$("sync-voice-folders-btn").disabled=!0,status("Syncing active_voices and hidden_voices\u2026");try{const r=await fetch("/api/voices/sync-folders",{method:"POST"});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();await loadVoiceLibrary();const conflicts=d.conflicts&&d.conflicts.length?`, ${d.conflicts.length} conflicts`:"";toast(`Synced: ${d.moved.active} active, ${d.moved.hidden} hidden${conflicts}`,d.conflicts&&d.conflicts.length?"error":"success"),status("Synced folders. Restart Qwen3-TTS after changing active voices.")}catch(e){toast("Sync failed: "+e.message,"error"),status("Folder sync failed")}finally{$("sync-voice-folders-btn").disabled=!1}});function visibleLibraryVoices(){const showDisabled=$("show-disabled-cb").checked;return _voices.filter(v=>showDisabled||v.enabled!==!1)}function libraryIssueMatch(v,filter=_libraryIssueFilter){const b=voiceBenchmark(v);return filter==="slow"?!!(b&&b.ok&&b.realtime_ok===!1):filter==="no_text"?!String(v.transcript||"").trim():filter==="restart"?!!v.needs_tts_restart:!0}function libraryIssueLabel(filter=_libraryIssueFilter){return{slow:"slow benchmark voices",no_text:"voices without reference text",restart:"voices needing TTS restart"}[filter]||"all voices"}function libraryIssueVoices(filter=_libraryIssueFilter){return visibleLibraryVoices().filter(v=>libraryIssueMatch(v,filter))}function describeIssueVoices(filter=_libraryIssueFilter,limit=12){const voices=libraryIssueVoices(filter).map(v=>v.id);if(!voices.length)return"No matching voices";const extra=voices.length>limit?`, +${voices.length-limit} more`:"";return voices.slice(0,limit).join(", ")+extra}function setLibraryIssueFilter(filter=""){_libraryIssueFilter=_libraryIssueFilter===filter?"":filter,renderVoiceList(),status(_libraryIssueFilter?`${libraryIssueLabel()}: ${describeIssueVoices()}`:"Showing all visible voices")}function libraryTargetDb(){var _a2;const input=$("library-target-db"),raw=Number((_a2=input==null?void 0:input.value)!=null?_a2:-20),value=Number.isFinite(raw)?Math.min(-1,Math.max(-60,raw)):-20;return input&&(input.value=String(value)),value}$("calculate-db-btn").addEventListener("click",async()=>{$("calculate-db-btn").disabled=!0;const _calcTarget=_bulkSelected&&_bulkSelected.size>0?`${_bulkSelected.size} selected`:"visible";status(`Calculating voice loudness (${_calcTarget})\u2026`);try{const d=await clientCalculateVoiceDb();renderVoiceList();const extra=d.errors&&d.errors.length?`, ${d.errors.length} errors`:"";toast(`Calculated dB for ${d.calculated} voices${extra}`,d.errors&&d.errors.length?"error":"success"),status("Calculated voice loudness. Use Normalize volume for visible WAV voices.")}catch(e){toast("Calculate dB failed: "+e.message,"error"),status("dB calculation failed")}finally{$("calculate-db-btn").disabled=!1}}),$("normalize-volume-btn").addEventListener("click",async()=>{var _a2;const target=libraryTargetDb(),visible=visibleLibraryVoices(),voices=visible.filter(v=>voiceFileType(v)==="wav"),skipped=visible.length-voices.length;if(!voices.length){toast("No visible WAV voices to normalize","error");return}if(!confirm(`Normalize ${voices.length} visible WAV voices to ${target} dBFS?${skipped?` ${skipped} non-WAV voices will be skipped.`:""}`))return;$("normalize-volume-btn").disabled=!0,$("calculate-db-btn").disabled=!0;const stats={startedAt:Date.now(),ok:0,slow:skipped,errors:0,middleLabel:"Skipped"},errors=[];let normalized=0;setBenchmarkProgress(0,voices.length,`Normalizing to ${target} dBFS...`,stats),status(`Normalizing ${voices.length} voices to ${target} dBFS...`);try{for(const v of voices){setBenchmarkProgress(normalized+errors.length,voices.length,`Normalizing: ${v.id}`,stats);try{const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:target})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a2=d.duration)!=null?_a2:v.duration,v.file_type=d.file_type||v.file_type,v.path=d.path||v.path,v.needs_tts_restart=!0,markVoiceAudioChanged(v),normalized++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs(v)} dBFS`}catch(e){errors.push({voice_id:v.id,detail:e.message}),stats.errors++,stats.last=`${v.id}: ${e.message}`}setBenchmarkProgress(normalized+errors.length,voices.length,`Normalizing: ${v.id}`,stats),status(`Normalized ${normalized} / ${voices.length}`),await new Promise(resolve=>setTimeout(resolve,0))}setBenchmarkProgress(voices.length,voices.length,"Volume normalization complete",stats),renderVoiceList(),updateLibraryInsights();const extra=`${skipped?`, ${skipped} skipped`:""}${errors.length?`, ${errors.length} errors`:""}`;toast(`Normalized ${normalized} voices${extra}`,errors.length?"error":"success"),status("Volume normalized. Restart TTS before rebenchmarking these voices.")}catch(e){toast("Normalize volume failed: "+e.message,"error"),status("Normalize volume failed")}finally{$("normalize-volume-btn").disabled=!1,$("calculate-db-btn").disabled=!1}});function fmtClock(ms){if(!Number.isFinite(ms)||ms<0)return"-";const total=Math.round(ms/1e3),m=Math.floor(total/60),s=total%60;return`${m}:${String(s).padStart(2,"0")}`}function setBenchmarkProgress(done,total,label="",stats={}){const panel=$("benchmark-progress"),track=panel.querySelector(".benchmark-progress-track"),pct=total?Math.round(done/total*100):0;panel.hidden=!1,$("benchmark-progress-label").textContent=label||(done>=total?"Benchmark complete":"Benchmarking voices..."),$("benchmark-progress-count").textContent=`${done} / ${total}`,$("benchmark-progress-bar").style.width=pct+"%",track.setAttribute("aria-valuenow",String(pct));const live=$("benchmark-live-stats");if(live){const elapsed=stats.startedAt?Date.now()-stats.startedAt:0,avg=done>0?elapsed/done:0,eta=done>0&&total>done?avg*(total-done):0;live.innerHTML=[`Elapsed ${fmtClock(elapsed)}`,`Avg ${done?(avg/1e3).toFixed(1)+"s":"-"}`,`ETA ${done&&total>done?fmtClock(eta):"-"}`,`OK ${stats.ok||0}`,`${stats.middleLabel||"Slow"} ${stats.slow||0}`,`${stats.errorLabel||"Errors"} ${stats.errors||0}`].map(x=>`<span>${escHtml(x)}</span>`).join("")}const last=$("benchmark-live-last");last&&stats.last&&(last.textContent=stats.last)}function hideBenchmarkProgress(){$("benchmark-progress").hidden=!0,$("benchmark-progress-bar").style.width="0%",$("benchmark-live-last")&&($("benchmark-live-last").textContent="")}async function clearTtsRestartFlags(){const r=await fetch("/api/tts/restart-flags/clear",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return _voices.forEach(voice=>{voice.needs_tts_restart=!1}),document.querySelectorAll(".vl-row.edit-open").forEach(row=>row.classList.remove("opt-restart-needed")),updateLibraryInsights(),d}async function runVoiceBenchmark(voiceId="",opts={}){var _a2;const text=(_a2=opts.text)!=null?_a2:benchmarkSampleText();if(!text)return toast("Enter a benchmark sample sentence","error"),null;const payload={active_only:!0,text};voiceId&&(payload.voice_id=voiceId);const r=await fetch("/api/voices/benchmark",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return r.json()}async function runVoiceBenchmarkBatch(){const voices=benchmarkTargetVoices(),text=benchmarkSampleText();if(!text)return toast("Enter a benchmark sample sentence","error"),null;if(!voices.length)return toast("No voices to benchmark","error"),null;const total=voices.length,aggregate={benchmarked:0,errors:[],voices:[],text,active_only:!0},stats={startedAt:Date.now(),ok:0,slow:0,errors:0,last:""};voices.forEach(v=>{const row=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);row&&(row.classList.remove("benchmarking-active","benchmarking-done"),row.classList.add("benchmarking-pending"))}),setBenchmarkProgress(0,total,"Starting benchmark...",stats);for(let i=0;i<voices.length;i++){const voice=voices[i],row=document.querySelector(`.vl-row[data-id="${CSS.escape(voice.id)}"]`);row&&(row.classList.remove("benchmarking-pending"),row.classList.add("benchmarking-active"),row.scrollIntoView({behavior:"smooth",block:"center"})),setBenchmarkProgress(i,total,`Benchmarking ${voice.id}`,stats),status(`Benchmarking ${voice.id} (${i+1} / ${total})...`);try{const d=await runVoiceBenchmark(voice.id,{text});if(d){aggregate.benchmarked+=Number(d.benchmarked||0),aggregate.errors.push(...d.errors||[]),aggregate.voices.push(...d.voices||[]),mergeBenchmarkResults(d);const hit=(d.voices||[]).find(x=>x.voice_id===voice.id),b=hit&&hit.benchmark;if(b&&b.ok){if(stats.ok++,b.realtime_ok===!1&&stats.slow++,stats.last=`${voice.id}: ${Number(b.elapsed_sec||0).toFixed(1)}s${b.speed!=null?` \xB7 ${Number(b.speed).toFixed(2)}x`:""}${b.realtime_ok===!1?" \xB7 slow":""}`,row){const bc=benchmarkClass(voice),btitle=benchmarkTitle(voice),durCell=row.querySelector(".vl-tbl-dur"),factorCell=row.querySelector(".vl-tbl-factor"),timeCell=row.querySelector(".vl-tbl-time"),wpmCell=row.querySelector(".vl-tbl-wpm");if(durCell&&(durCell.textContent=fmtBenchmarkAudio(voice),durCell.title=`${fmtBenchmarkAudio(voice)} \u2014 length of synthesised benchmark audio`),factorCell&&(factorCell.textContent=fmtFactor(voice),factorCell.className=`vl-tbl-factor ${bc}`,factorCell.title=btitle),timeCell&&(timeCell.textContent=fmtElapsed(voice),timeCell.className=`vl-tbl-time ${bc}`,timeCell.title=btitle),wpmCell){const wpm=voiceWpm(voice);wpmCell.textContent=fmtWpm(voice),wpmCell.title=wpm!=null?`${wpm} wpm \u2014 130\u2013180 wpm is natural for long listening`:""}}}else stats.errors++,stats.last=`${voice.id}: failed${b&&b.error?" \xB7 "+b.error:""}`}}catch(e){aggregate.errors.push({voice_id:voice.id,detail:e.message}),stats.errors++,stats.last=`${voice.id}: failed \xB7 ${e.message}`}row&&(row.classList.remove("benchmarking-active"),row.classList.add("benchmarking-done")),setBenchmarkProgress(i+1,total,`Finished ${voice.id}`,stats)}return voices.forEach(v=>{const row=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);row&&row.classList.remove("benchmarking-pending")}),setBenchmarkProgress(total,total,"Benchmark complete",stats),aggregate}function mergeBenchmarkResults(d){const byId=new Map((d.voices||[]).map(x=>[x.voice_id,x]));_voices.forEach(v=>{const hit=byId.get(v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark)})}window.loadVoiceLibrary=loadVoiceLibrary,window.mergeBenchmarkResults=mergeBenchmarkResults;function activeBenchmarkVoices(){return(_voices||[]).filter(v=>v.enabled!==!1)}function benchmarkTargetVoices(){return typeof _bulkSelected!="undefined"&&_bulkSelected.size>0?(_voices||[]).filter(v=>_bulkSelected.has(v.id)):activeBenchmarkVoices()}function showBenchmarkConfirm(){const voices=benchmarkTargetVoices(),onlySelected=typeof _bulkSelected!="undefined"&&_bulkSelected.size>0;if(!benchmarkSampleText()){toast("Enter a benchmark sample sentence","error");return}if(!voices.length){toast("No voices to benchmark","error");return}const staleCount=voices.filter(v=>v.needs_tts_restart).length;$("benchmark-confirm-title").textContent=`Benchmark ${voices.length} ${onlySelected?"selected":"active"} voice${voices.length===1?"":"s"}?`,$("benchmark-confirm-text").textContent="This sends the sample sentence to each active voice and can keep the GPU busy for a while. Progress updates after every voice."+(staleCount?` ${staleCount} edited voice${staleCount===1?"":"s"} should be restarted first, otherwise cached old voices may be benchmarked.`:""),$("benchmark-confirm").hidden=!1,$("benchmark-confirm-start").focus()}function hideBenchmarkConfirm(){const panel=$("benchmark-confirm");panel&&(panel.hidden=!0)}$("benchmark-voices-btn").addEventListener("click",showBenchmarkConfirm),(_z=$("benchmark-confirm-cancel"))==null||_z.addEventListener("click",hideBenchmarkConfirm),(_A=$("benchmark-confirm-start"))==null||_A.addEventListener("click",async()=>{hideBenchmarkConfirm(),$("benchmark-voices-btn").disabled=!0,$("benchmark-confirm-start").disabled=!0,status("Benchmarking active voices...");try{const d=await runVoiceBenchmarkBatch();if(!d)return;mergeBenchmarkResults(d),await loadVoiceLibrary();const slow=(d.voices||[]).filter(x=>x.benchmark&&x.benchmark.realtime_ok===!1).length,extra=d.errors&&d.errors.length?`, ${d.errors.length} errors`:"";toast(`Benchmarked ${d.benchmarked} voices${slow?`, ${slow} slow`:""}${extra}`,d.errors&&d.errors.length?"error":"success"),status("Benchmark saved with TTFA, total time, RTF, and speed.")}catch(e){toast("Benchmark failed: "+e.message,"error"),status("Benchmark failed")}finally{$("benchmark-voices-btn").disabled=!1,$("benchmark-confirm-start").disabled=!1}}),$("copy-active-voices-btn").addEventListener("click",async()=>{const useSelected=_bulkSelected&&_bulkSelected.size>0,ids=useSelected?[..._bulkSelected]:activeVoiceIds(),label=useSelected?`${ids.length} selected`:`${ids.length} active`;if(!ids.length){toast("No voices to copy","error");return}await copyText(ids.join(", ")),toast("Copied "+label+" voices","success"),status("Copied "+label+" voices to clipboard")});async function _verifyVoicesRoundtrip(ids,opts){opts=opts||{};const results=[],queue=ids.slice(),worker=async()=>{for(;queue.length;){const id=queue.shift(),v=(window._voices||[]).find(x=>x.id===id),text=v&&v.transcript&&v.transcript.trim()||getLibAddSampleText(v&&v.lang||"EN"),backend=typeof _ttsBackendForVoice=="function"?_ttsBackendForVoice(id,libraryTtsBackend()):libraryTtsBackend();try{const rt=await _voiceRoundtripCheck(id,text,backend);results.push({id,text,transcript:rt.transcript,score:rt.score,ok:rt.score>=.5})}catch(e){results.push({id,text,error:e.message||String(e),ok:!1})}opts.onProgress&&opts.onProgress(results.length,ids.length,id)}};return await Promise.all(Array.from({length:Math.min(2,ids.length)},worker)),results}(_B=$("verify-voices-stt-btn"))==null||_B.addEventListener("click",async()=>{const ids=_bulkSelected&&_bulkSelected.size>0?[..._bulkSelected]:activeVoiceIds();if(!ids.length){toast("No voices to verify","error");return}if(!confirm(`Verify ${ids.length} voice(s) by transcribing a synthesized line back with Whisper and comparing it to the original text? This makes one extra synth + STT call per voice.`))return;const btn=$("verify-voices-stt-btn");btn&&(btn.disabled=!0);const ov=document.createElement("div");ov.className="audiobook-overlay",ov.id="verify-voices-overlay",ov.innerHTML=`<div class="audiobook-box"><div class="audiobook-title"><span class="mdi mdi-check-decagram-outline"></span> Verifying voices</div>
<div class="audiobook-msg" id="vv-msg">0 / ${ids.length}</div>
<div class="reader-synth-track"><div class="reader-synth-fill" id="vv-fill"></div></div>
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="vv-cancel">Cancel</button></div></div>`,document.body.appendChild(ov);let cancelled=!1;ov.querySelector("#vv-cancel").addEventListener("click",()=>{cancelled=!0});const fill=ov.querySelector("#vv-fill"),msg=ov.querySelector("#vv-msg"),results=await _verifyVoicesRoundtrip(ids,{onProgress:(done,total,id)=>{if(fill&&(fill.style.width=done/total*100+"%"),msg&&(msg.textContent=`${done} / ${total} \xB7 ${id}`),cancelled)throw new Error("cancelled")}}).catch(()=>[]);ov.remove(),btn&&(btn.disabled=!1);const failed=results.filter(r=>!r.ok),resOv=document.createElement("div");resOv.className="audiobook-overlay",resOv.innerHTML=`<div class="audiobook-box" style="max-width:640px;max-height:80vh;overflow:auto;">
<div class="audiobook-title"><span class="mdi mdi-check-decagram-outline"></span> Voice verification results</div>
<p class="card-subtitle" style="margin:8px 0 12px;">${results.length-failed.length} / ${results.length} passed.${failed.length?" Failed voices likely need a redesign.":""}</p>
<div style="display:flex;flex-direction:column;gap:6px;">
${failed.map(r=>`<div style="border:1px solid var(--red);border-radius:6px;padding:8px;font-size:13px;">
<b>${escHtml(r.id)}</b> \u2014 ${r.error?escHtml(r.error):"score "+r.score.toFixed(2)}
${r.transcript!==void 0?`<br><span style="color:var(--text-2)">Expected: ${escHtml(r.text.slice(0,120))}</span><br><span style="color:var(--red)">Heard: ${escHtml((r.transcript||"(nothing)").slice(0,120))}</span>`:""}
</div>`).join("")||'<div style="color:var(--green)">All voices passed.</div>'}
</div>
<div style="display:flex; justify-content:flex-end; margin-top:14px;">
<button type="button" class="btn-primary btn-sm" id="vv-close">Close</button>
</div>
</div>`,document.body.appendChild(resOv),resOv.querySelector("#vv-close").addEventListener("click",()=>resOv.remove()),resOv.addEventListener("click",e=>{e.target===resOv&&resOv.remove()}),toast(`Verified ${results.length} voice(s) \u2014 ${failed.length} failed`,failed.length?"error":"success")}),(_C=$("precompute-embeddings-btn"))==null||_C.addEventListener("click",async()=>{const backend=libraryTtsBackend(),_useSelected=_bulkSelected&&_bulkSelected.size>0,ids=_useSelected?[..._bulkSelected]:activeVoiceIds(),_scopeLabel=_useSelected?`${ids.length} selected`:`${ids.length} active`;if(!ids.length){toast("No voices to precompute","error");return}if(!confirm(`Precompute speaker embeddings for ${_scopeLabel} voice(s) via \u201C${backend}\u201D?
This warms each voice so the engine caches its .pt and first playback is instant.`))return;const btn=$("precompute-embeddings-btn");btn&&(btn.disabled=!0);const ov=document.createElement("div");ov.className="audiobook-overlay",ov.id="precompute-overlay",ov.innerHTML=`<div class="audiobook-box"><div class="audiobook-title"><span class="mdi mdi-flash-outline"></span> Precomputing embeddings</div>
<div class="audiobook-msg" id="pc-msg">0 / ${ids.length}</div>
<div class="reader-synth-track"><div class="reader-synth-fill" id="pc-fill"></div></div>
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="pc-cancel">Cancel</button></div></div>`,document.body.appendChild(ov);let cancel=!1;ov.querySelector("#pc-cancel").addEventListener("click",()=>{cancel=!0});const fill=ov.querySelector("#pc-fill"),msg=ov.querySelector("#pc-msg");let done=0,ok=0,failed=0;const queue=ids.slice(),worker=async()=>{for(;queue.length&&!cancel;){const id=queue.shift();msg&&(msg.textContent=`${done} / ${ids.length} \xB7 ${id}`);const voiceBackend=typeof _ttsBackendForVoice=="function"?_ttsBackendForVoice(id,backend):backend;try{await fetchTtsPreviewBlob(id,"Hallo.","wav","",voiceBackend),ok++}catch{failed++}done++,fill&&(fill.style.width=done/ids.length*100+"%")}};try{await Promise.all(Array.from({length:Math.min(2,ids.length)},worker))}finally{ov.remove(),btn&&(btn.disabled=!1)}toast(cancel?`Cancelled \u2014 ${ok} warmed`:`Precomputed ${ok} embedding(s)${failed?`, ${failed} skipped/failed`:""}`,!ok&&failed?"error":"success")});const LIB_ADD_SAMPLE_TEXTS={EN:"The clear morning light warmed the quiet studio as I described a silver train, a bright red apple, and the gentle rhythm of rain on the window.",DE:"Das klare Morgenlicht waermte das ruhige Studio, waehrend ich einen silbernen Zug, einen roten Apfel und den sanften Rhythmus des Regens am Fenster beschrieb.",IT:"La luce chiara del mattino scaldava lo studio tranquillo mentre descrivevo un treno d argento, una mela rossa e il ritmo leggero della pioggia alla finestra.",ES:"La clara luz de la manana calentaba el estudio tranquilo mientras describia un tren plateado, una manzana roja y el suave ritmo de la lluvia en la ventana.",FR:"La lumiere claire du matin rechauffait le studio calme pendant que je decrivais un train argente, une pomme rouge et le doux rythme de la pluie sur la fenetre.",PT:"A luz clara da manha aquecia o estudio tranquilo enquanto eu descrevia um comboio prateado, uma maca vermelha e o ritmo suave da chuva na janela.",NL:"Het heldere ochtendlicht verwarmde de stille studio terwijl ik een zilveren trein, een rode appel en het zachte ritme van regen op het raam beschreef.",PL:"Jasne poranne swiatlo ogrzewalo ciche studio, gdy opisywalem srebrny pociag, czerwone jablko i lagodny rytm deszczu na oknie."},LIB_ADD_SAMPLE_STORAGE_KEY="vcf-lib-add-sample-texts";function libAddSampleOverrides(){try{return JSON.parse(localStorage.getItem(LIB_ADD_SAMPLE_STORAGE_KEY)||"{}")||{}}catch{return{}}}function getLibAddSampleText(code){return libAddSampleOverrides()[code]||LIB_ADD_SAMPLE_TEXTS[code]||LIB_ADD_SAMPLE_TEXTS.EN}function saveLibAddSampleText(){const code=$("lib-add-sample-lang").value,text=$("lib-add-sample-text").value.trim(),overrides=libAddSampleOverrides();text&&text!==LIB_ADD_SAMPLE_TEXTS[code]?overrides[code]=text:delete overrides[code],localStorage.setItem(LIB_ADD_SAMPLE_STORAGE_KEY,JSON.stringify(overrides)),setLibAddStatus("Sample sentence saved")}function resetLibAddSampleText(){const code=$("lib-add-sample-lang").value,overrides=libAddSampleOverrides();delete overrides[code],localStorage.setItem(LIB_ADD_SAMPLE_STORAGE_KEY,JSON.stringify(overrides)),$("lib-add-sample-text").value=LIB_ADD_SAMPLE_TEXTS[code]||LIB_ADD_SAMPLE_TEXTS.EN,setLibAddStatus("Sample sentence reset")}function updateLibAddSampleLanguage(lang){const code=LIB_ADD_SAMPLE_TEXTS[lang]?lang:"EN";$("lib-add-sample-lang").value=code,$("lib-add-lang").value=code,$("lib-add-sample-text").value=getLibAddSampleText(code);const voiceId=$("lib-add-voice-id").value.trim();voiceId&&/^[A-Z]{2}_/.test(voiceId)&&($("lib-add-voice-id").value=voiceId.replace(/^[A-Z]{2}_/,code+"_"))}function renderLibAddMeter(level=0,db=-1/0,clipped=!1){const meter=$("lib-add-mic-meter");if(!meter.children.length)for(let i=0;i<18;i++){const bar=document.createElement("div");bar.className="bar",meter.appendChild(bar)}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<active&&(bar.classList.add("on"),db>-12&&i>11&&bar.classList.add("hot"),clipped&&i>14&&bar.classList.add("clip"))}),$("lib-add-db-readout").textContent=Number.isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB"}function syncLibAddMicGain(){const gain=parseFloat($("lib-add-mic-gain").value)||0;$("lib-add-mic-gain-value").textContent=gain.toFixed(2)+"x",libAddState.gainNode&&(libAddState.gainNode.gain.value=gain)}function startLibAddMeter(){if(!libAddState.analyser)return;libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf);const data=new Float32Array(libAddState.analyser.fftSize),tick=()=>{libAddState.analyser.getFloatTimeDomainData(data);let sum=0,peak=0;for(const sample of data)sum+=sample*sample,peak=Math.max(peak,Math.abs(sample));const rms=Math.sqrt(sum/data.length),db=rms>0?20*Math.log10(rms):-1/0,level=Number.isFinite(db)?(db+60)/60:0;renderLibAddMeter(level,db,peak>.98),libAddState.meterRaf=requestAnimationFrame(tick)};tick()}async function ensureLibAddMicMonitor(){if(libAddState.recordStream)return;const AudioCtx=window.AudioContext||window.webkitAudioContext;if(libAddState.stream=await requestMicrophoneStream({raw:!0}),AudioCtx){libAddState.audioCtx=new AudioCtx,libAddState.sourceNode=libAddState.audioCtx.createMediaStreamSource(libAddState.stream),libAddState.gainNode=libAddState.audioCtx.createGain(),libAddState.analyser=libAddState.audioCtx.createAnalyser(),libAddState.analyser.fftSize=1024;const dest=libAddState.audioCtx.createMediaStreamDestination();syncLibAddMicGain(),libAddState.sourceNode.connect(libAddState.gainNode),libAddState.gainNode.connect(libAddState.analyser),libAddState.gainNode.connect(dest),libAddState.recordStream=dest.stream,startLibAddMeter()}else libAddState.recordStream=libAddState.stream;libAddState.monitoring=!0,$("lib-add-monitor-btn").disabled=!0,$("lib-add-monitor-stop").disabled=!1}function stopLibAddMic(){libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf),libAddState.meterRaf=null,[libAddState.sourceNode,libAddState.gainNode,libAddState.analyser].forEach(node=>{try{node&&node.disconnect()}catch{}}),libAddState.stream&&libAddState.stream.getTracks().forEach(t=>t.stop()),libAddState.recordStream&&libAddState.recordStream.getTracks().forEach(t=>t.stop()),libAddState.audioCtx&&libAddState.audioCtx.close().catch(()=>{}),libAddState.stream=null,libAddState.recordStream=null,libAddState.sourceNode=null,libAddState.gainNode=null,libAddState.analyser=null,libAddState.audioCtx=null,libAddState.monitoring=!1,$("lib-add-monitor-btn").disabled=!1,$("lib-add-monitor-stop").disabled=!0,renderLibAddMeter(0,-1/0,!1)}let libAddState={id:null,duration:0,audio:null,buffer:null,recorder:null,chunks:[],pendingSource:null,stream:null,recordStream:null,timer:null,secs:0,audioCtx:null,sourceNode:null,gainNode:null,analyser:null,meterRaf:null,monitoring:!1};window.libAddState=libAddState,$("add-new-voice-btn").addEventListener("click",()=>{$("lib-add-panel").classList.toggle("open")}),$("lib-add-sample-lang").addEventListener("change",()=>updateLibAddSampleLanguage($("lib-add-sample-lang").value)),$("lib-add-lang").addEventListener("change",()=>updateLibAddSampleLanguage($("lib-add-lang").value)),$("lib-add-sample-text").addEventListener("input",debounce(saveLibAddSampleText,500)),$("lib-add-use-sample").addEventListener("click",()=>{$("lib-add-transcript").value=$("lib-add-sample-text").value.trim(),setLibAddStatus("Sample sentence copied to transcript")}),$("lib-add-reset-sample").addEventListener("click",resetLibAddSampleText),$("lib-add-mic-help-btn").addEventListener("click",()=>{$("lib-add-mic-help").classList.toggle("open")}),$("lib-add-monitor-btn").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),setLibAddStatus("Mic level monitor active")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message)}}),$("lib-add-monitor-stop").addEventListener("click",()=>{stopLibAddMic(),setLibAddStatus("Mic level monitor stopped")}),$("lib-add-mic-gain").addEventListener("input",syncLibAddMicGain),renderLibAddMeter(),syncLibAddMicGain(),updateLibAddSampleLanguage("EN");function setLibAddStatus(msg){$("lib-add-status").textContent=msg,status(msg)}function suggestLibVoiceId(filename){if($("lib-add-voice-id").value.trim())return;const base=(typeof _umlautSafe=="function"?_umlautSafe(filename||"NewVoice"):String(filename||"NewVoice")).replace(/\.[^.]+$/,"").replace(/[^A-Za-z0-9_-]+/g,"_").replace(/^_+|_+$/g,"").slice(0,60)||"NewVoice";$("lib-add-voice-id").value=`${$("lib-add-lang").value||"EN"}_${$("lib-add-gender").value||"N"}_${base}`}function loadLibAddAudio(id,duration,label="Audio"){libAddState.id=id,libAddState.duration=Number(duration)||0,libAddState.buffer=null,$("lib-add-start").value="0.00",$("lib-add-end").value=libAddState.duration?Math.min(libAddState.duration,20).toFixed(2):"0.00",$("lib-add-audio").src="/api/audio/"+id,$("lib-add-audio").style.display="",$("lib-add-wave").style.display="",attachLibAddWaveSelection(),decodeTempAudio(id).then(buffer=>{libAddState.id===id&&(libAddState.buffer=buffer,drawLibAddWave())}).catch(()=>{}),setLibAddStatus(`${label} loaded${libAddState.duration?" ("+libAddState.duration.toFixed(1)+" s)":""}`)}async function decodeTempAudio(id){const resp=await fetch("/api/audio/"+encodeURIComponent(id));if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const data=await resp.arrayBuffer();return new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(data.slice(0))}function clampLibAddTime(value){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;return Math.max(0,Math.min(duration,Number(value)||0))}function setLibAddCropRange(start,end){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;let a=clampLibAddTime(start),b=clampLibAddTime(end);Math.abs(b-a)<.05&&(b=Math.min(duration,a+Math.min(1,duration||1))),b<a&&([a,b]=[b,a]),$("lib-add-start").value=a.toFixed(2),$("lib-add-end").value=b.toFixed(2),drawLibAddWave()}function libAddWaveTimeFromEvent(e){var _a2;const rect=$("lib-add-wave").getBoundingClientRect(),x=Math.max(0,Math.min(rect.width,e.clientX-rect.left)),duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;return rect.width?x/rect.width*duration:0}function updateLibAddCropHint(){const hint=$("lib-add-crop-hint");if(!hint)return;const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||0,dur=Math.max(0,end-start);hint.textContent=dur?`Selected ${dur.toFixed(1)}s. Aim for 3-20 seconds.`:"Select 3-20 seconds for best cloning.",hint.className="crop-duration-hint "+(dur>=3&&dur<=20?"ok":dur?"warn":"")}function drawLibAddWave(){libAddState.buffer&&(drawOptimizerWave($("lib-add-wave"),libAddState.buffer,parseFloat($("lib-add-start").value)||0,parseFloat($("lib-add-end").value)||libAddState.duration||libAddState.buffer.duration),updateLibAddCropHint())}function libAddWaveSelectionPixels(e){var _a2;const rect=$("lib-add-wave").getBoundingClientRect(),duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0,start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||duration),sx=duration&&rect.width?start/duration*rect.width:0,ex=duration&&rect.width?end/duration*rect.width:rect.width;return{x:Math.max(0,Math.min(rect.width,e.clientX-rect.left)),sx,ex,start,end,duration}}function libAddWaveDragMode(e){const{x,sx,ex}=libAddWaveSelectionPixels(e),hit=16;return Math.abs(x-sx)<=hit?"start":Math.abs(x-ex)<=hit?"end":"new"}function attachLibAddWaveSelection(){const canvas=$("lib-add-wave");if(!canvas||canvas.dataset.cropReady)return;canvas.dataset.cropReady="1";let drag=null;canvas.addEventListener("pointerdown",e=>{var _a2;if(!libAddState.buffer)return;e.preventDefault();const mode=libAddWaveDragMode(e),t=libAddWaveTimeFromEvent(e),currentStart=parseFloat($("lib-add-start").value)||0,currentEnd=parseFloat($("lib-add-end").value)||libAddState.duration||0;drag={mode,anchor:t,start:currentStart,end:currentEnd},(_a2=canvas.setPointerCapture)==null||_a2.call(canvas,e.pointerId),mode==="start"?setLibAddCropRange(t,currentEnd):setLibAddCropRange(mode==="end"?currentStart:t,t),setLibAddStatus(mode==="start"?"Dragging crop start handle":mode==="end"?"Dragging crop end handle":"Drag to choose a new crop range")}),canvas.addEventListener("pointermove",e=>{if(!libAddState.buffer)return;if(!drag){const mode=libAddWaveDragMode(e);canvas.style.cursor=mode==="start"||mode==="end"?"ew-resize":"crosshair";return}e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t)});const finish=e=>{if(!drag)return;e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t),drag=null;const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||0;setLibAddStatus(`Crop range ${start.toFixed(2)}s to ${end.toFixed(2)}s (${Math.max(0,end-start).toFixed(1)}s) selected`)};canvas.addEventListener("pointerup",finish),canvas.addEventListener("pointerleave",()=>{drag||(canvas.style.cursor="crosshair")}),canvas.addEventListener("pointercancel",()=>{drag=null,canvas.style.cursor="crosshair"})}async function uploadLibAddFile(file){if(!file)return;const fd=new FormData;fd.append("file",file),setLibAddStatus("Uploading audio\u2026");try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();suggestLibVoiceId(file.name),loadLibAddAudio(d.id,d.duration,file.name||"Audio"),toast("Audio loaded","success")}catch(e){toast("Load failed: "+e.message,"error"),setLibAddStatus("Load failed")}}const libAddDrop=$("lib-add-drop");libAddDrop.addEventListener("click",()=>$("lib-add-file").click()),libAddDrop.addEventListener("dragover",e=>{e.preventDefault(),libAddDrop.classList.add("drag-over")}),libAddDrop.addEventListener("dragleave",()=>libAddDrop.classList.remove("drag-over")),libAddDrop.addEventListener("drop",e=>{e.preventDefault(),libAddDrop.classList.remove("drag-over"),e.dataTransfer.files.length&&uploadLibAddFile(e.dataTransfer.files[0])}),$("lib-add-file").addEventListener("change",async()=>{$("lib-add-file").files.length&&await uploadLibAddFile($("lib-add-file").files[0]),$("lib-add-file").value=""}),$("lib-add-url-btn").addEventListener("click",()=>{const url=$("lib-add-url").value.trim();if(!url){toast("Enter a YouTube or audio URL","error");return}$("lib-add-url-btn").disabled=!0,setLibAddStatus("Starting download\u2026");const es=new EventSource("/api/download-yt?url="+encodeURIComponent(url));es.onmessage=e=>{const d=JSON.parse(e.data);d.error?(toast("Download failed: "+d.error,"error"),setLibAddStatus(d.error),$("lib-add-url-btn").disabled=!1,es.close()):d.done?(es.close(),$("lib-add-url-btn").disabled=!1,suggestLibVoiceId(url.split("/").pop()||"DownloadedVoice"),loadLibAddAudio(d.id,d.duration,"Downloaded audio"),toast("URL audio loaded","success")):setLibAddStatus(d.msg||"Downloading\u2026")},es.onerror=()=>{es.close(),$("lib-add-url-btn").disabled=!1,setLibAddStatus("Download connection closed")}}),$("lib-add-rec-start").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),libAddState.chunks=[],libAddState.secs=0,$("lib-add-rec-time").textContent="0:00",$("lib-add-rec-start").disabled=!0,$("lib-add-rec-stop").disabled=!1,$("lib-add-monitor-stop").disabled=!0,libAddState.timer=setInterval(()=>{libAddState.secs++,$("lib-add-rec-time").textContent=Math.floor(libAddState.secs/60)+":"+String(libAddState.secs%60).padStart(2,"0")},1e3),libAddState.recorder=new MediaRecorder(libAddState.recordStream),libAddState.recorder.ondataavailable=e=>{e.data.size&&libAddState.chunks.push(e.data)},libAddState.recorder.onstop=async()=>{clearInterval(libAddState.timer),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0;const blob=new Blob(libAddState.chunks,{type:libAddState.recorder.mimeType||"audio/webm"}),ext=(libAddState.recorder.mimeType||"").includes("ogg")?".ogg":".webm";stopLibAddMic(),suggestLibVoiceId("recording"),await uploadLibAddFile(new File([blob],"recording"+ext,{type:blob.type}))},libAddState.recorder.start(100),setLibAddStatus("Recording\u2026")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0}}),$("lib-add-rec-stop").addEventListener("click",()=>{libAddState.recorder&&libAddState.recorder.state!=="inactive"&&libAddState.recorder.stop()}),$("lib-add-auto-trim").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}$("lib-add-auto-trim").disabled=!0;try{const r=await fetch("/api/auto-trim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id})});let d;if(r.ok)d=await r.json();else if(r.status===404||r.status===405)d=await clientAutoTrimBounds(libAddState.id);else{const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}$("lib-add-start").value=Number(d.start).toFixed(2),$("lib-add-end").value=Number(d.end).toFixed(2),drawLibAddWave(),setLibAddStatus(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error"),setLibAddStatus("Auto trim failed")}finally{$("lib-add-auto-trim").disabled=!1}});async function transcribeLibAddCurrent(successMessage="Text recognised",audioId=libAddState.id){if(!audioId)throw new Error("Load audio first");setLibAddStatus("Recognising text...");const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:audioId})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const text=(await r.json()).text||"";return $("lib-add-transcript").value=text,setLibAddStatus(successMessage),text}function openSavedLibraryVoice(voiceId){const openRow=()=>{var _a2;const row=Array.from(document.querySelectorAll(".vl-row")).find(r=>r.dataset.id===voiceId);return row?(row.scrollIntoView({behavior:"smooth",block:"center"}),row.classList.contains("edit-open")||(_a2=row.querySelector(".edit-audio-btn"))==null||_a2.click(),!0):!1};openRow()||setTimeout(openRow,150)}async function applyLibAddCrop(){if(!libAddState.id){toast("Load audio first","error");return}const start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||libAddState.duration),duration=end-start;if(end<=start+.1){toast("Crop range is too short","error"),setLibAddStatus("Crop range is too short");return}(duration<3||duration>20)&&toast("Best clone references are 3-20 seconds; cropping anyway.","error"),["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!0)}),setLibAddStatus(`Cropping ${start.toFixed(2)}s to ${end.toFixed(2)}s...`);try{const r=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start,end})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();loadLibAddAudio(d.id,d.duration,"Cropped audio"),toast("Crop applied","success");try{await transcribeLibAddCurrent("Cropped audio loaded and text recognised",d.id)}catch(e){toast("Crop applied, but recognition failed: "+e.message,"error"),setLibAddStatus("Cropped audio loaded; recognition failed")}}catch(e){toast("Crop failed: "+e.message,"error"),setLibAddStatus("Crop failed")}finally{["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!1)})}}$("lib-add-save-crop").addEventListener("click",applyLibAddCrop),$("lib-add-save-crop-bottom").addEventListener("click",applyLibAddCrop),["lib-add-start","lib-add-end"].forEach(id=>$(id).addEventListener("input",drawLibAddWave)),$("lib-add-play").addEventListener("click",()=>{if(!libAddState.id)return;libAddState.audio&&libAddState.audio.pause(),libAddState.audio=new Audio("/api/audio/"+libAddState.id);const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||libAddState.duration;libAddState.audio.currentTime=start,libAddState.audio.ontimeupdate=()=>{libAddState.audio.currentTime>=end&&libAddState.audio.pause()},libAddState.audio.play()}),$("lib-add-recognize").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}try{await transcribeLibAddCurrent("Text recognised")}catch(e){toast("Recognition failed: "+e.message,"error"),setLibAddStatus("Recognition failed")}}),$("lib-add-save").addEventListener("click",async()=>{var _a2,_b2;if(!libAddState.id){toast("Load audio first","error");return}const voiceId=$("lib-add-voice-id").value.trim()||`${$("lib-add-lang").value}_${$("lib-add-gender").value}_NewVoice`;if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}setLibAddStatus("Saving voice..."),$("lib-add-save").disabled=!0;try{const pr=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start:parseFloat($("lib-add-start").value)||0,end:parseFloat($("lib-add-end").value)||libAddState.duration})});if(!pr.ok){const e=await pr.json().catch(()=>({}));throw new Error(e.detail||pr.statusText)}const p=await pr.json();let transcript=$("lib-add-transcript").value.trim();if(!transcript&&(transcript=await transcribeLibAddCurrent("Final clip recognised; saving voice...",p.id),!transcript.trim()))throw new Error("Recognition returned no transcript; add text or try recognising again.");const sr=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:p.id,voice_id:voiceId,transcript})});if(!sr.ok){const e=await sr.json().catch(()=>({}));throw new Error(e.detail||sr.statusText)}if((_a2=libAddState.pendingSource)!=null&&_a2.imageUrl)try{await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,image_url:libAddState.pendingSource.imageUrl})})}catch{}libAddState.pendingSource=null,setLibAddSourcePreview({}),(_b2=$("lib-add-panel"))==null||_b2.classList.remove("open"),toast("Voice saved: "+voiceId,"success"),setLibAddStatus("Voice saved"),await loadVoiceLibrary(),openSavedLibraryVoice(voiceId)}catch(e){toast("Save failed: "+e.message,"error"),setLibAddStatus("Save failed")}finally{$("lib-add-save").disabled=!1}}),$("show-disabled-cb").addEventListener("change",()=>{$("disabled-info").style.display=$("show-disabled-cb").checked?"":"none",renderVoiceList()}),["library-filter-lang","library-filter-sex","library-filter-type","library-filter-rating"].forEach(id=>{var _a2;(_a2=$(id))==null||_a2.addEventListener("change",()=>{readLibraryFilters(),renderVoiceList()})}),(_D=$("library-filter-text"))==null||_D.addEventListener("input",debounce(()=>{readLibraryFilters(),renderVoiceList()},180)),(_E=$("library-filter-tag"))==null||_E.addEventListener("change",function(){window._voiceTagFilter=this.value||null,renderVoiceList()}),(_F=$("library-filter-group"))==null||_F.addEventListener("change",function(){window._voiceGroupFilter=this.value||null,renderVoiceList()}),(_G=$("library-clear-filters"))==null||_G.addEventListener("click",()=>{window._voiceTagFilter=null,window._voiceGroupFilter=null;const tagSel=$("library-filter-tag"),groupSel=$("library-filter-group");tagSel&&(tagSel.value=""),groupSel&&(groupSel.value=""),clearLibraryFilters()}),(_H=$("library-tts-backend-select"))==null||_H.addEventListener("change",()=>{var _a2;status("Library TTS engine: "+(((_a2=backendById(libraryTtsBackend()))==null?void 0:_a2.label)||libraryTtsBackend()))});function renderVoiceList(){var _a2,_b2,_c2,_d2,_e2,_f2;const showDisabled=$("show-disabled-cb").checked,list=$("voice-list");list.innerHTML="",renderVoiceGroupsBar(),populateLibraryFilters(),readLibraryFilters();const _gtBtn=$("voice-group-tag-btn");if(_gtBtn){_gtBtn.classList.toggle("active",!!window._voiceGroupByTag);const ic=_gtBtn.querySelector(".mdi");ic&&(ic.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline")}const _tvBtn=$("voice-table-view-btn");if(_tvBtn){_tvBtn.classList.toggle("active",!!window._voiceTableView),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",!!window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode))}const cat=window._voiceSidebarCat||"all",enabledOk=v=>showDisabled||v.enabled!==!1;let filtered=_voices.filter(v=>cat==="cloned"?enabledOk(v)&&v.has_ref&&v.origin!=="designed":cat==="designed"?enabledOk(v)&&(v.origin==="designed"||!v.has_ref):cat==="favorites"?enabledOk(v)&&(v.rating||0)>=4:cat==="hidden"?v.enabled===!1:enabledOk(v));window._voiceGroupFilter&&(filtered=filtered.filter(v=>(v.group||"").trim()===window._voiceGroupFilter)),window._voiceTagFilter&&(filtered=filtered.filter(v=>String(v.tag||"").split(",").map(t=>t.trim()).includes(window._voiceTagFilter)));const visibleCount=filtered.length;filtered=filtered.filter(libraryFilterMatch);const filterCount=filtered.length;if(_libraryIssueFilter&&(filtered=filtered.filter(v=>libraryIssueMatch(v))),$("voice-count").textContent=filtered.length+" / "+_voices.length+" voices",filtered=filtered.slice().sort((a,b)=>{const av=getSortValue(a,_sortField),bv=getSortValue(b,_sortField);return av<bv?-_sortDir:av>bv?_sortDir:0}),updateLibraryInsights(),_libraryIssueFilter){const note=document.createElement("div");note.className="library-filter-note",note.innerHTML=`<span><strong>${escHtml(filtered.length)} / ${escHtml(filterCount)}</strong> ${escHtml(libraryIssueLabel())}: ${escHtml(describeIssueVoices())}</span><button class="btn-secondary" type="button">Clear filter</button>`,note.querySelector("button").addEventListener("click",()=>setLibraryIssueFilter("")),list.appendChild(note)}if(!filtered.length){if(_voices.length===0){const emptyEl=document.createElement("div");emptyEl.className="voices-empty-state",emptyEl.innerHTML=`
<div class="voices-empty-cases">
<div class="voices-empty-case">
<div class="voices-empty-case-icon"><span class="mdi mdi-folder-outline"></span></div>
<h4>Wrong folder path?</h4>
<p>Set the path where your <code>.wav</code> voice files live <em>inside the container</em>.</p>
<div class="voices-folder-row">
<input id="voices-empty-scan-dir" type="text" class="voices-folder-input" placeholder="/voices">
<button class="btn-secondary" id="voices-empty-browse-btn" title="Browse container filesystem"><span class="mdi mdi-folder-outline"></span> Browse</button>
<button class="btn-primary" id="voices-empty-save-btn">Set &amp; reload</button>
</div>
<div class="vef-dir-browser" id="vef-dir-browser" hidden>
<div class="vef-breadcrumb" id="vef-breadcrumb"></div>
<div class="vef-dir-list" id="vef-dir-list"><span class="vef-loading">Loading\u2026</span></div>
<div class="vef-browser-actions">
<span class="vef-selected-path note" id="vef-selected-path"></span>
<button class="btn-primary" id="vef-select-btn">Use this folder</button>
</div>
</div>
<p class="voices-empty-hint">Map a host folder via docker-compose:<br>
<code>- /your/host/path:/voices:rw</code><br>
or set <code>VOICE_HOST_DIR=/your/host/path</code> in the stack env.</p>
</div>
<div class="voices-empty-divider">or</div>
<div class="voices-empty-case">
<div class="voices-empty-case-icon"><span class="mdi mdi-microphone-variant"></span></div>
<h4>Folder is empty?</h4>
<p>Create your first voice from a recording or download ready-made voices.</p>
<div class="voices-empty-actions">
<button class="btn-primary voices-empty-action-btn" id="voices-goto-clone"><span class="mdi mdi-lightning-bolt"></span> Clone a Voice</button>
<button class="btn-secondary voices-empty-action-btn" id="voices-goto-studio"><span class="mdi mdi-earth"></span> Get Voices Online</button>
</div>
</div>
</div>
`,list.appendChild(emptyEl),fetch("/api/settings").then(r=>r.json()).then(s=>{const inp=document.getElementById("voices-empty-scan-dir");inp&&(inp.value=s.voices_scan_dir||"/voices")}).catch(()=>{});let _vefCurrentPath="/";const vefBrowser=document.getElementById("vef-dir-browser"),vefDirList=document.getElementById("vef-dir-list"),vefCrumb=document.getElementById("vef-breadcrumb"),vefSelPath=document.getElementById("vef-selected-path");async function vefNavigate(path){_vefCurrentPath=path,vefDirList.innerHTML='<span class="vef-loading">Loading\u2026</span>',vefSelPath&&(vefSelPath.textContent=path);try{const data=await fetch("/api/browse-dirs?path="+encodeURIComponent(path)).then(r=>r.json()),parts=data.path.split("/").filter(Boolean),crumbs=[{label:"/",path:"/"}];parts.forEach((p,i)=>crumbs.push({label:p,path:"/"+parts.slice(0,i+1).join("/")})),vefCrumb.innerHTML=crumbs.map((c,i)=>i<crumbs.length-1?`<button class="vef-crumb-btn" data-path="${escHtml(c.path)}">${escHtml(c.label)}</button><span class="vef-sep">/</span>`:`<span class="vef-crumb-cur">${escHtml(c.label)}</span>`).join(""),vefCrumb.querySelectorAll(".vef-crumb-btn").forEach(b=>b.addEventListener("click",()=>vefNavigate(b.dataset.path))),data.dirs.length?(vefDirList.innerHTML=data.dirs.map(d=>`<button class="vef-dir-item" data-path="${escHtml(data.path==="/"?"/"+d:data.path+"/"+d)}"><span class="mdi mdi-folder-outline"></span> ${escHtml(d)}</button>`).join(""),vefDirList.querySelectorAll(".vef-dir-item").forEach(b=>b.addEventListener("click",()=>vefNavigate(b.dataset.path)))):vefDirList.innerHTML='<span class="vef-empty">No subdirectories here.</span>',vefSelPath&&(vefSelPath.textContent=data.path),_vefCurrentPath=data.path}catch(e){vefDirList.innerHTML=`<span class="vef-error">Error: ${escHtml(e.message)}</span>`}}(_b2=document.getElementById("voices-empty-browse-btn"))==null||_b2.addEventListener("click",()=>{var _a3,_b3;const open=vefBrowser.hidden;if(vefBrowser.hidden=!open,open){const cur=((_b3=(_a3=document.getElementById("voices-empty-scan-dir"))==null?void 0:_a3.value)==null?void 0:_b3.trim())||"/voices";vefNavigate(cur)}}),(_c2=document.getElementById("vef-select-btn"))==null||_c2.addEventListener("click",()=>{const inp=document.getElementById("voices-empty-scan-dir");inp&&(inp.value=_vefCurrentPath),vefBrowser&&(vefBrowser.hidden=!0)}),(_d2=document.getElementById("voices-empty-save-btn"))==null||_d2.addEventListener("click",async()=>{var _a3;const inp=document.getElementById("voices-empty-scan-dir"),dir=(_a3=inp==null?void 0:inp.value)==null?void 0:_a3.trim();if(!dir)return;const btn=document.getElementById("voices-empty-save-btn");btn.disabled=!0,btn.textContent="Saving\u2026";try{const s=await fetch("/api/settings").then(r2=>r2.json());s.voices_scan_dir=dir;const r=await fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);toast("Folder saved \u2014 reloading voices\u2026","success"),_libraryLoadPromise=null,await loadVoiceLibrary()}catch(e){toast("Failed: "+e.message,"error"),btn.disabled=!1,btn.textContent="Set & reload"}}),(_e2=document.getElementById("voices-goto-clone"))==null||_e2.addEventListener("click",()=>navTo("s-clone")),(_f2=document.getElementById("voices-goto-studio"))==null||_f2.addEventListener("click",()=>navTo("s-studio"))}else list.innerHTML+='<div style="padding:14px;color:var(--subtext);font-size:14px">No voices match the current filters.</div>';return}if(window._voiceGroupByTag)renderVoicesByTag(list,filtered);else{const frag=document.createDocumentFragment();filtered.forEach(v=>frag.appendChild(makeVoiceRow(v))),list.appendChild(frag)}if(_pendingSelectId){const wrap=list.querySelector(`[data-id="${CSS.escape(_pendingSelectId)}"]`);wrap&&(_pendingSelectId=null,selectVoice(wrap))}syncSortHeaders()}function renderVoicesByTag(list,filtered){const UNTAG="\u2014 Untagged \u2014",groups=new Map;filtered.forEach(v=>{const tags=String(v.tag||"").split(",").map(t=>t.trim()).filter(Boolean);(tags.length?tags:[UNTAG]).forEach(t=>{groups.has(t)||groups.set(t,[]),groups.get(t).push(v)})});let collapsed={};try{collapsed=JSON.parse(localStorage.getItem("vl-tag-collapsed")||"{}")}catch{}const names=[...groups.keys()].sort((a,b)=>a===UNTAG?1:b===UNTAG?-1:a.toLowerCase().localeCompare(b.toLowerCase())),outer=document.createDocumentFragment();names.forEach(tag=>{const voices=groups.get(tag),isCol=!!collapsed[tag],sec=document.createElement("div");sec.className="vl-tag-group";const head=document.createElement("button");head.className="vl-tag-head"+(isCol?" collapsed":""),head.type="button",head.innerHTML=`<span class="mdi mdi-${isCol?"chevron-right":"chevron-down"} vl-tag-caret"></span><span class="mdi mdi-folder${isCol?"":"-open"}-outline"></span><span class="vl-tag-name">${escHtml(tag)}</span><span class="vl-tag-count">${voices.length}</span>`;const body=document.createElement("div");body.className="vl-tag-body",isCol&&(body.style.display="none");const rows=document.createDocumentFragment();voices.forEach(v=>rows.appendChild(makeVoiceRow(v))),body.appendChild(rows),head.addEventListener("click",()=>{const nowCol=body.style.display!=="none";body.style.display=nowCol?"none":"",head.classList.toggle("collapsed",nowCol),head.querySelector(".vl-tag-caret").className="mdi mdi-"+(nowCol?"chevron-right":"chevron-down")+" vl-tag-caret",head.querySelector(".mdi:nth-child(2)").className="mdi mdi-folder"+(nowCol?"":"-open")+"-outline";let c={};try{c=JSON.parse(localStorage.getItem("vl-tag-collapsed")||"{}")}catch{}c[tag]=nowCol,localStorage.setItem("vl-tag-collapsed",JSON.stringify(c))}),sec.appendChild(head),sec.appendChild(body),outer.appendChild(sec)}),list.appendChild(outer)}async function decodeVoiceAudio(v){const resp=await fetch(voiceFileUrl(v),{cache:"no-store"});if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const data=await resp.arrayBuffer();return new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(data.slice(0))}function drawOptimizerWave(canvas,buffer,start=0,end=buffer.duration){const dpr=window.devicePixelRatio||1,width=Math.max(1,canvas.clientWidth),height=Math.max(1,canvas.clientHeight);canvas.width=Math.round(width*dpr),canvas.height=Math.round(height*dpr);const ctx=canvas.getContext("2d");ctx.setTransform(dpr,0,0,dpr,0,0),ctx.clearRect(0,0,width,height),ctx.fillStyle=getComputedStyle(document.documentElement).getPropertyValue("--bg")||"#111",ctx.fillRect(0,0,width,height);const data=buffer.getChannelData(0),step=Math.max(1,Math.floor(data.length/width)),mid=height/2;ctx.strokeStyle="#89b4fa",ctx.lineWidth=1,ctx.beginPath();for(let x=0;x<width;x++){let min=1,max=-1;const base=x*step;for(let i=0;i<step&&base+i<data.length;i++){const v=data[base+i];v<min&&(min=v),v>max&&(max=v)}ctx.moveTo(x,mid+min*mid*.92),ctx.lineTo(x,mid+max*mid*.92)}ctx.stroke();const sx=Math.max(0,Math.min(width,start/buffer.duration*width)),ex=Math.max(sx,Math.min(width,end/buffer.duration*width));ctx.fillStyle="rgba(166,227,161,.18)",ctx.fillRect(sx,0,ex-sx,height),ctx.strokeStyle="#a6e3a1",ctx.lineWidth=2,ctx.strokeRect(sx+.5,.5,Math.max(1,ex-sx-1),height-1);const selectedSec=Math.max(0,end-start);if(ex-sx>54&&selectedSec>0){const label=`${selectedSec.toFixed(1)}s`;ctx.font="12px ui-monospace, Menlo, Consolas, monospace";const textW=ctx.measureText(label).width+14,tx=Math.max(sx+6,Math.min(ex-textW-6,sx+(ex-sx-textW)/2));ctx.fillStyle="rgba(30,30,46,.78)",ctx.fillRect(tx,6,textW,22),ctx.fillStyle="#ffffff",ctx.fillText(label,tx+7,21)}const handleW=12;ctx.fillStyle="#3d5ce8",ctx.strokeStyle="#ffffff",[sx,ex].forEach((x,idx)=>{const hx=Math.max(0,Math.min(width-handleW,x-handleW/2));ctx.fillRect(hx,0,handleW,height),ctx.strokeRect(hx+.5,.5,Math.max(1,handleW-1),height-1),ctx.fillStyle="#ffffff",ctx.fillRect(hx+3,Math.max(12,height/2-11),2,22),ctx.fillRect(hx+7,Math.max(12,height/2-11),2,22),ctx.fillStyle="#3d5ce8"})}function makeVoiceRow(v){var _a2;const wrap=document.createElement("div");wrap.className="vl-row"+(v.enabled===!1?" vr-disabled":""),wrap.dataset.id=v.id;const color=avatarColor(v.id);wrap.dataset.color=color,wrap.dataset.hasRef=v.has_ref?"true":"false",wrap.dataset.dbfs=fmtDbfs(v),wrap.dataset.lang=v.lang||v.id.split("_")[0].toUpperCase();const langCode=v.lang||v.id.split("_")[0].toUpperCase(),_idParts=String(v.id).split("_"),dispName=v.name&&String(v.name).trim()?String(v.name).trim():_idParts.length>1?_idParts[_idParts.length-1]:v.id,langOpts=FLAG_OPTIONS[langCode]||[],flagOpts=langOpts.length>1?langOpts:ALL_FLAGS,currentFlag=v.flag||LANG_FLAG_DEFAULT[langCode]||"",flagEmoji=currentFlag?cc2flag(currentFlag):'<span class="mdi mdi-earth"></span>',flagCode=currentFlag?ccDisplay(currentFlag):"?";wrap.dataset.flagEmoji=flagEmoji,wrap.dataset.flagCc=currentFlag||"",wrap.dataset.rating=v.rating||0,wrap.dataset.hasPicture=v.has_picture?"true":"false";const fileType=voiceFileType(v),dbfs=fmtDbfs(v),dbTitle=v.loudness?`avg ${dbfs} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",benchText=fmtBenchmark(v),benchTitle=benchmarkTitle(v),benchCls=benchmarkClass(v),genderMap={F:"\u2640",M:"\u2642",N:"\u26A5","":"?"},genderMapLetter={F:"F",M:"M",N:"D","":"?"},genderLabel={F:"Female",M:"Male",N:"Diverse","":"\u2014"},genderClass={F:"g-f",M:"g-m",N:"g-n","":"g-n"},gender=v.gender||"",starsHtml=[1,2,3,4,5].map(i=>`<span class="star${i<=(v.rating||0)?" on":""}" data-val="${i}"><span class="mdi mdi-star"></span></span>`).join(""),picSrc=v.has_picture?`/api/voice/picture/${encodeURIComponent(v.id)}`:null,pickerHtml=flagOpts.length>1?flagOpts.map(([cc,label])=>`<span class="flag-opt${cc===currentFlag?" active":""}" data-cc="${cc}" title="${label}">${cc2flag(cc)}<span class="fo-code">${ccDisplay(cc)}</span></span>`).join(""):'<span style="font-size:12px;color:var(--subtext);padding:3px 6px">No regional variants</span>',isClone=v.has_ref&&v.origin!=="designed",initial=v.id[0]?v.id[0].toUpperCase():"?",flagIconHtml=currentFlag?`<span class="fi fi-${currentFlag.toLowerCase()}" role="img" aria-label="${langCode}"></span>`:null,silHtml=`<span class="mdi ${gender==="M"?"mdi-face-man":gender==="F"?"mdi-face-woman":"mdi-account"}" style="color:#fff;font-size:20px;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:${color}"></span>`,iconHtml=window.voiceAvatarIcon?window.voiceAvatarIcon(v.avatar,34):null,avatarClass=picSrc?"vl-avatar vl-avatar-photo":iconHtml?"vl-avatar vl-avatar-icon":"vl-avatar vl-avatar-sil",avatarStyle=picSrc||iconHtml?"":`style="background:${color}"`,avatarContent=picSrc?`<img src="${picSrc}" alt="" class="vl-avatar-img" loading="lazy" decoding="async">`:iconHtml||silHtml;wrap.innerHTML=`
<div class="vl-compact">
<div class="${avatarClass}" ${avatarStyle}>${avatarContent}</div>
<div class="vl-info">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit vl-name-input" data-field="name" value="${escHtml(dispName)}" title="Display Name" placeholder="Name">`:`<span class="vl-name" title="${escHtml(v.id)}">${escHtml(dispName)}</span>`}
<div class="vl-meta">
<span class="vl-lang-label" title="${escHtml(currentFlag||langCode)}">${flagEmoji}</span>
${gender?`<span class="vl-gender-letter ${genderClass[gender]||"g-n"}" title="${genderLabel[gender]||""}">${genderMapLetter[gender]||"?"}</span>`:""}
${benchText!=="-"?`<span class="vl-bench-chip ${benchCls}" title="${escHtml(benchTitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(benchText.split(" ")[0])}</span>`:""}
<div class="vr-rating">${starsHtml}</div>
<span class="vl-type-label ${isClone?"vl-type-clone":"vl-type-design"}">${isClone?"Clone":"Design"}</span>
</div>
</div>
<div class="vr-play-group">
<div class="vr-play vr-play-original"><button class="play-btn-orig" title="Play original recording" aria-label="Play original recording"><span class="mdi mdi-play"></span></button></div>
<div class="vr-play vr-play-synth"><button class="play-btn-synth" title="Generate and play TTS preview" aria-label="Generate and play TTS preview"><span class="mdi mdi-play"></span></button></div>
</div>
<div class="vl-tbl-lang" title="${escHtml(currentFlag||langCode)}">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit" data-field="lang" value="${escHtml(langCode)}" style="width:100%">`:`${flagEmoji} ${escHtml(langCode)}`}
</div>
<div class="vl-tbl-gender" title="${escHtml(genderLabel[gender]||"")}">
${window._voiceTableEditMode?`<select class="vl-inline-edit" data-field="gender" style="width:100%"><option value="N" ${gender==="N"?"selected":""}>N</option><option value="M" ${gender==="M"?"selected":""}>M</option><option value="F" ${gender==="F"?"selected":""}>F</option></select>`:`${genderMap[gender]||"?"} ${escHtml(genderLabel[gender]||"\u2014")}`}
</div>
<div class="vl-tbl-factor ${benchCls}" title="${escHtml(benchTitle)}">${escHtml(fmtFactor(v))}</div>
<div class="vl-tbl-wpm" title="${escHtml(voiceWpm(v)!=null?`${voiceWpm(v)} words per minute \u2014 how fast this voice speaks. 130\u2013180 wpm is natural for long listening.`:"Benchmark this voice to see speaking rate")}">${escHtml(fmtWpm(v))}</div>
<div class="vl-tbl-dbfs" title="${dbTitle}">${dbfs} dB</div>
<div class="vl-tbl-type"><span class="vl-type-label ${isClone?"vl-type-clone":"vl-type-design"}">${isClone?"Clone":"Design"}</span></div>
<div class="vl-tbl-rating">
${window._voiceTableEditMode?`<input type="number" min="0" max="5" class="vl-inline-edit" data-field="rating" value="${v.rating||0}" style="width:40px">`:starsHtml}
</div>
<div class="vl-tbl-source" title="Click to edit source">${escHtml(_displaySource(v)||"-")}</div>
<div class="vl-tbl-seed">${v.seed!=null?"#"+v.seed:"Auto"}</div>
<div class="vl-tbl-note" title="${escHtml(v.note||"")}">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit" data-field="note" value="${escHtml(v.note||"")}" style="width:100%">`:escHtml(v.note||"")}
</div>
<div class="vl-tbl-tag" title="${escHtml(v.tag||"")}">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit" data-field="tag" value="${escHtml(v.tag||"")}" style="width:100%">`:escHtml(v.tag||"")}
</div>
<div class="vl-tbl-active">
${window._voiceTableEditMode?`<input type="checkbox" class="vl-inline-edit" data-field="enabled" ${v.enabled!==!1?"checked":""} style="width:16px;height:16px">`:`<span class="mdi ${v.enabled!==!1?"mdi-check-circle":"mdi-close-circle"}" style="color:${v.enabled!==!1?"var(--green)":"var(--subtext)"}"></span>`}
</div>
</div>
<div class="vr-main-row">
<div class="vr-photo" title="Click to upload photo">
${picSrc?`<img src="${picSrc}" alt="" loading="lazy" decoding="async">`:'<div class="ph-icon"><span class="mdi mdi-account-outline"></span></div>'}
<input type="file" class="photo-input" accept="image/*">
</div>
<div class="vr-identity">
<div class="vr-flag">
<span class="flag-emoji">${flagEmoji}</span>
<span class="flag-code">${flagCode}</span>
<div class="flag-picker">${pickerHtml}</div>
</div>
<div class="vr-gender">
<span class="gender-badge ${genderClass[gender]||"g-n"}" title="Click to cycle">
<span class="gender-sym">${genderMap[gender]||"?"}</span>
<span class="gender-txt">${genderLabel[gender]||"\u2014"}</span>
</span>
</div>
<div class="vr-name">
<span class="vr-name-text" title="${escHtml(v.id)}">${escHtml(v.id)}</span>
<span class="rename-confirm">
<input class="vr-name-input" value="${escHtml(v.id)}" spellcheck="false">
<button class="icon-btn rename-ok" title="Save"><span class="mdi mdi-check"></span></button>
<button class="icon-btn rename-cancel" title="Cancel"><span class="mdi mdi-close"></span></button>
</span>
</div>
</div>
<div class="vr-type" title="${escHtml(fileType)}">${escHtml(fileType.toUpperCase())}</div>
<div class="vr-length" title="${escHtml(String((_a2=v.duration)!=null?_a2:""))}">${fmtDuration(v.duration)}</div>
<div class="vr-db" title="${escHtml(dbTitle)}">
<span class="vr-db-value">${escHtml(dbfs)}</span>
<button class="normalize-voice-btn" title="Normalize this voice">\xB1</button>
</div>
<div class="vr-bench ${benchCls}" title="${escHtml(benchTitle)}">
<span class="vr-bench-value">${escHtml(benchText)}</span>
<button class="btn-secondary benchmark-one-btn" title="Benchmark this voice">\u23F1</button>
</div>
<div class="vr-rating">${starsHtml}</div>
<div class="vr-edit">
<button class="edit-audio-btn" title="Edit audio"><span class="mdi mdi-pencil-outline"></span></button>
</div>
<div class="vr-play-group">
<div class="vr-play vr-play-original">
<button title="Play / pause original recording"><span class="mdi mdi-play"></span></button>
</div>
<div class="vr-play vr-play-synth">
<button title="Generate and play synthesized sample"><span class="mdi mdi-play"></span></button>
</div>
</div>
</div>
<div class="vr-detail-row">
<div class="vr-ref">
<label>Reference</label>
<div class="vr-inline">
<input type="text" value="${escHtml(v.transcript||"")}" placeholder="No reference text" title="${escHtml(v.transcript||"")}">
<button class="ref-transcribe-btn" title="Recognise reference text" style="${v.transcript?"display:none":""}"><span class="mdi mdi-message-text-outline"></span></button>
</div>
</div>
<div class="vr-note">
<label>Note</label>
<div class="vr-inline">
<input type="text" placeholder="Add note..." value="${escHtml(v.note||"")}">
</div>
</div>
<div class="vr-source">
<label>Source</label>
<div class="vr-inline">
<input type="text" placeholder="e.g. fish-audio, cloned, designed\u2026" value="${escHtml(v.origin||"")}">
</div>
</div>
<div class="vr-detail-active">
<label>Active</label>
<div class="vr-active-tools">
<div class="vr-toggle">
<label class="toggle" title="${v.enabled===!1?"Disabled - click to enable":"Enabled - click to disable"}">
<input type="checkbox" ${v.enabled!==!1?"checked":""}>
<span class="t-slider"></span>
</label>
</div>
</div>
</div>
<div class="vr-detail-delete">
<button class="delete-btn" title="Delete voice permanently">Delete voice</button>
<div class="delete-confirm" role="group" aria-label="Confirm delete voice">
<strong>Delete?</strong>
<span title="${escHtml(v.id)}">${escHtml(v.id)}</span>
<button class="btn-secondary delete-confirm-cancel" type="button">Cancel</button>
<button class="delete-confirm-go" type="button">Delete</button>
</div>
</div>
</div>
<div class="vr-optimizer">
<div class="optimizer-grid">
<div class="opt-group opt-trim-panel">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Reference audio &middot; crop</span></div>
<div class="opt-group-subtitle">Crop the saved WAV to 3&ndash;20&nbsp;s of clean speech.</div>
<div class="opt-group-body">
<canvas class="opt-wave"></canvas>
<div class="opt-controls">
<div class="opt-field"><label>Start</label><input class="opt-start" type="number" step="0.01" value="0"></div>
<div class="opt-field"><label>End</label><input class="opt-end" type="number" step="0.01" value="0"></div>
<button class="btn-secondary opt-auto-trim">Auto trim</button>
<button class="btn-secondary opt-play">Play crop</button>
<button class="btn-secondary opt-undo">Undo crop</button>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-crop">Save crop</button>
</div>
</div>
</div>
<div class="opt-group opt-text-panel open">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Reference transcript</span></div>
<div class="opt-group-subtitle">The spoken text that matches this voice recording.</div>
<div class="opt-group-body">
<textarea class="opt-transcript" placeholder="Reference text">${escHtml(v.transcript||"")}</textarea>
<div class="opt-controls">
<button class="btn-secondary opt-recognize">Re-recognise text</button>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-text">Save text</button>
</div>
</div>
</div>
<div class="opt-group opt-compare-panel">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Voice match</span></div>
<div class="opt-group-subtitle">Compare the saved WAV with a fresh TTS synthesis.</div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field wide"><label>Comparison backend</label><select class="opt-compare-backend">${styleBackendOptions("voice_clone")}</select></div>
<button class="btn-secondary opt-play-reference">Play WAV file</button>
<button class="btn-primary opt-synth-reference">Synthesize reference text</button>
</div>
<p class="opt-group-note">Compare the saved reference WAV with a fresh synthesis of the same reference text. Restart TTS first after editing a voice, otherwise the backend may still use a cached version.</p>
<div class="opt-compare-grid">
<div class="opt-compare-card">
<strong>WAV file</strong>
<audio class="opt-compare-ref-audio" controls></audio>
</div>
<div class="opt-compare-card">
<strong>Synthesized reference text</strong>
<audio class="opt-compare-synth-audio" controls></audio>
</div>
</div>
</div>
</div>
${isClone?"":`<div class="opt-group opt-style-panel">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Style variation</span></div>
<div class="opt-group-subtitle">Create a styled variant and save it as a new voice (CustomVoice only).</div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field wide"><label>Style instruction</label><input class="opt-style-instruct" type="text" placeholder="cheerful, calm, excited"></div>
<div class="opt-field wide"><label>Style backend</label><select class="opt-style-backend">${styleBackendOptions("customvoice",!0)}</select></div>
<div class="opt-field wide"><label>New voice ID</label><input class="opt-style-voice-id" type="text" placeholder="DE_F_Amala_happy"></div>
</div>
<p class="opt-group-note">Preview first. Saving creates a new active WAV voice from the current reference text. Same-voice style only works when the selected backend knows this voice and honors <code>instruct</code>; CustomVoice is style-aware; Base/Streaming are fastest but often ignore style.</p>
<div class="backend-help opt-style-backend-help"></div>
<div class="opt-style-preview-box">
<div class="opt-controls">
<button class="btn-secondary opt-preview-style">Preview style</button>
</div>
<audio class="opt-style-audio" controls></audio>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-style">Save style variation</button>
</div>
</div>
</div>`}
<div class="opt-group opt-maintenance open">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Loudness</span></div>
<div class="opt-group-subtitle">Normalize the volume of the reference audio file.</div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field"><label>Target dBFS</label><input class="opt-target-db" type="number" step="0.5" value="-20"></div>
<button class="btn-secondary opt-db-minus">-</button>
<button class="btn-secondary opt-db-plus">+</button>
<button class="btn-secondary opt-db-auto">Auto</button>
<button class="btn-secondary opt-restart-tts" title="Restarts the WAV-loading TTS backends (Voice Clone + Streaming) so they rescan the active_voices folder">Restart WAV engines</button>
<button class="btn-secondary opt-rebenchmark">Rebenchmark this one</button>
<span class="opt-restart-note" hidden>Restart TTS before benchmarking.</span>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-volume">Save volume</button>
</div>
</div>
</div>
<div class="opt-status optimizer-workflow">Click the pencil to load waveform and tools.</div>
</div>
</div>
`;const vrLengthEl=wrap.querySelector(".vr-length");hydrateVoiceDuration(v,vrLengthEl);const photoCell=wrap.querySelector(".vr-photo"),photoInput=wrap.querySelector(".photo-input"),updatePhotoImg=()=>{const ts=Date.now(),imgSrc=`/api/voice/picture/${encodeURIComponent(v.id)}?t=${ts}`,img=document.createElement("img");img.src=imgSrc,img.alt="",photoCell.innerHTML="",photoCell.appendChild(img),photoCell.appendChild(photoInput);const compactAvatar=wrap.querySelector(".vl-avatar");compactAvatar&&(compactAvatar.className="vl-avatar vl-avatar-photo",compactAvatar.style.background="",compactAvatar.innerHTML=`<img src="${imgSrc}" alt="" class="vl-avatar-img">`);const inspectorAvatar=document.querySelector(".inspector-avatar");inspectorAvatar&&wrap.classList.contains("vr-selected")&&(inspectorAvatar.classList.add("insp-avatar-photo"),inspectorAvatar.style.background="",inspectorAvatar.innerHTML=`<img src="${imgSrc}" alt="" class="insp-avatar-img">`),v.has_picture=!0};photoCell.addEventListener("update-photo",updatePhotoImg),photoCell.addEventListener("click",()=>photoInput.click()),photoInput.addEventListener("change",async()=>{if(!photoInput.files.length)return;const fd=new FormData;fd.append("voice_id",v.id),fd.append("file",photoInput.files[0]);try{const r=await fetch("/api/voice/picture",{method:"POST",body:fd});if(!r.ok)throw new Error((await r.json()).detail);updatePhotoImg(),toast("Photo uploaded","success")}catch(e){toast("Photo upload failed: "+e.message,"error")}}),photoCell.addEventListener("dragenter",e=>{e.preventDefault(),photoCell.classList.add("drag-over")}),photoCell.addEventListener("dragover",e=>{e.preventDefault(),photoCell.classList.add("drag-over")}),photoCell.addEventListener("dragleave",()=>photoCell.classList.remove("drag-over")),photoCell.addEventListener("drop",async e=>{if(e.preventDefault(),photoCell.classList.remove("drag-over"),e.dataTransfer.files&&e.dataTransfer.files.length>0){photoInput.files=e.dataTransfer.files,photoInput.dispatchEvent(new Event("change"));return}let url=e.dataTransfer.getData("text/uri-list");if(!url){const html=e.dataTransfer.getData("text/html");if(html){const match=html.match(/src=["'](.*?)["']/);match&&(url=match[1])}}if(url||(url=e.dataTransfer.getData("text/plain")),url&&/^https?:\/\//i.test(url)){status("Downloading picture from URL...");try{const r=await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,image_url:url})});if(!r.ok)throw new Error((await r.json()).detail);updatePhotoImg(),toast("Photo saved from URL","success"),status("Photo saved successfully")}catch(err){toast("Photo URL download failed: "+err.message,"error"),status("Photo URL download failed")}}});const normalizeBtn=wrap.querySelector(".normalize-voice-btn"),dbValue=wrap.querySelector(".vr-db-value"),dbCell=wrap.querySelector(".vr-db");normalizeBtn.addEventListener("click",async()=>{var _a3;const target=libraryTargetDb();if(confirm(`Normalize "${v.id}" to ${target} dBFS?`)){normalizeBtn.disabled=!0,status("Normalizing "+v.id+"\u2026");try{const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:target})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a3=d.duration)!=null?_a3:v.duration,v.file_type=d.file_type||v.file_type,v.path=d.path||v.path,v.needs_tts_restart=!0,markVoiceAudioChanged(v),dbValue.textContent=fmtDbfs(v),dbCell.title=v.loudness?`avg ${fmtDbfs(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",vrLengthEl&&(vrLengthEl.textContent=fmtDuration(v.duration)),toast("Normalized: "+v.id,"success"),status(`Normalized ${v.id} to ${target} dBFS. Restart TTS before rebenchmarking.`)}catch(e){toast("Normalize failed: "+e.message,"error"),status("Normalize failed")}finally{normalizeBtn.disabled=!1}}});const flagEmojiEl=wrap.querySelector(".flag-emoji"),flagCodeEl=wrap.querySelector(".flag-code"),flagPicker=wrap.querySelector(".flag-picker");wrap.querySelector(".vr-flag").addEventListener("click",e=>{e.stopPropagation(),document.querySelectorAll(".flag-picker.open").forEach(fp=>{fp!==flagPicker&&fp.classList.remove("open")}),flagPicker.classList.toggle("open")}),flagPicker.querySelectorAll(".flag-opt").forEach(opt=>{opt.addEventListener("click",async e=>{e.stopPropagation();const cc=opt.dataset.cc;flagPicker.classList.remove("open"),flagEmojiEl.textContent=cc2flag(cc),flagCodeEl.textContent=ccDisplay(cc),flagPicker.querySelectorAll(".flag-opt").forEach(o=>o.classList.toggle("active",o.dataset.cc===cc)),v.flag=cc,await saveMeta(v.id,{flag:cc})})});const gBadge=wrap.querySelector(".gender-badge");gBadge.addEventListener("click",async()=>{const cycle=["F","M","N"];v.gender=cycle[(cycle.indexOf(v.gender||"F")+1)%3],gBadge.innerHTML=`<span class="gender-sym">${genderMap[v.gender]||"?"}</span><span class="gender-txt">${genderLabel[v.gender]||"\u2014"}</span>`,gBadge.className="gender-badge "+genderClass[v.gender],await saveMeta(v.id,{gender:v.gender})});const nameText=wrap.querySelector(".vr-name-text"),renameConf=wrap.querySelector(".rename-confirm"),nameInput=wrap.querySelector(".vr-name-input"),renameOk=wrap.querySelector(".rename-ok"),renameCancel=wrap.querySelector(".rename-cancel"),startRename=()=>{nameText.style.display="none",renameConf.classList.add("show"),nameInput.focus(),nameInput.select()};nameText.addEventListener("dblclick",startRename);const cancelRename=()=>{nameText.style.display="",renameConf.classList.remove("show"),nameInput.value=v.id};renameCancel.addEventListener("click",cancelRename);const doRename=async()=>{const newId=nameInput.value.trim();if(!newId||newId===v.id){cancelRename();return}if(!/^[A-Za-z0-9_\-\.]+$/.test(newId)){toast("Invalid characters in name","error");return}try{const r=await fetch("/api/voice/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({old_id:v.id,new_id:newId})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();v.id=newId,d.path&&(v.path=d.path),d.file_type&&(v.file_type=d.file_type),nameText.textContent=newId,nameText.title=newId,nameText.style.display="",renameConf.classList.remove("show"),wrap.dataset.id=newId,nameInput.value=newId,toast("Renamed to "+newId,"success")}catch(e){toast("Rename failed: "+e.message,"error")}};renameOk.addEventListener("click",doRename),nameInput.addEventListener("keydown",e=>{e.key==="Enter"&&doRename(),e.key==="Escape"&&cancelRename()}),wrap.querySelector(".vl-compact").addEventListener("click",function(e){e.target.closest("button")||selectVoice(wrap)});const editAudioBtn=wrap.querySelector(".edit-audio-btn"),optPanel=wrap.querySelector(".vr-optimizer"),vrTypeEl=wrap.querySelector(".vr-type"),optCanvas=wrap.querySelector(".opt-wave"),optStart=wrap.querySelector(".opt-start"),optEnd=wrap.querySelector(".opt-end"),optTranscript=wrap.querySelector(".opt-transcript"),optTargetDb=wrap.querySelector(".opt-target-db"),optStyleInstruct=wrap.querySelector(".opt-style-instruct"),optStyleBackend=wrap.querySelector(".opt-style-backend"),optStyleVoiceId=wrap.querySelector(".opt-style-voice-id"),optCompareBackend=wrap.querySelector(".opt-compare-backend"),optPlayReferenceBtn=wrap.querySelector(".opt-play-reference"),optSynthReferenceBtn=wrap.querySelector(".opt-synth-reference"),optCompareRefAudio=wrap.querySelector(".opt-compare-ref-audio"),optCompareSynthAudio=wrap.querySelector(".opt-compare-synth-audio"),optPreviewStyleBtn=wrap.querySelector(".opt-preview-style"),optSaveStyleBtn=wrap.querySelector(".opt-save-style"),optStyleAudio=wrap.querySelector(".opt-style-audio"),optStatus=wrap.querySelector(".opt-status"),optSaveTextBtn=wrap.querySelector(".opt-save-text"),optRestartTtsBtn=wrap.querySelector(".opt-restart-tts"),optRebenchmarkBtn=wrap.querySelector(".opt-rebenchmark"),optRestartNote=wrap.querySelector(".opt-restart-note");let optState={loaded:!1,id:null,duration:0,buffer:null,audio:null,compareSynthUrl:null};const setOptStatus=msg=>{optStatus.textContent=msg,status(msg)},setVoiceRestartState=(required,msg="")=>{v.needs_tts_restart=required,optPanel.classList.toggle("opt-restart-needed",required),optRestartNote.hidden=!required,optRestartNote.textContent=required?"Restart TTS before benchmarking; the backend may still have the old voice cached.":"",optRebenchmarkBtn.title=required?"Restart TTS first, otherwise the benchmark may use a cached voice":"Benchmark this voice",benchmarkOneBtn.title=required?"Restart TTS first, otherwise the benchmark may use a cached voice":"Benchmark this voice",msg&&setOptStatus(msg)},markTtsRestartRequired=msg=>setVoiceRestartState(!0,msg),refreshOptimizerFromVoice=async()=>{var _a3;markVoiceAudioChanged(v),optState.loaded=!1,optState.buffer=null,optState.id=null,await loadOptimizer(),vrLengthEl&&(vrLengthEl.textContent=fmtDuration(v.duration)),vrLengthEl&&(vrLengthEl.title=String((_a3=v.duration)!=null?_a3:"")),dbValue.textContent=fmtDbfs(v),dbCell.title=v.loudness?`avg ${fmtDbfs(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:""},saveOptimizerText=async()=>{const transcript=optTranscript.value.trim();setOptStatus("Saving reference text..."),v.transcript=transcript,refInput.value=transcript,refInput.title=transcript,refTranscribeBtn.style.display=transcript?"none":"",await saveMeta(v.id,{transcript}),markTtsRestartRequired("Reference text saved. Restart TTS before rebenchmarking."),toast("Reference text saved: "+v.id,"success")},redrawOpt=()=>{!optState.buffer||optCanvas.clientWidth<4||drawOptimizerWave(optCanvas,optState.buffer,parseFloat(optStart.value)||0,parseFloat(optEnd.value)||optState.duration)};new ResizeObserver(()=>redrawOpt()).observe(optCanvas);const syncCompareReferenceAudio=()=>{if(!optState.id||!optCompareRefAudio)return;const src="/api/audio/"+optState.id;optCompareRefAudio.src.endsWith(src)||(optCompareRefAudio.src=src,optCompareRefAudio.load());const stopAtEnd=()=>{const end=parseFloat(optEnd.value)||optState.duration;optCompareRefAudio.currentTime>=end&&optCompareRefAudio.pause()};optCompareRefAudio.ontimeupdate=stopAtEnd},optWaveTimeFromEvent=e=>{const rect=optCanvas.getBoundingClientRect(),x=Math.max(0,Math.min(rect.width,e.clientX-rect.left));return optState.duration?x/Math.max(1,rect.width)*optState.duration:0},setOptCropRange=(start,end)=>{start=Math.max(0,Math.min(optState.duration||0,Number(start)||0)),end=Math.max(0,Math.min(optState.duration||0,Number(end)||0)),end<start&&([start,end]=[end,start]),optStart.value=start.toFixed(2),optEnd.value=end.toFixed(2),redrawOpt()},optWaveSelectionPixels=e=>{const rect=optCanvas.getBoundingClientRect(),duration=Math.max(.01,optState.duration||.01),sx=(parseFloat(optStart.value)||0)/duration*rect.width,ex=(parseFloat(optEnd.value)||optState.duration||0)/duration*rect.width;return{x:e.clientX-rect.left,sx,ex}},optWaveDragMode=e=>{const{x,sx,ex}=optWaveSelectionPixels(e),hit=18,nearStart=Math.abs(x-sx)<=hit,nearEnd=Math.abs(x-ex)<=hit;return nearStart&&nearEnd?Math.abs(x-sx)<=Math.abs(x-ex)?"start":"end":nearStart?"start":nearEnd?"end":x>sx&&x<ex?"move":"new"};(()=>{let drag=null;optCanvas.addEventListener("pointerdown",e=>{var _a3;if(!optState.buffer||!optState.duration)return;e.preventDefault(),(_a3=optCanvas.setPointerCapture)==null||_a3.call(optCanvas,e.pointerId);const mode=optWaveDragMode(e),currentStart=parseFloat(optStart.value)||0,currentEnd=parseFloat(optEnd.value)||optState.duration;drag={mode,anchor:optWaveTimeFromEvent(e),start:currentStart,end:currentEnd,length:Math.max(.05,currentEnd-currentStart)},optCanvas.style.cursor=mode==="move"?"grabbing":"ew-resize",mode==="new"&&setOptCropRange(drag.anchor,drag.anchor)}),optCanvas.addEventListener("pointermove",e=>{if(!optState.buffer||!optState.duration)return;if(!drag){const mode=optWaveDragMode(e);optCanvas.style.cursor=mode==="move"?"grab":mode==="start"||mode==="end"?"ew-resize":"crosshair";return}e.preventDefault();const t=optWaveTimeFromEvent(e);if(drag.mode==="start")setOptCropRange(Math.min(t,drag.end-.05),drag.end);else if(drag.mode==="end")setOptCropRange(drag.start,Math.max(t,drag.start+.05));else if(drag.mode==="move"){let start=t-(drag.anchor-drag.start);start=Math.max(0,Math.min((optState.duration||0)-drag.length,start)),setOptCropRange(start,start+drag.length)}else setOptCropRange(drag.anchor,t)});const finish=e=>{var _a3;drag&&((_a3=optCanvas.releasePointerCapture)==null||_a3.call(optCanvas,e.pointerId),drag=null,optCanvas.style.cursor="crosshair")};optCanvas.addEventListener("pointerup",finish),optCanvas.addEventListener("pointercancel",finish),optCanvas.addEventListener("pointerleave",()=>{drag||(optCanvas.style.cursor="crosshair")})})();const loadOptimizer=async()=>{if(optState.loaded)return;setOptStatus("Loading voice optimizer\u2026");const d=await loadLibraryVoiceAudio(v);optState.id=d.id,optState.duration=d.duration,v.duration=d.duration,optState.buffer=await decodeVoiceAudio(v),optState.loaded=!0,optStart.value="0.00",optEnd.value=d.duration.toFixed(2),optEnd.max=d.duration.toFixed(2),optTranscript.value=d.transcript||v.transcript||"",redrawOpt(),syncCompareReferenceAudio(),setVoiceRestartState(!!v.needs_tts_restart),setOptStatus(v.needs_tts_restart?"Optimizer ready. Restart TTS before benchmarking this edit.":"Optimizer ready")};if(wrap._loadOptimizer=loadOptimizer,wrap._redrawOpt=redrawOpt,editAudioBtn.addEventListener("click",async()=>{editAudioBtn.disabled=!0;try{const opening=!wrap.classList.contains("edit-open");document.querySelectorAll(".vl-row.edit-open").forEach(r=>{r!==wrap&&r.classList.remove("edit-open")}),wrap.classList.toggle("edit-open",opening),opening&&(await loadOptimizer(),wrap.scrollIntoView({behavior:"smooth",block:"nearest"}))}catch(e){toast("Edit load failed: "+e.message,"error"),status("Edit load failed")}finally{editAudioBtn.disabled=!1}}),[optStart,optEnd].forEach(inp=>inp.addEventListener("input",()=>{redrawOpt(),syncCompareReferenceAudio()})),optStyleInstruct==null||optStyleInstruct.addEventListener("input",()=>{optStyleVoiceId.value.trim()||(optStyleVoiceId.value=suggestedStyleVoiceId(v.id,optStyleInstruct.value))}),optStyleBackend==null||optStyleBackend.addEventListener("change",()=>updateStyleBackendHelp(wrap)),optCompareBackend.addEventListener("change",()=>{var _a3;return setOptStatus(`Comparison backend: ${((_a3=optCompareBackend.options[optCompareBackend.selectedIndex])==null?void 0:_a3.textContent)||optCompareBackend.value}`)}),optCompareBackend.value===""&&(optCompareBackend.innerHTML=styleBackendOptions("voice_clone"),optCompareBackend.disabled=!availableTtsBackends().length),optStyleBackend&&updateStyleBackendHelp(wrap),wrap.querySelector(".opt-db-minus").addEventListener("click",()=>{optTargetDb.value=(Number(optTargetDb.value||-20)-1).toFixed(1)}),wrap.querySelector(".opt-db-plus").addEventListener("click",()=>{optTargetDb.value=(Number(optTargetDb.value||-20)+1).toFixed(1)}),wrap.querySelector(".opt-db-auto").addEventListener("click",()=>{optTargetDb.value="-20.0"}),wrap.querySelector(".opt-play").addEventListener("click",async()=>{try{await loadOptimizer(),optState.audio&&optState.audio.pause(),optState.audio=new Audio("/api/audio/"+optState.id),optState.audio.currentTime=parseFloat(optStart.value)||0;const end=parseFloat(optEnd.value)||optState.duration;optState.audio.ontimeupdate=()=>{optState.audio.currentTime>=end&&optState.audio.pause()},optState.audio.play()}catch(e){toast("Preview failed: "+e.message,"error")}}),optPlayReferenceBtn.addEventListener("click",async()=>{try{await loadOptimizer(),syncCompareReferenceAudio(),optCompareRefAudio.currentTime=parseFloat(optStart.value)||0,await optCompareRefAudio.play().catch(()=>{}),setOptStatus("Playing reference WAV selection for comparison.")}catch(e){toast("Reference playback failed: "+e.message,"error")}}),optSynthReferenceBtn.addEventListener("click",async()=>{const text=optTranscript.value.trim();if(!text){toast("Enter reference text first","error"),optTranscript.focus();return}if(v.needs_tts_restart){if(!confirm("This voice is still marked as needing a TTS restart. If you already restarted TTS manually, clear the restart flags and synthesize now?")){setOptStatus("Restart TTS before synthesizing this comparison, or clear the flag after a manual restart.");return}try{const d=await clearTtsRestartFlags();setVoiceRestartState(!1,`Restart flags cleared (${d.cleared_restart_flags||0}). Synthesizing comparison...`),toast("Restart flags cleared","success")}catch(e){toast("Could not clear restart flags: "+e.message,"error"),setOptStatus("Could not clear restart flags");return}}optSynthReferenceBtn.disabled=!0;try{await loadOptimizer(),setOptStatus("Synthesizing reference text for comparison...");const source=await createTtsAudioSource(v.id,text,optCompareBackend.value,"settings","");optState.compareSynthUrl&&URL.revokeObjectURL(optState.compareSynthUrl),optCompareSynthAudio.src=source.url,optState.compareSynthUrl=source.streaming?null:source.url,await optCompareSynthAudio.play().catch(()=>{}),setOptStatus(source.streaming?"Streaming synthesized comparison.":"Synthesized comparison ready.")}catch(e){toast("Synthesis comparison failed: "+e.message,"error"),setOptStatus("Synthesis comparison failed")}finally{optSynthReferenceBtn.disabled=!1}}),wrap.querySelector(".opt-auto-trim").addEventListener("click",async()=>{try{await loadOptimizer();const d=await clientAutoTrimBounds(optState.id);optStart.value=Number(d.start).toFixed(2),optEnd.value=Number(d.end).toFixed(2),redrawOpt(),setOptStatus(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error")}}),wrap.querySelector(".opt-recognize").addEventListener("click",async()=>{try{await loadOptimizer();const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:optState.id})});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();optTranscript.value=d.text||"",setOptStatus("Reference text recognised. Review it, then Save text.")}catch(e){toast("Recognition failed: "+e.message,"error")}}),optSaveTextBtn.addEventListener("click",async()=>{optSaveTextBtn.disabled=!0;try{await saveOptimizerText()}catch(e){toast("Save text failed: "+e.message,"error"),setOptStatus("Save text failed")}finally{optSaveTextBtn.disabled=!1}}),wrap.querySelector(".opt-save-crop").addEventListener("click",async()=>{var _a3;try{await loadOptimizer();const cropStart=Math.max(0,parseFloat(optStart.value)||0),cropEnd=Math.min(optState.duration,parseFloat(optEnd.value)||optState.duration);if(cropStart<=.01&&cropEnd>=optState.duration-.05){setOptStatus("No crop range selected. Adjust Start or End first, then Save crop."),toast("No crop range selected","error");return}if(cropEnd<=cropStart+.1){setOptStatus("Crop range is too short."),toast("Crop range is too short","error");return}setOptStatus(`Saving crop ${cropStart.toFixed(2)}s -> ${cropEnd.toFixed(2)}s...`);const pr=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:optState.id,start:cropStart,end:cropEnd})});if(!pr.ok){const e=await pr.json();throw new Error(e.detail||pr.statusText)}const p=await pr.json(),rr=await fetch("/api/voice-replace",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:p.id,voice_id:v.id,path:v.path,transcript:optTranscript.value})});if(!rr.ok){const e=await rr.json().catch(()=>({}));throw new Error(e.detail||rr.statusText)}const saved=await rr.json();v.transcript=optTranscript.value,v.duration=(_a3=saved.duration)!=null?_a3:p.duration,saved.loudness&&(v.loudness=saved.loudness),saved.path&&(v.path=saved.path),saved.file_type&&(v.file_type=saved.file_type),markVoiceAudioChanged(v),refInput.value=v.transcript,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",vrTypeEl&&(vrTypeEl.textContent=voiceFileType(v).toUpperCase(),vrTypeEl.title=voiceFileType(v)),await refreshOptimizerFromVoice(),toast("Voice crop saved: "+v.id,"success"),markTtsRestartRequired(saved.backup?"Crop saved and loaded. Restart TTS before rebenchmarking; undo is available.":"Crop saved and loaded. Restart TTS before rebenchmarking.")}catch(e){toast("Save crop failed: "+e.message,"error"),setOptStatus("Save crop failed")}}),optStyleInstruct){const styleVariationInput=()=>{const style=optStyleInstruct.value.trim(),text=optTranscript.value.trim()||benchmarkSampleText(),newId=optStyleVoiceId.value.trim()||suggestedStyleVoiceId(v.id,style);return style?text?/^[A-Za-z0-9_\-.]+$/.test(newId)?{style,text,newId,backend:optStyleBackend.value}:(toast("Invalid characters in new voice ID","error"),optStyleVoiceId.focus(),null):(toast("Enter reference text first","error"),optTranscript.focus(),null):(toast("Enter a style instruction first","error"),optStyleInstruct.focus(),null)};optPreviewStyleBtn.addEventListener("click",async()=>{const input=styleVariationInput();if(input){optPreviewStyleBtn.disabled=!0;try{setOptStatus("Synthesizing style preview...");const blob=await fetchTtsPreviewBlob(v.id,input.text,"wav",input.style,input.backend);optStyleAudio.src&&URL.revokeObjectURL(optStyleAudio.src),optStyleAudio.src=URL.createObjectURL(blob),optStyleAudio.style.display="",await optStyleAudio.play().catch(()=>{}),setOptStatus("Style preview ready. If it sounds right, save it as a new voice.")}catch(e){toast("Style preview failed: "+e.message,"error"),setOptStatus("Style preview failed")}finally{optPreviewStyleBtn.disabled=!1}}}),optSaveStyleBtn.addEventListener("click",async()=>{const input=styleVariationInput();if(input){optSaveStyleBtn.disabled=!0;try{setOptStatus(`Synthesizing style variation ${input.newId}...`);const r=await fetch("/api/tts-style-variation",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({source_voice:v.id,voice_id:input.newId,text:input.text,instruct:input.style,backend:input.backend})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();toast("Style variation saved: "+d.voice_id,"success"),setOptStatus("Style variation saved. Restart TTS so the backend scans the new voice."),await loadVoiceLibrary(),renderIntegrationSnippets()}catch(e){toast("Style variation failed: "+e.message,"error"),setOptStatus("Style variation failed")}finally{optSaveStyleBtn.disabled=!1}}})}wrap.querySelector(".opt-undo").addEventListener("click",async()=>{var _a3;if(confirm(`Restore the original backup for "${v.id}"?`))try{setOptStatus("Restoring original\u2026");const r=await fetch("/api/voice/undo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.duration=(_a3=d.duration)!=null?_a3:v.duration,v.loudness=d.loudness||v.loudness,v.path=d.path||v.path,v.file_type=d.file_type||v.file_type,markVoiceAudioChanged(v),vrTypeEl&&(vrTypeEl.textContent=voiceFileType(v).toUpperCase(),vrTypeEl.title=voiceFileType(v)),await refreshOptimizerFromVoice(),toast("Original restored: "+v.id,"success"),markTtsRestartRequired("Original restored. Restart TTS before rebenchmarking.")}catch(e){toast("Undo failed: "+e.message,"error"),setOptStatus("Undo failed")}}),wrap.querySelector(".opt-save-volume").addEventListener("click",async()=>{var _a3;try{setOptStatus("Saving volume\u2026");const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:Number(optTargetDb.value||-20)})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a3=d.duration)!=null?_a3:v.duration,d.path&&(v.path=d.path),d.file_type&&(v.file_type=d.file_type),markVoiceAudioChanged(v),dbValue.textContent=fmtDbfs(v),dbCell.title=v.loudness?`avg ${fmtDbfs(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",await refreshOptimizerFromVoice(),toast("Volume saved: "+v.id,"success"),markTtsRestartRequired("Volume saved. Restart TTS before rebenchmarking this voice.")}catch(e){toast("Volume save failed: "+e.message,"error"),setOptStatus("Volume save failed")}}),optRestartTtsBtn.addEventListener("click",async()=>{optRestartTtsBtn.disabled=!0;try{setOptStatus("Restarting WAV backends (Voice Clone + Streaming)\u2026");const r=await fetch("/api/tts/restart",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_voices.forEach(voice=>{voice.needs_tts_restart=!1}),updateLibraryInsights();const names=(d.restarted||[]).join(", ")||"containers",errTxt=(d.errors||[]).length?` (errors: ${d.errors.join("; ")})`:"";setVoiceRestartState(!1,`Restarted: ${names}${errTxt}. Rebenchmark now uses the edited voice.`),toast(`TTS restarted: ${names}`,"success")}catch(e){toast("Restart TTS failed: "+e.message,"error"),setOptStatus("Restart TTS failed: "+e.message)}finally{optRestartTtsBtn.disabled=!1}});const refInput=wrap.querySelector(".vr-ref input"),refTranscribeBtn=wrap.querySelector(".ref-transcribe-btn");refInput.addEventListener("input",debounce(async()=>{v.transcript=refInput.value,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",await saveMeta(v.id,{transcript:v.transcript}),wrap.classList.contains("edit-open")?markTtsRestartRequired("Reference text saved. Restart TTS before rebenchmarking."):v.needs_tts_restart=!0},800)),refTranscribeBtn.addEventListener("click",async()=>{refTranscribeBtn.disabled=!0;try{const d=await loadLibraryVoiceAudio(v),tr=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:d.id})});if(!tr.ok){const e=await tr.json();throw new Error(e.detail)}const text=await tr.json();v.transcript=text.text||"",refInput.value=v.transcript,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",await saveMeta(v.id,{transcript:v.transcript}),v.needs_tts_restart=!0,toast("Reference text recognised; restart TTS before benchmarking","success")}catch(e){toast("Recognition failed: "+e.message,"error")}finally{refTranscribeBtn.disabled=!1}});const noteInput=wrap.querySelector(".vr-note input");noteInput.addEventListener("input",debounce(async()=>{v.note=noteInput.value,await saveMeta(v.id,{note:v.note})},800));const sourceInput=wrap.querySelector(".vr-source input");sourceInput.addEventListener("input",debounce(async()=>{v.origin=sourceInput.value.trim(),await saveMeta(v.id,{origin:v.origin});const tblCell=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"] .vl-tbl-source`);tblCell&&(tblCell.textContent=_displaySource(v)||"-")},800));const starSpans=wrap.querySelectorAll(".star");starSpans.forEach(s=>{s.addEventListener("click",async()=>{const val=parseInt(s.dataset.val),newRating=val===v.rating?0:val;v.rating=newRating,starSpans.forEach((ss,i)=>ss.classList.toggle("on",i<newRating)),await saveMeta(v.id,{rating:newRating})}),s.addEventListener("mouseenter",()=>{const val=parseInt(s.dataset.val);starSpans.forEach((ss,i)=>ss.classList.toggle("on",i<val))}),s.addEventListener("mouseleave",()=>{starSpans.forEach((ss,i)=>ss.classList.toggle("on",i<(v.rating||0)))})});const benchmarkOneBtn=wrap.querySelector(".benchmark-one-btn"),benchmarkThisVoice=async triggerBtn=>{if(v.needs_tts_restart&&!confirm("This voice changed since the last TTS restart. Benchmarking now may use the cached old voice. Continue anyway?")){setOptStatus("Restart TTS first, then rebenchmark this voice.");return}triggerBtn.disabled=!0,status("Benchmarking "+v.id+"...");try{setBenchmarkProgress(0,1,`Benchmarking ${v.id}`);const d=await runVoiceBenchmark(v.id);mergeBenchmarkResults(d);const hit=(d.voices||[]).find(x=>x.voice_id===v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark);const benchCell=wrap.querySelector(".vr-bench")||document.querySelector("#voices-inspector .vr-bench");if(benchCell){benchCell.className="vr-bench "+benchmarkClass(v),benchCell.title=benchmarkTitle(v);const benchValue=benchCell.querySelector(".vr-bench-value");benchValue&&(benchValue.textContent=fmtBenchmark(v))}const tblRow=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);if(tblRow){const bc=benchmarkClass(v),btitle=benchmarkTitle(v),tblDur=tblRow.querySelector(".vl-tbl-dur"),tblFactor=tblRow.querySelector(".vl-tbl-factor"),tblTime=tblRow.querySelector(".vl-tbl-time"),tblWpm=tblRow.querySelector(".vl-tbl-wpm");if(tblDur&&(tblDur.textContent=fmtBenchmarkAudio(v),tblDur.title=`${fmtBenchmarkAudio(v)} \u2014 length of synthesised benchmark audio`),tblFactor&&(tblFactor.textContent=fmtFactor(v),tblFactor.className=`vl-tbl-factor ${bc}`,tblFactor.title=btitle),tblTime&&(tblTime.textContent=fmtElapsed(v),tblTime.className=`vl-tbl-time ${bc}`,tblTime.title=btitle),tblWpm){const wpm=voiceWpm(v);tblWpm.textContent=fmtWpm(v),tblWpm.title=wpm!=null?`${wpm} wpm \u2014 130\u2013180 wpm is natural for long listening`:""}}setBenchmarkProgress(1,1,`Finished ${v.id}`),toast("Benchmarked "+v.id,"success"),setVoiceRestartState(!1,"Benchmark saved for "+v.id)}catch(e){toast("Benchmark failed: "+e.message,"error"),setOptStatus("Benchmark failed")}finally{triggerBtn.disabled=!1}};benchmarkOneBtn.addEventListener("click",()=>benchmarkThisVoice(benchmarkOneBtn)),optRebenchmarkBtn.addEventListener("click",()=>benchmarkThisVoice(optRebenchmarkBtn));const originalPlayBtn=wrap.querySelector(".vr-play-original button"),synthPlayBtn=wrap.querySelector(".vr-play-synth button"),playIcon='<span class="mdi mdi-play"></span>',pauseIcon='<span class="mdi mdi-pause-circle-outline"></span>',generatingIcon='<span class="mdi mdi-timer-sand"></span>';function setLibraryPlayButtonState(btn,state){btn.classList.toggle("is-generating",state==="generating"),btn.innerHTML=state==="playing"?pauseIcon:state==="generating"?generatingIcon:playIcon,btn.title=state==="generating"?"Generating synthesized sample...":state==="playing"?"Pause playback":btn.dataset.playKind==="synth"?"Generate and play synthesized sample":"Play original recording"}async function playLibraryVoice(kind,playBtn){var _a3,_b2,_c2;const bar=$("lib-audio-bar"),audio=$("lib-audio"),playKey=v.id+":"+kind;if(playBtn.dataset.playKind=kind,_activePlayVoiceId===playKey&&!audio.paused){audio.pause(),setLibraryPlayButtonState(playBtn,"idle");return}if(_activePlayVoiceId===playKey&&audio.paused&&audio.src){_activePlayButton=playBtn;try{await audio.play()}catch(e){toast("Play failed: "+e.message,"error")}return}_activePlayButton&&_activePlayButton!==playBtn&&setLibraryPlayButtonState(_activePlayButton,"idle"),_activePlayButton=playBtn,_activePlayVoiceId=playKey,_activePlayUrl&&(URL.revokeObjectURL(_activePlayUrl),_activePlayUrl=null),kind==="synth"&&setLibraryPlayButtonState(playBtn,"generating"),playBtn.disabled=!0;try{if(kind==="synth"){v.needs_tts_restart&&toast("This voice changed since backend refresh; synthesized playback may use a cached voice.","error");const synthMode=((_a3=document.querySelector("#vl-synth-mode-seg .vl-synth-seg-btn.active"))==null?void 0:_a3.dataset.mode)||"preview",text=synthMode==="transcript"&&((_b2=v.transcript)==null?void 0:_b2.trim())||benchmarkSampleText(),textLabel=synthMode==="transcript"?"reference transcript":"preview text",backend=isClone?libraryTtsBackend():"voice_design",source=await createTtsAudioSource(v.id,text,backend,"settings","");audio.src=source.url,source.streaming||(_activePlayUrl=source.url),$("lib-audio-label").textContent=v.id+" \xB7 synthesized "+textLabel+" \xB7 "+(((_c2=backendById(backend))==null?void 0:_c2.label)||backend)}else audio.src=voiceFileUrl(v),$("lib-audio-label").textContent=v.id+" \xB7 original recording";bar.style.display="",audio.onended=()=>{setLibraryPlayButtonState(playBtn,"idle"),_activePlayVoiceId=null},audio.onpause=()=>{_activePlayButton===playBtn&&setLibraryPlayButtonState(playBtn,"idle")},audio.onplay=()=>{setLibraryPlayButtonState(playBtn,"playing")},await audio.play()}catch(e){setLibraryPlayButtonState(playBtn,"idle"),toast("Play failed: "+e.message,"error")}finally{playBtn.disabled=!1}}originalPlayBtn.dataset.playKind="original",synthPlayBtn.dataset.playKind="synth",setLibraryPlayButtonState(originalPlayBtn,"idle"),setLibraryPlayButtonState(synthPlayBtn,"idle"),originalPlayBtn.addEventListener("click",()=>playLibraryVoice("original",originalPlayBtn)),synthPlayBtn.addEventListener("click",()=>playLibraryVoice("synth",synthPlayBtn));const toggleCb=wrap.querySelector(".toggle input");toggleCb.addEventListener("change",async()=>{const nextEnabled=toggleCb.checked,previousEnabled=v.enabled!==!1;toggleCb.disabled=!0;try{const saved=await saveMeta(v.id,{enabled:nextEnabled});v.enabled=nextEnabled,saved&&saved.path&&(v.path=saved.path),wrap.classList.toggle("vr-disabled",!v.enabled),toast(nextEnabled?"Moved to active_voices":"Moved to hidden_voices","success"),!v.enabled&&!$("show-disabled-cb").checked&&(wrap.style.transition="opacity .4s",wrap.style.opacity="0",setTimeout(()=>wrap.remove(),400))}catch(e){toggleCb.checked=previousEnabled,v.enabled=previousEnabled,wrap.classList.toggle("vr-disabled",!v.enabled),toast("Move failed: "+e.message,"error")}finally{toggleCb.disabled=!1}});const deleteBtn=wrap.querySelector(".delete-btn"),deleteConfirm=wrap.querySelector(".delete-confirm"),deleteCancelBtn=wrap.querySelector(".delete-confirm-cancel"),deleteGoBtn=wrap.querySelector(".delete-confirm-go"),closeDeleteConfirm=()=>wrap.classList.remove("delete-pending");return deleteBtn.addEventListener("click",e=>{e.stopPropagation(),document.querySelectorAll(".vl-row.delete-pending").forEach(row=>{row!==wrap&&row.classList.remove("delete-pending")}),wrap.classList.add("delete-pending"),deleteGoBtn.focus()}),deleteCancelBtn.addEventListener("click",e=>{e.stopPropagation(),closeDeleteConfirm()}),deleteConfirm.addEventListener("click",e=>e.stopPropagation()),deleteGoBtn.addEventListener("click",async e=>{e.stopPropagation(),deleteGoBtn.disabled=!0,deleteCancelBtn.disabled=!0;try{const r=await fetch(`/api/voice/${encodeURIComponent(v.id)}`,{method:"DELETE"});if(!r.ok){const e2=await r.json();throw new Error(e2.detail)}_voices=_voices.filter(x=>x.id!==v.id),wrap.style.transition="opacity .3s",wrap.style.opacity="0",setTimeout(()=>{wrap.remove(),$("voice-count").textContent=_voices.filter(x=>$("show-disabled-cb").checked||x.enabled!==!1).length+" / "+_voices.length+" voices"},300),toast(`Deleted: ${v.id}`,"success")}catch(e2){toast("Delete failed: "+e2.message,"error"),deleteGoBtn.disabled=!1,deleteCancelBtn.disabled=!1,closeDeleteConfirm()}}),wrap}function renderVoiceGroupsBar(){var _a2;const bar=$("voice-groups-bar");if(!bar)return;const groups={};(_voices||[]).forEach(v=>{const g=(v.group||"").trim();g&&(groups[g]=(groups[g]||0)+1)});const names=Object.keys(groups).sort();if(!names.length){bar.hidden=!0,bar.innerHTML="";return}bar.hidden=!1;const active=window._voiceGroupFilter||"";bar.innerHTML='<span class="vg-label"><span class="mdi mdi-folder-multiple-outline"></span> Groups</span>'+names.map(g=>`<span class="vg-chip${g===active?" active":""}" data-group="${escHtml(g)}">
${escHtml(g)} <span class="vg-count">${groups[g]}</span>
<button class="vg-del" data-group="${escHtml(g)}" title="Delete all ${groups[g]} voices in this group">&times;</button>
</span>`).join("")+(active?'<button class="vg-clear" title="Clear group filter">Show all</button>':""),bar.querySelectorAll(".vg-chip").forEach(chip=>{chip.addEventListener("click",e=>{if(e.target.closest(".vg-del"))return;const g=chip.dataset.group;window._voiceGroupFilter=window._voiceGroupFilter===g?"":g;const groupSel=$("library-filter-group");groupSel&&(groupSel.value=window._voiceGroupFilter||""),renderVoiceList()})}),bar.querySelectorAll(".vg-del").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),deleteVoiceGroup(btn.dataset.group)})}),(_a2=bar.querySelector(".vg-clear"))==null||_a2.addEventListener("click",()=>{window._voiceGroupFilter="";const groupSel=$("library-filter-group");groupSel&&(groupSel.value=""),renderVoiceList()})}async function deleteVoiceGroup(group){const count=(_voices||[]).filter(v=>(v.group||"").trim()===group).length;if(confirm(`Delete all ${count} voice${count!==1?"s":""} in group "${group}"? This cannot be undone.`))try{const r=await fetch("/api/voices/delete-group",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({group})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();window._voiceGroupFilter===group&&(window._voiceGroupFilter=""),toast(`Deleted ${d.count} voice${d.count!==1?"s":""} from "${group}"`,"success"),await loadVoiceLibrary()}catch(e){toast("Delete failed: "+e.message,"error")}}async function saveMeta(voiceId,patch){const r=await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,...patch})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return r.json()}document.addEventListener("click",()=>{document.querySelectorAll(".flag-picker.open").forEach(fp=>fp.classList.remove("open")),document.querySelectorAll(".vl-row.delete-pending").forEach(row=>row.classList.remove("delete-pending"))},{passive:!0});const _bulkSelected=new Set;function _bulkUpdate(){const bar=$("vl-bulk-bar"),count=$("vl-bulk-count"),n=_bulkSelected.size;bar&&(bar.hidden=n===0),count&&(count.textContent=`${n} selected`),document.querySelectorAll(".vl-bulk-cb").forEach(cb=>{cb.checked=_bulkSelected.has(cb.dataset.id)});const headerCb=document.getElementById("vl-select-all-header");if(headerCb){const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id),allSelected=rows.length>0&&rows.every(r=>_bulkSelected.has(r.dataset.id));headerCb.checked=allSelected}}function _bulkToggle(id,checked){checked?_bulkSelected.add(id):_bulkSelected.delete(id),_bulkUpdate()}const _origRenderVoiceList=renderVoiceList;renderVoiceList=function(){_origRenderVoiceList.apply(this,arguments),_bulkInjectCheckboxes()};let _bulkLastClickedId=null;function _bulkInjectCheckboxes(){document.querySelectorAll("#voice-list .vl-row").forEach(row=>{if(row.querySelector(".vl-bulk-cb"))return;const id=row.dataset.id,cb=document.createElement("input");cb.type="checkbox",cb.className="vl-bulk-cb",cb.dataset.id=id,cb.checked=_bulkSelected.has(id),cb.title="Select for bulk edit (shift-click to select a range)",cb.addEventListener("click",e=>{if(e.stopPropagation(),e.shiftKey&&_bulkLastClickedId){const ids=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id).map(r=>r.dataset.id),from=ids.indexOf(_bulkLastClickedId),to=ids.indexOf(id);if(from!==-1&&to!==-1){const[lo,hi]=from<to?[from,to]:[to,from];for(let i=lo;i<=hi;i++)cb.checked?_bulkSelected.add(ids[i]):_bulkSelected.delete(ids[i])}}else _bulkToggle(id,cb.checked);_bulkLastClickedId=id,_bulkUpdate()});const compact=row.querySelector(".vl-compact");compact&&compact.prepend(cb)})}(_I=$("vl-bulk-select-all"))==null||_I.addEventListener("click",()=>{document.querySelectorAll("#voice-list .vl-row").forEach(row=>{row.dataset.id&&_bulkSelected.add(row.dataset.id)}),_bulkUpdate()}),(_J=$("vl-select-all-header"))==null||_J.addEventListener("change",e=>{const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id);e.target.checked?rows.forEach(r=>_bulkSelected.add(r.dataset.id)):rows.forEach(r=>_bulkSelected.delete(r.dataset.id)),_bulkUpdate()}),(_K=$("vl-bulk-deselect"))==null||_K.addEventListener("click",()=>{_bulkSelected.clear(),_bulkUpdate()}),(_L=$("vl-bulk-hide"))==null||_L.addEventListener("click",async()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],r=await _bulkSetEnabled(ids,!1);toast(`Hidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),(_M=$("vl-bulk-unhide"))==null||_M.addEventListener("click",async()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],r=await _bulkSetEnabled(ids,!0);toast(`Unhidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),(_N=$("vl-bulk-tag"))==null||_N.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-tag-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-tag-inp",inp.placeholder="tag1, tag2\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML='<span class="mdi mdi-check"></span> Apply',applyBtn.addEventListener("click",async()=>{const newTags=inp.value.split(",").map(t=>t.trim()).filter(Boolean);let done=0;for(const id of ids){const v=(window._voices||[]).find(vv=>vv.id===id),merged=String((v==null?void 0:v.tag)||"").split(",").map(t=>t.trim()).filter(Boolean).slice();for(const t of newTags)merged.some(e=>e.toLowerCase()===t.toLowerCase())||merged.push(t);await saveMeta(id,{tag:merged.join(", ")}).catch(()=>{}),done++}toast(`Tag added on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_O=$("vl-bulk-source"))==null||_O.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-source-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-source-inp",inp.placeholder="e.g. fish-audio, cloned\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML='<span class="mdi mdi-check"></span> Apply',applyBtn.addEventListener("click",async()=>{const val=inp.value.trim();let done=0;for(const id of ids)await saveMeta(id,{origin:val}).catch(()=>{}),done++;toast(`Source set on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_P=$("vl-bulk-rating"))==null||_P.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let sel=bar.querySelector(".vl-bulk-rating-sel");sel||(sel=document.createElement("select"),sel.className="vl-bulk-rating-sel",sel.innerHTML='<option value="">\u2B50 Pick rating\u2026</option>'+[1,2,3,4,5].map(n=>`<option value="${n}">${"\u2605".repeat(n)}</option>`).join(""),sel.addEventListener("change",async()=>{const rating=Number(sel.value);if(!rating)return;let done=0;for(const id of ids)await saveMeta(id,{rating}).catch(()=>{}),done++;toast(`Rated ${done} voice${done!==1?"s":""}`,"success"),sel.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(sel),sel.focus())}),(_Q=$("vl-bulk-delete"))==null||_Q.addEventListener("click",()=>{_bulkSelected.size&&_showBulkDeleteConfirm([..._bulkSelected])});function _showBulkDeleteConfirm(ids){var _a2;(_a2=document.querySelector(".vl-bdc-overlay"))==null||_a2.remove();const plural=ids.length!==1?"s":"",names=ids.slice(0,12).map(id=>`<span class="vl-bdc-id" title="${escHtml(id)}">${escHtml(id)}</span>`).join(""),more=ids.length>12?`<span class="vl-bdc-more">+${ids.length-12} more</span>`:"",ov=document.createElement("div");ov.className="vl-bdc-overlay",ov.innerHTML=`
<div class="vl-bdc-modal" role="alertdialog" aria-modal="true" aria-label="Confirm delete voices">
<div class="vl-bdc-title"><span class="mdi mdi-alert-circle-outline"></span> Delete ${ids.length} voice${plural}?</div>
<p class="vl-bdc-sub">This permanently removes the selected voice${plural} from your library. This cannot be undone.</p>
<div class="vl-bdc-list">${names}${more}</div>
<div class="vl-bdc-actions">
<button type="button" class="vl-bdc-cancel">Cancel</button>
<button type="button" class="vl-bdc-go">Delete ${ids.length} voice${plural}</button>
</div>
</div>`;const close=()=>{ov.remove(),document.removeEventListener("keydown",onKey)};function onKey(e){e.key==="Escape"&&close()}ov.addEventListener("click",e=>{e.target===ov&&close()}),ov.querySelector(".vl-bdc-cancel").addEventListener("click",close),document.addEventListener("keydown",onKey),ov.querySelector(".vl-bdc-go").addEventListener("click",async()=>{const goBtn=ov.querySelector(".vl-bdc-go"),cancelBtn=ov.querySelector(".vl-bdc-cancel");goBtn.disabled=cancelBtn.disabled=!0;let done=0,errors=0;await runPool(ids,async id=>{try{(await fetch(`/api/voice/${encodeURIComponent(id)}`,{method:"DELETE"})).ok?done++:errors++}catch{errors++}},5,n=>{goBtn.innerHTML=`<span class="reh-imsdb-spinner"></span> Deleting ${n}/${ids.length}\u2026`}),close(),toast(`Deleted ${done} voice${done!==1?"s":""}${errors?` (${errors} errors)`:""}`,errors?"error":"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),document.body.appendChild(ov),ov.querySelector(".vl-bdc-cancel").focus()}async function _bulkSetEnabled(ids,enabled){let done=0;for(const id of ids)await saveMeta(id,{enabled}).catch(()=>{}),done++;return done}function backendVoiceId(value){return typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||JSON.stringify(value)}function shouldFilterBackendVoices(backend){return["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend||"")}function shouldReplaceWithLibraryVoices(backend){return backend==="voice_design"}async function activeLibraryVoiceIds(){return _voices.length||await loadVoiceLibrary(),new Set((_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id))}function cleanReferenceText(text){return String(text||"").trim()}function selectedPreviewLibraryVoice(){var _a2;const id=((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"";return id?(_voices||[]).find(v=>v.id===id):null}function previewVoiceWarnings(v){var _a2;const warnings=[],backend=backendById(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"");backend&&backend.id&&!["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend.id)&&warnings.push(backend.id==="nvidia_magpie"?"NVIDIA Magpie uses fixed speaker voices, not saved WAV clone identity.":"This backend may follow style/model voice more than the saved WAV identity."),backend&&backend.id==="nvidia_zeroshot"&&v.duration&&(Number(v.duration)<3||Number(v.duration)>10)&&warnings.push("NVIDIA Zeroshot works best with a clear 3-10 second prompt."),backend&&backend.id==="nvidia_flow"&&!v.transcript&&warnings.push("NVIDIA Flow requires the exact saved reference transcript for this voice."),v.transcript||warnings.push("No reference transcript is saved; cloned identity is harder to judge."),v.duration&&(Number(v.duration)<3||Number(v.duration)>20)&&warnings.push("Reference clip length is outside the 3-20 second sweet spot."),v.needs_tts_restart&&warnings.push("This voice changed since the last backend refresh; restart or clear restart flags before judging it.");const healthWarnings=v.health&&Array.isArray(v.health.warnings)?v.health.warnings:[];return warnings.push(...healthWarnings.slice(0,3)),warnings}function updatePreviewVoiceMatchPanel(){const panel=$("preview-match-panel");if(!panel)return;const v=selectedPreviewLibraryVoice();if(!v){panel.hidden=!0;return}panel.hidden=!1;const lang=v.language||v.lang||(v.id||"").split("_")[0]||"-",gender=v.gender||(v.id||"").split("_")[1]||"-",db=fmtDbfs(v),dur=v.duration?fmtDuration(v.duration):"-";$("preview-match-title").textContent=v.id,$("preview-match-detail").textContent=`${lang} \xB7 ${gender} \xB7 ${dur} \xB7 ${db} dBFS`;const warnings=previewVoiceWarnings(v);$("preview-match-warning").textContent=warnings.length?warnings.join(" "):"For a fair voice match check, play the WAV and synthesize the exact saved reference text.";const transcript=cleanReferenceText(v.transcript||"");$("preview-match-transcript").textContent=transcript||"No reference text saved for this voice.",$("preview-ref-use-text").disabled=!transcript,$("preview-ref-synth").disabled=!transcript;const audio=$("preview-ref-audio"),expected=voiceFileUrl(v);audio.dataset.src!==expected&&(audio.pause(),audio.src=expected,audio.dataset.src=expected);const actionsEl=panel.querySelector(".preview-match-actions");let personaBtn=panel.querySelector(".preview-persona-btn");v.persona?personaBtn||(personaBtn=document.createElement("button"),personaBtn.className="btn-secondary preview-persona-btn",personaBtn.type="button",personaBtn.textContent="Rewrite with persona",actionsEl==null||actionsEl.appendChild(personaBtn),personaBtn.addEventListener("click",async()=>{const text=$("preview-text-area").value.trim();if(!text){toast("Enter text to rewrite","error");return}const lv=selectedPreviewLibraryVoice();if(!(lv!=null&&lv.persona)){toast("This voice has no persona","error");return}personaBtn.disabled=!0,personaBtn.textContent="Rewriting\u2026";try{const llmUrl=localStorage.getItem("refine-llm-url")||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",r=await fetch("/api/rewrite-with-persona",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,persona:lv.persona,llm_url:llmUrl,model:(_appSettings==null?void 0:_appSettings.llm_model)||"",mode:"rewrite"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("preview-text-area").value=d.text,toast("Text rewritten in persona style","success")}catch(e){toast("Persona rewrite failed: "+e.message,"error")}finally{personaBtn.disabled=!1,personaBtn.textContent="Rewrite with persona"}})):personaBtn==null||personaBtn.remove();const personaToggle=$("preview-persona-toggle");if(personaToggle){const label=personaToggle.closest("label");v.persona?(personaToggle.disabled=!1,label&&(label.title="Rewrite text through this voice's character persona before generating")):(personaToggle.checked=!1,personaToggle.disabled=!0,label&&(label.title="This voice has no character persona saved \u2014 set one on the Voice Inspector page first."))}}async function synthesizeSelectedReferenceText(){const v=selectedPreviewLibraryVoice();if(!v){toast("Select a library voice first","error");return}let text=cleanReferenceText(v.transcript||"");if(!text){toast("This voice has no reference text","error");return}if(v.needs_tts_restart){if(!confirm("This voice is marked as needing a TTS restart. If you already restarted the backend, clear the flag and synthesize anyway?"))return;await clearTtsRestartFlags(),v.needs_tts_restart=!1,updatePreviewVoiceMatchPanel()}const backend=$("tts-backend-select").value;if(!backend){toast("No available TTS backend","error");return}const btn=$("preview-ref-synth");btn.disabled=!0;try{$("preview-text-area").value=text;const source=await createTtsAudioSource(v.id,text,backend,$("preview-playback-mode").value,$("preview-style-instruction").value.trim());previewBlob=source.blob;const audio=$("preview-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,toast(source.streaming?"Reference text streaming":"Reference text synthesized","success")}catch(e){toast("Reference synthesis failed: "+e.message,"error")}finally{btn.disabled=!1}}$("fetch-tts-voices-btn").addEventListener("click",async()=>{var _a2;$("fetch-tts-voices-btn").disabled=!0;try{const backend=(_a2=$("tts-backend-select"))==null?void 0:_a2.value;if(!backend)throw new Error("No available TTS backend");let ids;if(shouldReplaceWithLibraryVoices(backend))_voices.length||await loadVoiceLibrary(),ids=(_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id);else{const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());let voices=Array.isArray(rawVoices)?rawVoices:[];if(shouldFilterBackendVoices(backend)){const activeIds=await activeLibraryVoiceIds();voices=voices.filter(v=>activeIds.has(backendVoiceId(v)))}ids=voices.map(backendVoiceId)}const sel=$("tts-voice-select"),prev=sel.value;window.VoicePicker?(VoicePicker.upgrade("tts-voice-select"),VoicePicker.populate("tts-voice-select",ids),prev&&ids.includes(prev)&&VoicePicker.setValue("tts-voice-select",prev)):(sel.innerHTML='<option value="">\u2014 select after fetch \u2014</option>',ids.forEach(id=>{const o=document.createElement("option");o.value=o.textContent=id,sel.appendChild(o)}),prev&&ids.includes(prev)&&(sel.value=prev)),updatePreviewVoiceMatchPanel();const suffix=shouldFilterBackendVoices(backend)||shouldReplaceWithLibraryVoices(backend)?" active voices":" voices";toast("Fetched "+ids.length+suffix,"success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("fetch-tts-voices-btn").disabled=!1}});function _ttsIsFishBackend(id){return/fish/i.test(id||"")}const FISH_EMOTION_TAGS={"Basic emotions":[["happy","Cheerful, upbeat"],["sad","Melancholic, downcast"],["angry","Frustrated, aggressive"],["excited","Energetic, enthusiastic"],["calm","Peaceful, relaxed"],["nervous","Anxious, uncertain"],["confident","Assertive, self-assured"],["surprised","Shocked, amazed"],["satisfied","Content, pleased"],["delighted","Very pleased, joyful"],["scared","Frightened, fearful"],["worried","Concerned, troubled"],["upset","Disturbed, distressed"],["frustrated","Annoyed, exasperated"],["depressed","Very sad, hopeless"],["empathetic","Understanding, caring"],["embarrassed","Ashamed, awkward"],["disgusted","Repelled, revolted"],["moved","Emotionally touched"],["proud","Accomplished, satisfied"],["relaxed","At ease, casual"],["grateful","Thankful, appreciative"],["curious","Inquisitive, interested"],["sarcastic","Ironic, mocking"]],"Advanced emotions":[["disdainful","Contemptuous, scornful"],["unhappy","Discontent, dissatisfied"],["anxious","Very worried, uneasy"],["hysterical","Uncontrollably emotional"],["indifferent","Uncaring, neutral"],["uncertain","Doubtful, unsure"],["doubtful","Skeptical, questioning"],["confused","Puzzled, perplexed"],["disappointed","Let down"],["regretful","Sorry, remorseful"],["guilty","Culpable, responsible"],["ashamed","Deeply embarrassed"],["jealous","Envious, resentful"],["envious","Wanting what others have"],["hopeful","Optimistic about future"],["optimistic","Positive outlook"],["pessimistic","Negative outlook"],["nostalgic","Longing for the past"],["lonely","Isolated, alone"],["bored","Uninterested, weary"],["contemptuous","Showing contempt"],["sympathetic","Showing sympathy"],["compassionate","Showing deep care"],["determined","Resolved, decided"],["resigned","Accepting defeat"]]},FISH_TAG_SAMPLE_TEXT=`[happy] I got the promotion!
[uncertain] But... it means relocating.
[sad] I'll miss everyone here.
[hopeful] Though it's a great opportunity.
[determined] I'm going to make it work!`;function _fishInsertTag(tag){var _a2,_b2;const ta=$("preview-text-area");if(!ta)return;const snippet=`[${tag}] `,s=(_a2=ta.selectionStart)!=null?_a2:ta.value.length,e=(_b2=ta.selectionEnd)!=null?_b2:ta.value.length;ta.value=ta.value.slice(0,s)+snippet+ta.value.slice(e);const pos=s+snippet.length;ta.setSelectionRange(pos,pos),ta.focus()}function _fishTagsRender(){const body=$("fish-tags-body");!body||body.dataset.built||(body.innerHTML=Object.entries(FISH_EMOTION_TAGS).map(([group,tags])=>`
<div class="fish-tags-group">
<div class="fish-tags-group-title">${escHtml(group)} <span class="note">(${tags.length})</span></div>
<div class="fish-tags-grid">
${tags.map(([t,d])=>`<button type="button" class="fish-tag" data-tag="${escHtml(t)}" title="${escHtml(d)}">[${escHtml(t)}]</button>`).join("")}
</div>
</div>`).join(""),body.querySelectorAll(".fish-tag").forEach(b=>b.addEventListener("click",()=>_fishInsertTag(b.dataset.tag))),body.dataset.built="1")}function _fishTagsSync(){var _a2;const panel=$("fish-tags-panel");panel&&(panel.hidden=!_ttsIsFishBackend(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||""))}(function(){var _a2;const toggle=$("fish-tags-toggle"),body=$("fish-tags-body");toggle==null||toggle.addEventListener("click",()=>{_fishTagsRender(),body.hidden=!body.hidden,toggle.textContent=body.hidden?"Show tags":"Hide tags"}),(_a2=$("fish-tags-sample"))==null||_a2.addEventListener("click",()=>{const ta=$("preview-text-area");ta&&(ta.value=FISH_TAG_SAMPLE_TEXT,ta.focus())})})();function _ttsApplyEmotionTag(text,emotionValue){var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"";if(!_ttsIsFishBackend(backend)||!emotionValue||/^\s*\[/.test(text))return text;const tag=typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(emotionValue):"";return tag?`[${tag}] ${text}`:text}setTimeout(function(){const sel=$("preview-emotion-select");!sel||typeof window.REH_EMOTIONS=="undefined"||(window.REH_EMOTIONS.forEach(e=>{if(!e.value)return;const o=document.createElement("option");o.value=e.value,o.textContent=`${e.emoji} ${e.label}`,sel.appendChild(o)}),sel.addEventListener("change",()=>{var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"",help=$("preview-emotion-help");if(_ttsIsFishBackend(backend))help&&(help.style.display=sel.value?"block":"none",help.textContent=sel.value?"Applied as an inline [tag] in the text for Fish-Speech \u2014 the style instruction field below is ignored by this backend.":"");else{help&&(help.style.display="none");const styleInput=$("preview-style-instruction");styleInput&&sel.value&&(styleInput.value=sel.value)}}),_fishTagsSync())},0),$("tts-backend-select").addEventListener("change",()=>{var _a2;const sel=$("tts-voice-select");sel.innerHTML='<option value="">\u2014 select after fetch \u2014</option>',updateBackendHelp(),updatePreviewVoiceMatchPanel(),previewBlob=null,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,(_a2=$("preview-emotion-select"))==null||_a2.dispatchEvent(new Event("change")),_fishTagsSync()}),$("tts-voice-select").addEventListener("change",updatePreviewVoiceMatchPanel),$("preview-ref-play").addEventListener("click",async()=>{updatePreviewVoiceMatchPanel();const audio=$("preview-ref-audio");try{await audio.play()}catch(e){toast("Reference playback failed: "+e.message,"error")}}),$("preview-ref-use-text").addEventListener("click",()=>{const v=selectedPreviewLibraryVoice(),text=cleanReferenceText((v==null?void 0:v.transcript)||"");if(!text){toast("This voice has no reference text","error");return}$("preview-text-area").value=text,toast("Reference text copied to target text","success")}),$("preview-ref-synth").addEventListener("click",synthesizeSelectedReferenceText);let _ttsStreamHealth=null;function effectiveTtsPlaybackMode(override="settings"){return override&&override!=="settings"?override:_appSettings.tts_stream_mode||"auto"}async function isTtsStreamAvailable(force=!1){if(_ttsStreamHealth&&!force)return _ttsStreamHealth.ok;try{return _ttsStreamHealth=await fetch("/api/tts-stream-health").then(r=>r.json()),!!_ttsStreamHealth.ok}catch{return _ttsStreamHealth={ok:!1},!1}}async function createTtsStreamUrl(voice,text,instruct=""){if(!await isTtsStreamAvailable())throw new Error("streaming backend unavailable");const r=await fetch("/api/tts-stream-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,voice,instruct})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return(await r.json()).url}async function _ttsPreviewFetchWithRetry(body,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})}catch(e){if(i===tries)throw e;await new Promise(r=>setTimeout(r,2500*i))}}function _ttsBackendForVoice(voiceId,fallbackBackend){if(!voiceId||voiceId==="me")return fallbackBackend;const v=(window._voices||[]).find(x=>x.id===voiceId);return v&&!v.has_ref?"voice_design":fallbackBackend}async function fetchTtsPreviewBlob(voice,text,responseFormat="wav",instruct="",backend="voice_clone",applyPersona=!1,extra=null){const body={text,voice,response_format:responseFormat,instruct,backend};applyPersona&&(body.apply_persona=!0),extra&&typeof extra=="object"&&Object.assign(body,extra);const r=await _ttsPreviewFetchWithRetry(body);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const blob=await r.blob();if(responseFormat==="wav"){const v=(window._voices||[]).find(x=>x.id===voice),gainDb=v&&v.loudness&&typeof v.loudness.gain_db=="number"?v.loudness.gain_db:0;if(gainDb)try{const nr=await fetch("/api/audio/apply-gain?gain_db="+encodeURIComponent(gainDb),{method:"POST",body:blob});if(nr.ok)return await nr.blob()}catch{}}return blob}function _textWords(s){return String(s||"").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean)}function _textSimilarity(a,b){const wa=_textWords(a),wb=_textWords(b);if(!wa.length&&!wb.length)return 1;if(!wa.length||!wb.length)return 0;const counts=new Map;wa.forEach(w=>counts.set(w,(counts.get(w)||0)+1));let overlap=0;return wb.forEach(w=>{const c=counts.get(w);c&&(overlap++,counts.set(w,c-1))}),2*overlap/(wa.length+wb.length)}async function _voiceRoundtripCheck(voiceId,text,backend,instruct=""){const blob=await fetchTtsPreviewBlob(voiceId,text,"wav",instruct,backend),fd=new FormData;fd.append("file",blob,"roundtrip.wav"),fd.append("backend","configured");const r=await fetch("/api/transcribe-bytes",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return{transcript:d.text||"",score:_textSimilarity(text,d.text||""),blob}}async function createTtsAudioSource(voice,text,backend="voice_clone",modeOverride="settings",instruct="",applyPersona=!1,extra=null){const mode=effectiveTtsPlaybackMode(modeOverride);if(backend!=="streaming"||mode==="buffered"){const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}try{return{url:await createTtsStreamUrl(voice,text,instruct),blob:null,streaming:!0,label:"streaming"}}catch(e){if(mode==="streaming")throw e;const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}}let previewBlob=null;const PREVIEW_SAMPLE_TEXT="Hello! This is a voice preview from TTS Voice Creator - Clone and Design.";$("preview-text-area").addEventListener("focus",()=>{$("preview-text-area").value===PREVIEW_SAMPLE_TEXT&&($("preview-text-area").value="")},{once:!0});function _onPreviewGenerated(source,voice,text,backend,instruct){typeof effectsSourceBlob!="undefined"&&(window._effectsSourceBlob=null),window._effectsSynthArgs={voice,text,instruct:instruct||"",backend};const ea=$("effects-apply-btn");ea&&(ea.disabled=!1);const ap=$("add-to-playlist-btn");ap&&source.blob&&(ap.disabled=!1),typeof historyPush=="function"&&source.blob&&historyPush(voice,text,backend,source.blob,source.url)}const _TRYOUT_SPEED_KEY="ttsvc_tryout_native_speed";(function(){const saved=localStorage.getItem(_TRYOUT_SPEED_KEY);if(saved){const el=$("preview-native-speed");el&&(el.value=saved)}})(),(_R=$("preview-native-speed"))==null||_R.addEventListener("change",function(){localStorage.setItem(_TRYOUT_SPEED_KEY,this.value)}),$("preview-btn").addEventListener("click",async()=>{var _a2,_b2,_c2,_d2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value),applyPersona=((_b2=$("preview-persona-toggle"))==null?void 0:_b2.checked)||!1;if(!backend){toast("No available TTS backend","error");return}if(!voice){toast("Select a TTS voice","error");return}if(!text){toast("Enter preview text","error");return}$("preview-btn").disabled=!0,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,$("add-to-playlist-btn")&&($("add-to-playlist-btn").disabled=!0),$("effects-apply-btn")&&($("effects-apply-btn").disabled=!0);const _nspd=parseFloat((_c2=$("preview-native-speed"))==null?void 0:_c2.value),_extra=!isNaN(_nspd)&&_nspd!==1?{speed:_nspd}:null;try{const audio=$("preview-audio"),source=((_d2=$("preview-chunked-toggle"))==null?void 0:_d2.checked)&&text.length>200&&typeof generateChunkedTts=="function"?await generateChunkedTts(voice,text,backend,instruct,_extra,applyPersona):await createTtsAudioSource(voice,text,backend,$("preview-playback-mode").value,instruct,applyPersona,_extra);previewBlob=source.blob,window._previewVoice=voice,window._previewBackend=backend,window._previewText=text,audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,_onPreviewGenerated(source,voice,text,backend,instruct),toast(source.streaming?"Streaming preview playing":source.label==="chunked"?`Chunked (${text.length} chars) playing`:"Preview playing","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{$("preview-btn").disabled=!1}}),$("save-preview-mp3-btn").addEventListener("click",async()=>{var _a2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value);if(!backend){toast("No available TTS backend","error");return}if(!voice||!text)return;const btn=$("save-preview-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(voice||"preview")+"_preview.mp3",a.click(),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),$("save-preview-btn").addEventListener("click",()=>{if(!previewBlob)return;const a=document.createElement("a");a.href=URL.createObjectURL(previewBlob),a.download=($("tts-voice-select").value||"preview")+"_preview.wav",a.click()});const PERF_HISTORY_KEY="vcf-perf-history",PERF_HISTORY_MAX=50,PERF_HISTORY_SORT={key:"ts",dir:"desc"};function perfHistoryLoad(){try{return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY)||"[]")}catch{return[]}}function perfHistorySave(entries){try{localStorage.setItem(PERF_HISTORY_KEY,JSON.stringify(entries.slice(-PERF_HISTORY_MAX)))}catch{}}function perfHistoryAdd(entry){const h=perfHistoryLoad();h.push(entry),perfHistorySave(h)}function perfSparklineSvg(rtfValues){if(!rtfValues.length)return"";const W=120,H=32,PAD=2,barW=Math.max(4,Math.floor((W-PAD*2)/rtfValues.length)-1),maxV=Math.max(...rtfValues,1),bars=rtfValues.map((v,i)=>{const bh=Math.max(3,Math.round(v/maxV*(H-PAD*2))),x=PAD+i*(barW+1),y=H-PAD-bh,col=v<1?"var(--green)":"var(--yellow)";return`<rect x="${x}" y="${y}" width="${barW}" height="${bh}" rx="1" fill="${col}" opacity=".8"/>`}).join("");return`<svg viewBox="0 0 ${W} ${H}" class="perf-sparkline" aria-hidden="true">${bars}</svg>`}function perfHistoryVoiceLookup(voiceId){return(Array.isArray(window._voices)&&window._voices.length?window._voices:typeof _voices!="undefined"&&Array.isArray(_voices)?_voices:[]).find(v=>v&&(v.id===voiceId||v.name===voiceId||v.voice_id===voiceId))||null}function perfHistoryVoiceMeta(entry){var _a2,_b2,_c2;const voice=(entry==null?void 0:entry.voice)||"",saved=(entry==null?void 0:entry.voiceMeta)||{},lib=perfHistoryVoiceLookup(voice)||{},language=saved.lang||saved.language||(entry==null?void 0:entry.lang)||(entry==null?void 0:entry.language)||lib.lang||lib.language||"",gender=String(saved.gender||(entry==null?void 0:entry.gender)||lib.gender||"").trim().toUpperCase().charAt(0);return{id:voice,label:saved.label||saved.display_name||(entry==null?void 0:entry.voiceLabel)||lib.display_name||lib.name||voice,lang:language,gender:["F","M","N"].includes(gender)?gender:"",flag:saved.flag||(entry==null?void 0:entry.flag)||lib.flag||"",avatar:saved.avatar||(entry==null?void 0:entry.avatar)||lib.avatar||"",hasPicture:!!((_c2=(_b2=(_a2=saved.has_picture)!=null?_a2:saved.hasPicture)!=null?_b2:entry==null?void 0:entry.hasPicture)!=null?_c2:lib.has_picture)}}function perfHistoryExtraFields(voice,backend){const lib=perfHistoryVoiceLookup(voice)||{};return{voiceMeta:{label:lib.display_name||lib.name||voice,lang:lib.lang||lib.language||"",gender:lib.gender||"",flag:lib.flag||"",avatar:lib.avatar||"",has_picture:!!lib.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function perfHistoryGenderLabel(gender){return{F:"Female",M:"Male",N:"Diverse"}[gender]||""}function perfHistoryDeviceLabel(entry){return(entry==null?void 0:entry.device)||(typeof backendComputeDevice=="function"?backendComputeDevice((entry==null?void 0:entry.backend)||""):"")||"Unknown"}function perfHistoryDeviceHtml(entry){const label=perfHistoryDeviceLabel(entry),cls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass((entry==null?void 0:entry.backend)||""):label.toLowerCase().includes("gpu")?"gpu":label.toLowerCase().includes("cpu")?"cpu":"";return`<span class="bench-device ${escHtml(cls)}">${escHtml(label)}</span>`}function perfHistoryAvatarHtml(entry){var _a2;const meta=perfHistoryVoiceMeta(entry),title=meta.label||meta.id||"Voice";if(meta.hasPicture)return`<span class="perf-history-avatar" title="${escHtml(title)}"><img src="/api/voice/picture/${encodeURIComponent(meta.id)}" alt=""></span>`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,24):null;if(icon)return`<span class="perf-history-avatar" title="${escHtml(title)}">${icon.replace(/vp-avatar/g,"perf-history-avatar-icon")}</span>`;const color=typeof avatarColor=="function"?avatarColor(meta.lang||meta.id||title):"#6b7280",init=((_a2=(title||"?").trim()[0])==null?void 0:_a2.toUpperCase())||"?";return`<span class="perf-history-avatar perf-history-avatar-init" style="background:${color}" title="${escHtml(title)}">${escHtml(init)}</span>`}function perfHistorySortValue(entry,key){switch(key){case"backend":return String(entry.backend||"").toLowerCase();case"language":return String(perfHistoryVoiceMeta(entry).lang||"").toLowerCase();case"gender":return String(perfHistoryGenderLabel(perfHistoryVoiceMeta(entry).gender)||"").toLowerCase();case"voice":return String(entry.voice||"").toLowerCase();case"device":return String(perfHistoryDeviceLabel(entry)||"").toLowerCase();case"avgLatencyMs":return Number(entry.avgLatencyMs);case"minLatencyMs":return Number(entry.minLatencyMs);case"avgRtf":return Number(entry.avgRtf);case"ts":default:return Number(entry.ts)}}function perfHistoryCompare(a,b){const av=perfHistorySortValue(a,PERF_HISTORY_SORT.key),bv=perfHistorySortValue(b,PERF_HISTORY_SORT.key);let result=0;if(typeof av=="string"||typeof bv=="string")result=String(av).localeCompare(String(bv),void 0,{numeric:!0,sensitivity:"base"});else{const an=Number.isFinite(av)?av:-1/0,bn=Number.isFinite(bv)?bv:-1/0;result=an===bn?0:an-bn}return PERF_HISTORY_SORT.dir==="asc"?result:-result}function perfHistoryHeadButton(key,label){const active=PERF_HISTORY_SORT.key===key,icon=active?PERF_HISTORY_SORT.dir==="asc"?"mdi-arrow-up":"mdi-arrow-down":"mdi-swap-vertical";return`<button class="perf-history-sort${active?" active":""}" type="button" data-history-sort="${escHtml(key)}" aria-label="Sort by ${escHtml(label)}">
<span>${escHtml(label)}</span><span class="mdi ${icon}" aria-hidden="true"></span>
</button>`}function renderPerfHistory(){var _a2,_b2;const histList=$("perf-history-list");if(!histList)return;const filterEl=$("perf-history-filter-current"),filterOn=filterEl==null?void 0:filterEl.checked,curBack=(_a2=$("perf-backend-select"))==null?void 0:_a2.value,curVoice=(_b2=$("perf-voice-select"))==null?void 0:_b2.value;let entries=perfHistoryLoad().slice();if(filterOn&&curBack&&(entries=entries.filter(e=>e.backend===curBack&&e.voice===curVoice)),!entries.length){histList.innerHTML='<div class="perf-history-empty">'+(filterOn?"No history for this backend/voice yet.":"No benchmark history yet. Run a benchmark above to start tracking.")+"</div>";return}entries.sort((a,b)=>perfHistoryCompare(a,b)||Number(b.ts)-Number(a.ts));const head=`<div class="perf-history-row perf-history-head">
<span>${perfHistoryHeadButton("ts","Date / Time")}</span>
<span>${perfHistoryHeadButton("backend","Backend")}</span>
<span>Profile</span>
<span>${perfHistoryHeadButton("language","Language")}</span>
<span>${perfHistoryHeadButton("gender","Gender")}</span>
<span>${perfHistoryHeadButton("voice","Voice")}</span>
<span>${perfHistoryHeadButton("device","CPU / GPU")}</span>
<span>${perfHistoryHeadButton("avgLatencyMs","Avg latency")}</span>
<span>${perfHistoryHeadButton("minLatencyMs","Min")}</span>
<span>${perfHistoryHeadButton("avgRtf","Avg RTF")}</span>
<span></span>
</div>`,rows=entries.map(e=>{const dt=new Date(e.ts).toLocaleString([],{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),rtfCls=e.avgRtf<1?"perf-good":"perf-slow",meta=perfHistoryVoiceMeta(e);return`<div class="perf-history-row">
<span class="perf-history-ts">${escHtml(dt)}</span>
<span>${escHtml(e.backend)}</span>
<span>${perfHistoryAvatarHtml(e)}</span>
<span>${escHtml(meta.lang||"-")}</span>
<span>${escHtml(perfHistoryGenderLabel(meta.gender)||"-")}</span>
<span>${escHtml(e.voice)}</span>
<span>${perfHistoryDeviceHtml(e)}</span>
<span>${Math.round(e.avgLatencyMs)} ms</span>
<span>${Math.round(e.minLatencyMs)} ms</span>
<span class="${rtfCls}">${e.avgRtf.toFixed(2)}</span>
<span class="perf-history-del" data-ts="${e.ts}" title="Remove"><span class="mdi mdi-close"></span></span>
</div>`}).join("");histList.innerHTML=head+rows,histList.querySelectorAll(".perf-history-sort").forEach(btn=>{btn.addEventListener("click",()=>{const key=btn.dataset.historySort||"ts";PERF_HISTORY_SORT.key===key?PERF_HISTORY_SORT.dir=PERF_HISTORY_SORT.dir==="asc"?"desc":"asc":(PERF_HISTORY_SORT.key=key,PERF_HISTORY_SORT.dir=key==="backend"||key==="voice"?"asc":"desc"),renderPerfHistory()})}),histList.querySelectorAll(".perf-history-del").forEach(btn=>{btn.addEventListener("click",()=>{const ts=Number(btn.dataset.ts);perfHistorySave(perfHistoryLoad().filter(e=>e.ts!==ts)),renderPerfHistory()})})}(function(){var _a2,_b2;const perfBackendSel=$("perf-backend-select"),perfVoiceSel=$("perf-voice-select"),perfFetchBtn=$("perf-fetch-voices-btn"),perfRunBtn=$("perf-run-btn"),perfClearBtn=$("perf-clear-btn"),perfProgress=$("perf-progress"),perfResultsCard=$("perf-results-card"),perfSummary=$("perf-summary"),perfTbody=$("perf-tbody"),perfText=$("perf-text"),perfRunsSel=$("perf-runs");if(!perfRunBtn)return;let perfRows=[];function populatePerfBackends(){if(!perfBackendSel)return;const cur=perfBackendSel.value;perfBackendSel.innerHTML=availableTtsBackends().map(b=>`<option value="${escHtml(b.id)}"${b.id===cur?" selected":""}>${escHtml(b.label)}</option>`).join("")||'<option value="">No backends available</option>'}populatePerfBackends(),window.BenchmarkVoicePicker&&BenchmarkVoicePicker.upgrade("perf-voice-select",{placeholder:"-- select after fetch --",empty:"No voices"}),perfFetchBtn.addEventListener("click",async()=>{const backend=perfBackendSel.value;if(!backend){toast("Select a backend first","error");return}perfFetchBtn.disabled=!0;try{const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json()),cur=perfVoiceSel.value,items=rawVoices.map(v=>{const id=backendVoiceId(v);return{id,label:id,meta:(window._voices||[]).find(x=>x&&x.id===id)||(typeof v=="object"?v:null)}}).filter(v=>v.id);window.BenchmarkVoicePicker?(BenchmarkVoicePicker.populate("perf-voice-select",items,{placeholder:"-- select after fetch --",empty:"No voices"}),cur&&items.some(v=>v.id===cur)&&BenchmarkVoicePicker.set("perf-voice-select",cur)):perfVoiceSel.innerHTML=items.map(v=>`<option value="${escHtml(v.id)}"${v.id===cur?" selected":""}>${escHtml(v.id)}</option>`).join("")||'<option value="">No voices</option>',window.dispatchEvent(new CustomEvent("benchmark:tts-voices-fetched",{detail:{backend,voices:items}}))}catch(e){toast("Fetch voices failed: "+e.message,"error")}finally{perfFetchBtn.disabled=!1}});function updateTrendDisplay(backend,voice,currentAvgRtf){const trendRow=$("perf-trend-row"),trendBadge=$("perf-trend-badge"),sparkWrap=$("perf-sparkline-wrap")||(trendRow==null?void 0:trendRow.querySelector(".perf-sparkline-wrap"));if(!trendRow)return;const history=perfHistoryLoad().filter(e=>e.backend===backend&&e.voice===voice&&typeof e.avgRtf=="number");if(history.length===0){trendRow.style.display="none";return}const prevRtf=history[history.length-1].avgRtf,delta=currentAvgRtf-prevRtf,pct=Math.abs(delta/Math.max(prevRtf,.01))*100;let cls,label;pct<5?(cls="perf-trend-stable",label='<span class="mdi mdi-minus"></span> Stable'):delta<0?(cls="perf-trend-better",label=`<span class="mdi mdi-arrow-down"></span> ${pct.toFixed(0)}% faster`):(cls="perf-trend-worse",label=`<span class="mdi mdi-arrow-up"></span> ${pct.toFixed(0)}% slower`),trendBadge.className="perf-trend-badge "+cls,trendBadge.innerHTML=label;const rtfValues=[...history.slice(-9).map(e=>e.avgRtf),currentAvgRtf];sparkWrap&&(sparkWrap.innerHTML=perfSparklineSvg(rtfValues)),trendRow.style.display=""}function renderPerfTable(sessionDone=!1){if(!perfRows.length){perfResultsCard.style.display="none";return}perfResultsCard.style.display="",perfTbody.innerHTML=perfRows.map((r,i)=>{const rtf=r.audioDuration>0?(r.latencyMs/1e3/r.audioDuration).toFixed(2):"\u2014",ok2=r.ok;return`<tr class="${ok2?"":"perf-row-error"}">
<td>${i+1}</td>
<td>${escHtml(r.backend)}</td>
<td><span class="bench-device ${typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass(r.backend):""}" title="Inferred from the selected TTS backend metadata">${escHtml(typeof backendComputeDevice=="function"?backendComputeDevice(r.backend):"Unknown")}</span></td>
<td>${escHtml(r.voice)}</td>
<td>${ok2?r.latencyMs:"\u2014"}</td>
<td>${ok2&&r.audioDuration>0?r.audioDuration.toFixed(2):"\u2014"}</td>
<td>${ok2?rtf:"\u2014"}</td>
<td>${ok2?'<span class="perf-ok">OK</span>':`<span class="perf-err">${escHtml(r.error||"Error")}</span>`}</td>
</tr>`}).join("");const ok=perfRows.filter(r=>r.ok);if(ok.length){const avg=ok.reduce((s,r)=>s+r.latencyMs,0)/ok.length,minL=Math.min(...ok.map(r=>r.latencyMs)),maxL=Math.max(...ok.map(r=>r.latencyMs)),rtfArr=ok.filter(r=>r.audioDuration>0).map(r=>r.latencyMs/1e3/r.audioDuration),avgRtf=rtfArr.length?rtfArr.reduce((s,v)=>s+v,0)/rtfArr.length:0,labelEl=$("perf-results-label");labelEl&&(labelEl.textContent=`${ok.length} run${ok.length>1?"s":""} \u2014 ${perfBackendSel.value} / ${perfVoiceSel.value}`),perfSummary.innerHTML=`
<span class="perf-stat"><strong>${Math.round(avg)} ms</strong> avg latency</span>
<span class="perf-stat"><strong>${minL} ms</strong> best</span>
<span class="perf-stat"><strong>${maxL} ms</strong> worst</span>
<span class="perf-stat"><strong>${avgRtf.toFixed(2)}</strong> avg RTF</span>
<span class="perf-stat ${avgRtf<1?"perf-good":"perf-slow"}">${avgRtf<1?'<span class="mdi mdi-check-circle-outline"></span> Real-time capable':'<span class="mdi mdi-alert-outline"></span> Slower than real-time'}</span>
`,sessionDone&&rtfArr.length&&(updateTrendDisplay(perfBackendSel.value,perfVoiceSel.value,avgRtf),perfHistoryAdd({ts:Date.now(),backend:perfBackendSel.value,voice:perfVoiceSel.value,...perfHistoryExtraFields(perfVoiceSel.value,perfBackendSel.value),textLen:perfText.value.trim().length,avgLatencyMs:avg,minLatencyMs:minL,maxLatencyMs:maxL,avgRtf,runCount:ok.length,allOk:ok.length===perfRows.length}),renderPerfHistory())}else perfSummary.innerHTML='<span class="perf-err">All runs failed</span>'}perfClearBtn.addEventListener("click",()=>{perfRows=[],renderPerfTable(),$("perf-trend-row")&&($("perf-trend-row").style.display="none"),perfProgress.style.display="none"}),perfRunBtn.addEventListener("click",async()=>{const backend=perfBackendSel.value,voice=perfVoiceSel.value,text=perfText.value.trim(),runs=parseInt(perfRunsSel.value)||3;if(!backend){toast("Select a backend first","error");return}if(!voice){toast("Fetch and select a voice first","error");return}if(!text){toast("Enter sample text","error");return}perfRows=[],perfRunBtn.disabled=!0,perfProgress.style.display="";for(let i=0;i<runs;i++){perfProgress.textContent=`Run ${i+1} / ${runs}\u2026`;const row={backend,voice,ok:!1,latencyMs:0,audioDuration:0,error:""};try{const t0=performance.now(),blob=await fetchTtsPreviewBlob(voice,text,"wav","",backend);row.latencyMs=Math.round(performance.now()-t0),row.ok=!0;try{const audioCtx=new(window.AudioContext||window.webkitAudioContext),buf=await audioCtx.decodeAudioData(await blob.arrayBuffer());row.audioDuration=buf.duration,audioCtx.close()}catch{}}catch(e){row.error=e.message}perfRows.push(row),renderPerfTable(!1)}perfProgress.textContent=`Done \u2014 ${runs} run${runs>1?"s":""} completed.`,renderPerfTable(!0),perfRunBtn.disabled=!1}),(_a2=$("perf-history-filter-current"))==null||_a2.addEventListener("change",renderPerfHistory),(_b2=$("perf-history-clear-btn"))==null||_b2.addEventListener("click",()=>{perfHistorySave([]),renderPerfHistory(),toast("Benchmark history cleared","success")}),renderPerfHistory()})();function _parseWavBytes(bytes){const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);let off=12,fmt=null,dataOff=0,dataSize=0;for(;off+8<=bytes.length;){const id=v.getUint32(off,!1),sz=v.getUint32(off+4,!0);id===1718449184?fmt={channels:v.getUint16(off+10,!0),sampleRate:v.getUint32(off+12,!0),bitDepth:v.getUint16(off+22,!0)}:id===1684108385&&(dataOff=off+8,dataSize=sz),off+=8+sz}return{fmt,dataOff,dataSize}}function _buildWavBlob(pcmBytes,fmt){const out=new Uint8Array(44+pcmBytes.length),dv=new DataView(out.buffer);return dv.setUint32(0,1380533830,!1),dv.setUint32(4,36+pcmBytes.length,!0),dv.setUint32(8,1463899717,!1),dv.setUint32(12,1718449184,!1),dv.setUint32(16,16,!0),dv.setUint16(20,1,!0),dv.setUint16(22,fmt.channels,!0),dv.setUint32(24,fmt.sampleRate,!0),dv.setUint32(28,fmt.sampleRate*fmt.channels*(fmt.bitDepth>>3),!0),dv.setUint16(32,fmt.channels*(fmt.bitDepth>>3),!0),dv.setUint16(34,fmt.bitDepth,!0),dv.setUint32(36,1684108385,!1),dv.setUint32(40,pcmBytes.length,!0),out.set(pcmBytes,44),new Blob([out],{type:"audio/wav"})}function _silencePcmBytes(ms,fmt){const bytesPerSample=fmt.bitDepth>>3,frames=Math.round(ms/1e3*fmt.sampleRate);return new Uint8Array(frames*fmt.channels*bytesPerSample)}async function mergeWavBlobs(blobs,gapMs=0){if(!blobs||blobs.length===0)return null;if(blobs.length===1&&!gapMs)return blobs[0];const parsed=[];for(const b of blobs){const bytes=new Uint8Array(await b.arrayBuffer()),p=_parseWavBytes(bytes);if(!p.fmt)throw new Error("Invalid WAV in chunk");parsed.push({bytes,...p})}const ref=parsed[0].fmt,silence=gapMs>0?_silencePcmBytes(gapMs,ref):null,totalPcm=parsed.reduce((s,p)=>s+p.dataSize,0)+(silence?silence.length*(parsed.length-1):0),out=new Uint8Array(totalPcm);let pos=0;return parsed.forEach((p,i)=>{out.set(p.bytes.slice(p.dataOff,p.dataOff+p.dataSize),pos),pos+=p.dataSize,silence&&i<parsed.length-1&&(out.set(silence,pos),pos+=silence.length)}),_buildWavBlob(out,ref)}function splitTextIntoChunks(text,maxLen=800){const abbrev=/\b(Mr|Mrs|Ms|Dr|Prof|Sr|Jr|vs|etc|e\.g|i\.e)\.\s/g,safe=text.replace(abbrev,m=>m.replace(".","\0")),restore=s=>s.replace(/\x00/g,"."),segments=[];for(const line of safe.split(`
`)){if(!line.trim()){segments.length&&segments.push("");continue}const sentences=line.match(/[^.!?]+[.!?]+\s*/g)||[],rest=line.replace(/[^.!?]+[.!?]+\s*/g,"").trim();segments.push(...sentences),rest&&segments.push(rest)}if(!segments.length)return[text];const chunks=[];let cur="",paraPending=!1;for(const seg of segments){if(seg===""){paraPending=!0;continue}const joined=cur?cur+(paraPending?`
`:" ")+seg.trim():seg.trim();joined.length>maxLen&&cur?(chunks.push(restore(cur.trim())),cur=seg.trim()):cur=joined,paraPending=!1}return cur.trim()&&chunks.push(restore(cur.trim())),chunks.length?chunks:[text]}async function generateChunkedTts(voice,text,backend,instruct,extra=null,applyPersona=!1){const chunks=splitTextIntoChunks(text),prog=$("preview-chunk-progress");prog&&(prog.hidden=!1,prog.textContent=`Chunk 1 / ${chunks.length}\u2026`);const blobs=[];for(let i=0;i<chunks.length;i++)prog&&(prog.textContent=`Chunk ${i+1} / ${chunks.length}\u2026`),blobs.push(await fetchTtsPreviewBlob(voice,chunks[i],"wav",instruct,backend,applyPersona,extra));prog&&(prog.textContent="Merging\u2026");const merged=await mergeWavBlobs(blobs);return prog&&(prog.hidden=!0,prog.textContent=""),{url:URL.createObjectURL(merged),blob:merged,streaming:!1,label:"chunked"}}const _genHistory=[];function historyPush(voice,text,backend,blob,url){const id=Date.now()+"-"+Math.random().toString(36).slice(2,6);_genHistory.unshift({id,ts:Date.now(),voice,text:text.slice(0,200),backend,blob,url}),_genHistory.length>20&&_genHistory.pop(),renderHistory()}function _histAvatarColor(id){const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<(id||"").length;i++)h=h*31+id.charCodeAt(i)>>>0;return palette[h%palette.length]}function renderHistory(){const list=$("history-list");if(list){if(!_genHistory.length){list.innerHTML='<div class="history-empty">No generations yet.</div>';return}list.innerHTML=_genHistory.map(item=>{const t=new Date(item.ts),ts=String(t.getHours()).padStart(2,"0")+":"+String(t.getMinutes()).padStart(2,"0"),preview=escHtml(item.text.length>90?item.text.slice(0,90)+"\u2026":item.text),v=(window._voices||[]).find(vx=>vx.id===item.voice),avatar=v!=null&&v.has_picture?`<img src="/api/voice/picture/${encodeURIComponent(item.voice)}" class="hist-avatar" alt="">`:`<span class="hist-avatar hist-avatar-init" style="background:${_histAvatarColor(item.voice)}">${(item.voice||"?")[0].toUpperCase()}</span>`;return`<div class="history-item" data-hid="${escHtml(item.id)}">
<div class="history-item-row1">
${avatar}
<span class="history-voice">${escHtml(item.voice)}</span>
<span class="history-backend">${escHtml(item.backend)}</span>
<span class="history-time">${ts}</span>
<button class="hist-del-btn" title="Delete"><span class="mdi mdi-close"></span></button>
</div>
<div class="history-item-text">${preview}</div>
<div class="history-item-actions">
<button class="btn-secondary btn-xs hist-play-btn" ${item.blob?"":"disabled"}><span class="mdi mdi-play"></span> Play</button>
<button class="btn-secondary btn-xs hist-reuse-btn"><span class="mdi mdi-restore"></span> Reuse</button>
<button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?"":"disabled"}><span class="mdi mdi-plus"></span> Playlist</button>
</div>
</div>`}).join(""),list.querySelectorAll(".hist-del-btn").forEach(btn=>{btn.addEventListener("click",e=>{var _a2;e.stopPropagation();const hid=(_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid,idx=_genHistory.findIndex(h=>h.id===hid);idx!==-1&&(_genHistory.splice(idx,1),renderHistory())})}),list.querySelectorAll(".hist-play-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});if(!(item!=null&&item.url))return;const audio=$("preview-audio");audio.src=item.url,audio.style.display="",audio.play().catch(()=>{})})}),list.querySelectorAll(".hist-reuse-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});if(!item)return;$("preview-text-area").value=item.text;const bSel=$("tts-backend-select");bSel&&[...bSel.options].forEach(o=>{o.value===item.backend&&(bSel.value=item.backend)});const vSel=$("tts-voice-select");vSel&&[...vSel.options].forEach(o=>{o.value===item.voice&&(vSel.value=item.voice)}),toast("Settings restored from history","success")})}),list.querySelectorAll(".hist-playlist-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});item!=null&&item.blob&&playlistAdd(item.voice,item.text,item.blob,item.url)})})}}(_S=$("history-clear-btn"))==null||_S.addEventListener("click",()=>{_genHistory.length=0,renderHistory(),toast("History cleared","success")});const _playlist=[];function playlistAdd(voice,text,blob,url){const id="pl-"+Date.now()+"-"+Math.random().toString(36).slice(2,5);_playlist.push({id,voice,text:text.slice(0,120),blob,url}),renderPlaylist(),$("playlist-export-btn")&&($("playlist-export-btn").disabled=!1),toast("Added to playlist","success")}function renderPlaylist(){const list=$("playlist-list");if(list){if(!_playlist.length){list.innerHTML='<div class="history-empty">No clips in playlist. Use <strong>+ Playlist</strong> after generating.</div>',$("playlist-export-btn")&&($("playlist-export-btn").disabled=!0);return}list.innerHTML=_playlist.map((item,i)=>`
<div class="playlist-item" data-pid="${escHtml(item.id)}">
<span class="playlist-num">${i+1}</span>
<div class="playlist-info">
<span class="history-voice">${escHtml(item.voice)}</span>
<span class="playlist-text">${escHtml(item.text.length>70?item.text.slice(0,70)+"\u2026":item.text)}</span>
</div>
<div class="playlist-item-actions">
<button class="btn-secondary btn-xs pl-up" ${i===0?"disabled":""}><span class="mdi mdi-upload"></span></button>
<button class="btn-secondary btn-xs pl-dn" ${i===_playlist.length-1?"disabled":""}><span class="mdi mdi-download"></span></button>
<button class="btn-secondary btn-xs pl-rm"><span class="mdi mdi-close"></span></button>
</div>
</div>`).join(""),list.querySelectorAll(".pl-rm").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx>=0&&(_playlist.splice(idx,1),renderPlaylist())})}),list.querySelectorAll(".pl-up").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx>0&&([_playlist[idx-1],_playlist[idx]]=[_playlist[idx],_playlist[idx-1]],renderPlaylist())})}),list.querySelectorAll(".pl-dn").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx<_playlist.length-1&&([_playlist[idx],_playlist[idx+1]]=[_playlist[idx+1],_playlist[idx]],renderPlaylist())})})}}(_T=$("add-to-playlist-btn"))==null||_T.addEventListener("click",()=>{var _a2,_b2,_c2;if(!previewBlob){toast("Generate audio first","error");return}playlistAdd(window._previewVoice||((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"",window._previewText||((_b2=$("preview-text-area"))==null?void 0:_b2.value)||"",previewBlob,((_c2=$("preview-audio"))==null?void 0:_c2.src)||"")}),(_U=$("playlist-export-btn"))==null||_U.addEventListener("click",async()=>{if(!_playlist.length)return;const btn=$("playlist-export-btn"),orig=btn.textContent;btn.disabled=!0,btn.textContent="Merging\u2026";try{const merged=await mergeWavBlobs(_playlist.map(p=>p.blob).filter(Boolean));if(!merged)throw new Error("No audio to export");const a=document.createElement("a");a.href=URL.createObjectURL(merged),a.download="playlist_"+Date.now()+".wav",a.click(),toast("Playlist exported as WAV","success")}catch(e){toast("Export failed: "+e.message,"error")}finally{btn.disabled=_playlist.length===0,btn.textContent=orig}}),(_V=$("playlist-clear-btn"))==null||_V.addEventListener("click",()=>{_playlist.length=0,renderPlaylist(),toast("Playlist cleared","success")});const _FX_PRESETS={studio:{reverb:{on:!0,room_size:.6,wet:.35},compressor:{on:!0,threshold_db:-24,ratio:3}},broadcast:{compressor:{on:!0,threshold_db:-20,ratio:6}},telephone:{compressor:{on:!0,threshold_db:-18,ratio:8},bandpass:{on:!0,low:300,high:3400}},warm:{reverb:{on:!0,room_size:.2,wet:.15},compressor:{on:!0,threshold_db:-26,ratio:2}},radio:{compressor:{on:!0,threshold_db:-22,ratio:5},bandpass:{on:!0,low:150,high:5500}}};function fxSliderBind(sliderId,labelId,fmt){const s=$(sliderId),l=$(labelId);if(!s||!l)return;const upd=()=>{l.textContent=fmt(s.value)};upd(),s.addEventListener("input",upd)}fxSliderBind("fx-reverb-room","fx-reverb-room-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-reverb-wet","fx-reverb-wet-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-comp-thresh","fx-comp-thresh-val",v=>v+" dB"),fxSliderBind("fx-comp-ratio","fx-comp-ratio-val",v=>v+":1"),fxSliderBind("fx-chorus-rate","fx-chorus-rate-val",v=>parseFloat(v).toFixed(1)+" Hz"),fxSliderBind("fx-chorus-mix","fx-chorus-mix-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-pitch-semi","fx-pitch-semi-val",v=>(parseFloat(v)>=0?"+":"")+v+" st"),fxSliderBind("fx-bandpass-low","fx-bandpass-low-val",v=>Math.round(v)+" Hz"),fxSliderBind("fx-bandpass-high","fx-bandpass-high-val",v=>Math.round(v)+" Hz"),(_W=$("effects-preset"))==null||_W.addEventListener("change",()=>{const preset=_FX_PRESETS[$("effects-preset").value];preset&&(["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-on","fx-bandpass-on"].forEach(id=>{const el=$(id);el&&(el.checked=!1)}),preset.reverb&&($("fx-reverb-on").checked=!!preset.reverb.on,preset.reverb.room_size!=null&&($("fx-reverb-room").value=preset.reverb.room_size),preset.reverb.wet!=null&&($("fx-reverb-wet").value=preset.reverb.wet)),preset.compressor&&($("fx-compressor-on").checked=!!preset.compressor.on,preset.compressor.threshold_db!=null&&($("fx-comp-thresh").value=preset.compressor.threshold_db),preset.compressor.ratio!=null&&($("fx-comp-ratio").value=preset.compressor.ratio)),preset.bandpass&&($("fx-bandpass-on").checked=!!preset.bandpass.on,preset.bandpass.low!=null&&($("fx-bandpass-low").value=preset.bandpass.low),preset.bandpass.high!=null&&($("fx-bandpass-high").value=preset.bandpass.high)),["fx-reverb-room","fx-reverb-wet","fx-comp-thresh","fx-comp-ratio","fx-chorus-rate","fx-chorus-mix","fx-pitch-semi","fx-bandpass-low","fx-bandpass-high"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))}))}),(_X=$("effects-reset-btn"))==null||_X.addEventListener("click",()=>{$("effects-preset").value="",["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-on","fx-bandpass-on"].forEach(id=>{const el=$(id);el&&(el.checked=!1)}),$("fx-bandpass-low").value="300",$("fx-bandpass-high").value="3400",$("fx-reverb-room").value="0.35",$("fx-reverb-wet").value="0.25",$("fx-comp-thresh").value="-20",$("fx-comp-ratio").value="4",$("fx-chorus-rate").value="1",$("fx-chorus-mix").value="0.5",$("fx-pitch-semi").value="0",["fx-reverb-room","fx-reverb-wet","fx-comp-thresh","fx-comp-ratio","fx-chorus-rate","fx-chorus-mix","fx-pitch-semi","fx-bandpass-low","fx-bandpass-high"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))})});let _effectsSourceBlob=null;(_Y=$("effects-apply-btn"))==null||_Y.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2;const chain=[];if((_a2=$("fx-reverb-on"))!=null&&_a2.checked&&chain.push({type:"reverb",params:{room_size:+$("fx-reverb-room").value,wet:+$("fx-reverb-wet").value,dry:1-+$("fx-reverb-wet").value}}),(_b2=$("fx-compressor-on"))!=null&&_b2.checked&&chain.push({type:"compressor",params:{threshold_db:+$("fx-comp-thresh").value,ratio:+$("fx-comp-ratio").value}}),(_c2=$("fx-chorus-on"))!=null&&_c2.checked&&chain.push({type:"chorus",params:{rate_hz:+$("fx-chorus-rate").value,mix:+$("fx-chorus-mix").value}}),(_d2=$("fx-pitch-on"))!=null&&_d2.checked&&chain.push({type:"pitch_shift",params:{semitones:+$("fx-pitch-semi").value}}),(_e2=$("fx-bandpass-on"))!=null&&_e2.checked&&(chain.push({type:"highpass",params:{cutoff_hz:+$("fx-bandpass-low").value}}),chain.push({type:"lowpass",params:{cutoff_hz:+$("fx-bandpass-high").value}})),!chain.length){toast("Enable at least one effect","error");return}const btn=$("effects-apply-btn"),st=$("effects-status");btn.disabled=!0,st&&(st.textContent="Processing\u2026");try{let blob=previewBlob||_effectsSourceBlob;if(!blob){const args=window._effectsSynthArgs;if(!args||typeof fetchTtsPreviewBlob!="function"){toast("Generate audio first","error");return}st&&(st.textContent="Synthesizing audio\u2026"),blob=await fetchTtsPreviewBlob(args.voice,args.text,"wav",args.instruct,args.backend)}const fd=new FormData;fd.append("audio",blob,"audio.wav"),fd.append("effects",JSON.stringify(chain));const resp=await fetch("/api/audio/effects",{method:"POST",body:fd});if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.detail||resp.statusText)}const out=await resp.blob();_effectsSourceBlob||(_effectsSourceBlob=previewBlob),previewBlob=out;const audio=$("preview-audio");audio.src=URL.createObjectURL(out),audio.style.display="",audio.play().catch(()=>{}),st&&(st.textContent="Applied."),toast("Effects applied","success")}catch(e){st&&(st.textContent=""),toast("Effects failed: "+e.message,"error")}finally{btn.disabled=!1}});let _refineOriginal=null;(function(){const inp=$("refine-llm-url");if(!inp)return;const saved=_appSettings&&_appSettings.refine_llm_url||localStorage.getItem("refine-llm-url");saved?inp.value=saved:inp.value=_appSettings&&_appSettings.llm_url||_appSettings&&_appSettings.engine_local_urls&&_appSettings.engine_local_urls.ollama||localStorage.getItem("llm-local-url-ollama")||"http://localhost:11434/v1",inp.addEventListener("input",()=>{localStorage.setItem("refine-llm-url",inp.value),_patchSettings({refine_llm_url:inp.value}),_appSettings&&(_appSettings.refine_llm_url=inp.value)})})();function updateRefineButtonState(){var _a2,_b2;const btn=$("refine-btn");btn&&(btn.disabled=!((_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)!=null&&_b2.trim()))}(_Z=$("stt-tts-text"))==null||_Z.addEventListener("input",updateRefineButtonState),(__=$("refine-btn"))==null||__.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l2,_m2,_n2,_o2,_p2;const text=(_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)==null?void 0:_b2.trim();if(!text)return;const btn=$("refine-btn"),st=$("refine-status");btn.disabled=!0,st&&(st.textContent="Refining\u2026");try{const r=await fetch("/api/refine-text",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,llm_url:((_d2=(_c2=$("refine-llm-url"))==null?void 0:_c2.value)==null?void 0:_d2.trim())||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",model:((_f2=(_e2=$("refine-model"))==null?void 0:_e2.value)==null?void 0:_f2.trim())||(_appSettings==null?void 0:_appSettings.refine_model)||(_appSettings==null?void 0:_appSettings.llm_model)||"",toggles:{fillers:(_h2=(_g2=$("refine-fillers"))==null?void 0:_g2.checked)!=null?_h2:!0,repetitions:(_j2=(_i2=$("refine-repetitions"))==null?void 0:_i2.checked)!=null?_j2:!0,corrections:(_l2=(_k2=$("refine-corrections"))==null?void 0:_k2.checked)!=null?_l2:!0,punctuation:(_n2=(_m2=$("refine-punctuation"))==null?void 0:_m2.checked)!=null?_n2:!0}})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_refineOriginal=text,$("stt-tts-text").value=d.text,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!1),st&&(st.textContent="Done."),toast("Transcription refined","success")}catch(e){st&&(st.textContent=""),toast("Refinement failed: "+e.message,"error")}finally{btn.disabled=!((_p2=(_o2=$("stt-tts-text"))==null?void 0:_o2.value)!=null&&_p2.trim())}}),(_$=$("refine-restore-btn"))==null||_$.addEventListener("click",()=>{_refineOriginal&&($("stt-tts-text").value=_refineOriginal,_refineOriginal=null,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!0),$("refine-status")&&($("refine-status").textContent=""),toast("Original transcription restored","success"))}),(_aa=$("preview-style-instruction"))==null||_aa.addEventListener("input",()=>{typeof updateBackendHelp=="function"&&updateBackendHelp()}),function(){var _a2;const batchBackendSel=$("batch-backend-select"),batchRunsSel=$("batch-runs"),batchLoadBtn=$("batch-load-voices-btn"),batchSearchInput=$("batch-voice-search"),batchSelectAllBtn=$("batch-select-all-btn"),batchSelectNoneBtn=$("batch-select-none-btn"),batchVoiceList=$("batch-voice-list"),batchSelCount=$("batch-selected-count"),batchRunBtn=$("batch-run-btn"),batchStopBtn=$("batch-stop-btn"),perfRunSelectedBtn=$("perf-run-selected-btn"),batchProgress=$("batch-progress"),batchProgLabel=$("batch-progress-label"),batchProgCount=$("batch-progress-count"),batchProgBar=$("batch-progress-bar"),batchResultsCard=$("batch-results-card"),batchResultsLabel=$("batch-results-label"),batchTbody=$("batch-tbody");if(!batchRunBtn)return;let batchStopped=!1,batchResults=[],batchVoices=[],batchSelected=new Set,batchSortField="factor",batchSortDir=-1;function populateBatchBackends(){if(!batchBackendSel)return;const cur=batchBackendSel.value;batchBackendSel.innerHTML=availableTtsBackends().map(b=>`<option value="${escHtml(b.id)}"${b.id===cur?" selected":""}>${escHtml(b.label)}</option>`).join("")||'<option value="">No backends available</option>'}populateBatchBackends();function libraryVoice(id){return(window._voices||[]).find(v=>v&&v.id===id)||null}function normalizeBatchVoice(value){const id=typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||(value==null?void 0:value.voice_id)||"",meta=typeof value=="object"?value.meta||libraryVoice(id)||value:libraryVoice(id)||null;return id?{id,label:(meta==null?void 0:meta.display_name)||(value==null?void 0:value.label)||(meta==null?void 0:meta.name)||(value==null?void 0:value.name)||id,meta}:null}function batchVoiceStats(v){var _a3,_b2;const meta=(v==null?void 0:v.meta)||libraryVoice(v==null?void 0:v.id)||{},b=meta.benchmark||{},parts=[],dur=Number(meta.duration),speed=Number((_a3=b.speed)!=null?_a3:b.avg_speed),rtf=Number((_b2=b.rtf)!=null?_b2:b.avg_rtf);return Number.isFinite(dur)&&dur>0&&parts.push(`${dur.toFixed(1)}s`),Number.isFinite(speed)&&speed>0?parts.push(`${speed.toFixed(2)}x`):Number.isFinite(rtf)&&rtf>0&&parts.push(`RTF ${rtf.toFixed(2)}`),parts.join(" \xB7 ")}function batchColor(id){const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<String(id||"").length;i++)h=h*31+String(id).charCodeAt(i)>>>0;return palette[h%palette.length]}function batchAvatar(v){const meta=(v==null?void 0:v.meta)||libraryVoice(v==null?void 0:v.id)||{};if(meta.has_picture)return`<img class="batch-voice-avatar" src="/api/voice/picture/${encodeURIComponent(v.id)}" alt="">`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,28):null;if(icon)return icon.replace(/vp-avatar/g,"batch-voice-avatar");const init=(v.label||v.id||"?")[0].toUpperCase();return`<span class="batch-voice-avatar batch-voice-avatar-init" style="background:${batchColor(meta.lang||v.id)}">${escHtml(init)}</span>`}function batchHistoryExtraFields(voice,backend){const item=batchVoices.find(v=>v.id===voice)||{},meta=item.meta||libraryVoice(voice)||{};return{voiceMeta:{label:meta.display_name||meta.name||item.label||voice,lang:meta.lang||meta.language||"",gender:meta.gender||"",flag:meta.flag||"",avatar:meta.avatar||"",has_picture:!!meta.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function batchSearchText(v){const meta=v.meta||{};return[v.id,v.label,meta.display_name,meta.name,meta.lang,meta.language,Array.isArray(meta.tags)?meta.tags.join(" "):meta.tags,batchVoiceStats(v)].filter(Boolean).join(" ").toLowerCase()}function syncRunSelectedLabel(){const count=batchSelected.size;perfRunSelectedBtn&&(perfRunSelectedBtn.disabled=count===0,perfRunSelectedBtn.innerHTML=`<span class="mdi mdi-playlist-play"></span> ${count?`Run ${count} selected voice${count===1?"":"s"}`:"Run selected voices"}`)}function updateSelCount(){const total=batchVoices.length,checked=batchSelected.size,visible=filteredBatchVoices().length;batchSelCount&&(batchSelCount.textContent=total?`${checked} of ${total} selected${visible!==total?` \xB7 ${visible} shown`:""}`:""),batchRunBtn.disabled=checked===0,syncRunSelectedLabel()}function filteredBatchVoices(){const q=((batchSearchInput==null?void 0:batchSearchInput.value)||"").trim().toLowerCase();return q?batchVoices.filter(v=>batchSearchText(v).includes(q)):batchVoices}function renderBatchVoiceList(){if(!batchVoiceList)return;const visible=filteredBatchVoices();if(!batchVoices.length){batchVoiceList.innerHTML='<div class="perf-history-empty">No voices found. Fetch voices above or reload from backend.</div>',updateSelCount();return}if(!visible.length){batchVoiceList.innerHTML='<div class="perf-history-empty">No matching voices.</div>',updateSelCount();return}const activeIds=new Set(activeVoiceIds());batchVoiceList.innerHTML=visible.map(v=>{const checked=batchSelected.has(v.id),isActive=activeIds.has(v.id),stats=batchVoiceStats(v),meta=v.meta||libraryVoice(v.id)||{},flag=meta.flag||"",lang=meta.lang||meta.language||"",gender={M:"\u2642",F:"\u2640",N:"\u26AC"}[meta.gender]||"",langGender=[flag||lang,gender].filter(Boolean).join(" ");return`<label class="batch-voice-item${isActive?" is-active":""}${checked?" is-selected":""}">
<input type="checkbox" class="batch-voice-cb" value="${escHtml(v.id)}" aria-label="Benchmark ${escHtml(v.label||v.id)}"${checked?" checked":""}>
${batchAvatar(v)}
<span class="batch-voice-main"><span class="batch-voice-name">${escHtml(v.label||v.id)}</span><span class="batch-voice-id">${escHtml(v.id)}</span></span>
${langGender?`<span class="batch-voice-meta">${escHtml(langGender)}</span>`:""}
${stats?`<span class="batch-voice-stats">${escHtml(stats)}</span>`:""}
${isActive?'<span class="batch-voice-tag">active</span>':""}
</label>`}).join(""),batchVoiceList.querySelectorAll(".batch-voice-cb").forEach(cb=>cb.addEventListener("change",()=>{cb.checked?batchSelected.add(cb.value):batchSelected.delete(cb.value),renderBatchVoiceList()})),updateSelCount()}function buildVoiceList(voices,opts={}){const previous=opts.preserve?new Set(batchSelected):null,byId=new Map;if((voices||[]).map(normalizeBatchVoice).filter(Boolean).forEach(v=>byId.set(v.id,v)),batchVoices=Array.from(byId.values()).sort((a,b)=>(a.label||a.id).localeCompare(b.label||b.id)),previous)batchSelected=new Set(batchVoices.filter(v=>previous.has(v.id)).map(v=>v.id));else if(opts.keepSelection)batchSelected=new Set(Array.from(batchSelected).filter(id=>byId.has(id)));else{const activeIds=new Set(activeVoiceIds());batchSelected=new Set(batchVoices.filter(v=>activeIds.has(v.id)).map(v=>v.id))}renderBatchVoiceList()}async function populateFromLibrary(){try{(!window._voices||!window._voices.length)&&typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary();const voices=(window._voices||[]).filter(v=>v.enabled!==!1).map(v=>({id:v.id,label:v.display_name||v.name||v.id,meta:v}));buildVoiceList(voices)}catch(e){batchVoiceList&&(batchVoiceList.innerHTML=`<div class="perf-history-empty">Voice library unavailable: ${escHtml(e.message)}</div>`),updateSelCount()}}window.populateBatchBenchmarkFromLibrary=populateFromLibrary;async function loadBatchVoicesFromBackend(){const backend=batchBackendSel.value;if(!backend){toast("Select a backend first","error");return}batchLoadBtn.disabled=!0,batchVoiceList.innerHTML='<div class="perf-history-empty">Loading from backend...</div>';try{const voices=(await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json())).map(v=>{const id=backendVoiceId(v);return{id,label:id,meta:libraryVoice(id)||(typeof v=="object"?v:null)}}).filter(v=>v.id);buildVoiceList(voices)}catch(e){batchVoiceList.innerHTML=`<div class="perf-history-empty">Failed: ${escHtml(e.message)}</div>`,updateSelCount()}finally{batchLoadBtn.disabled=!1}}batchLoadBtn.addEventListener("click",loadBatchVoicesFromBackend),batchSearchInput==null||batchSearchInput.addEventListener("input",renderBatchVoiceList),batchSelectAllBtn.addEventListener("click",()=>{batchVoices.forEach(v=>batchSelected.add(v.id)),renderBatchVoiceList()}),batchSelectNoneBtn.addEventListener("click",()=>{batchSelected.clear(),renderBatchVoiceList()}),window.addEventListener("benchmark:tts-voices-fetched",e=>{const detail=e.detail||{};detail.backend&&batchBackendSel&&(batchBackendSel.value=detail.backend),buildVoiceList(detail.voices||[])});function batchSortValue(r,field){const factor=r.ok&&r.avgRtf>0?1/r.avgRtf:null;switch(field){case"voice":return(r.voice||"").toLowerCase();case"lang":return(r.flag||r.lang||"").toLowerCase();case"gender":return(r.gender||"").toLowerCase();case"device":return typeof backendComputeDevice=="function"?backendComputeDevice(r.backend||"").toLowerCase():"";case"latency":return r.ok?r.avgLatency:999999;case"best":return r.ok?r.minLatency:999999;case"duration":return r.ok&&r.avgAudio>0?r.avgAudio:999999;case"factor":return factor!=null?factor:-1;case"time":return r.ok?r.avgLatency/1e3:999999;case"wpm":return r.ok&&r.avgAudio>0&&r.wordCount>0?Math.round(r.wordCount/(r.avgAudio/60)):-1;default:return 0}}function renderBatchResults(){if(!batchResults.length){batchResultsCard.style.display="none";return}batchResultsCard.style.display="";const sorted=batchResults.slice().sort((a,b)=>{if(a.ok&&!b.ok)return-1;if(!a.ok&&b.ok)return 1;const av=batchSortValue(a,batchSortField),bv=batchSortValue(b,batchSortField);return typeof av=="string"?av.localeCompare(bv)*batchSortDir:(av-bv)*batchSortDir}),okCount=batchResults.filter(r=>r.ok).length;batchResultsLabel&&(batchResultsLabel.textContent=`${okCount} / ${batchResults.length} voices \u2014 ${batchBackendSel.value}`),document.querySelectorAll("#batch-table th[data-sort]").forEach(th=>{th.classList.remove("perf-sort-asc","perf-sort-desc"),th.dataset.sort===batchSortField&&th.classList.add(batchSortDir===1?"perf-sort-asc":"perf-sort-desc")}),batchTbody.innerHTML=sorted.map(r=>{const factor=r.ok&&r.avgRtf>0?1/r.avgRtf:null,timeSec=r.ok&&r.avgLatency>0?r.avgLatency/1e3:null,wpm=r.ok&&r.avgAudio>0&&r.wordCount>0?Math.round(r.wordCount/(r.avgAudio/60)):null,factorCls=factor==null?"":factor>=1?"perf-good":"perf-slow",trend=r.trend?`<span class="perf-trend-badge ${r.trend.cls}" style="font-size:11px;padding:1px 6px">${r.trend.label}</span>`:"",device=typeof backendComputeDevice=="function"?backendComputeDevice(r.backend||batchBackendSel.value):"Unknown",deviceCls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass(r.backend||batchBackendSel.value):"",genderMap={M:"\u2642",F:"\u2640",N:"\u26AC"};return`<tr class="${r.ok?"":"perf-row-error"}">
<td>${escHtml(r.voice)}</td>
<td>${escHtml(r.flag||r.lang||"\u2014")}</td>
<td>${escHtml(genderMap[r.gender]||"\u2014")}</td>
<td><span class="bench-device ${deviceCls}" title="Inferred from the selected TTS backend metadata">${escHtml(device)}</span></td>
<td>${r.ok?Math.round(r.avgLatency)+" ms":"\u2014"}</td>
<td>${r.ok?r.minLatency+" ms":"\u2014"}</td>
<td>${r.ok&&r.avgAudio>0?r.avgAudio.toFixed(1)+"s":"\u2014"}</td>
<td class="${factorCls}">${factor!=null?factor.toFixed(2)+"\xD7":"\u2014"}${trend}</td>
<td>${timeSec!=null?timeSec.toFixed(1)+"s":"\u2014"}</td>
<td>${wpm!=null?wpm+" wpm":"\u2014"}</td>
<td>${r.ok?'<span class="perf-ok">OK</span>':`<span class="perf-err">${escHtml(r.error||"Failed")}</span>`}</td>
</tr>`}).join("")}(_a2=document.querySelector("#batch-table"))==null||_a2.addEventListener("click",e=>{const th=e.target.closest("th[data-sort]");if(!th)return;const field=th.dataset.sort;field===batchSortField?batchSortDir*=-1:(batchSortField=field,batchSortDir=field==="voice"||field==="lang"||field==="gender"?1:-1),renderBatchResults()});async function runBatchBenchmark(){var _a3,_b2,_c2,_d2,_e2;const topBackend=((_a3=$("perf-backend-select"))==null?void 0:_a3.value)||"";topBackend&&batchBackendSel&&batchBackendSel.value!==topBackend&&(batchBackendSel.value=topBackend);const topRuns=((_b2=$("perf-runs"))==null?void 0:_b2.value)||"";topRuns&&batchRunsSel&&Array.from(batchRunsSel.options).some(o=>o.value===topRuns)&&(batchRunsSel.value=topRuns);const singleVoice=((_c2=$("perf-voice-select"))==null?void 0:_c2.value)||"";!batchSelected.size&&singleVoice&&batchVoices.some(v=>v.id===singleVoice)&&(batchSelected.add(singleVoice),renderBatchVoiceList());const backend=batchBackendSel.value,text=(_d2=$("perf-text"))==null?void 0:_d2.value.trim(),runs=parseInt(batchRunsSel.value)||1,validIds=new Set(batchVoices.map(v=>v.id)),selected=Array.from(batchSelected).filter(id=>validIds.has(id));if(!backend){toast("Select a backend first","error");return}if(!text){toast("Enter sample text in the single-voice form above","error");return}if(!selected.length){toast("Select one or more voices first","error");return}batchStopped=!1,batchResults=[],batchRunBtn.disabled=!0,perfRunSelectedBtn&&(perfRunSelectedBtn.disabled=!0),batchStopBtn.disabled=!1,batchProgress.style.display="",batchResultsCard.style.display="none";for(let vi=0;vi<selected.length&&!batchStopped;vi++){const voice=selected[vi];batchProgLabel.textContent=`${voice} (${vi+1} / ${selected.length})`,batchProgCount.textContent=`${vi+1} / ${selected.length}`,batchProgBar.style.width=`${Math.round(vi/selected.length*100)}%`;const _vm=batchVoices.find(bv=>bv.id===voice),_meta=(_vm==null?void 0:_vm.meta)||libraryVoice(voice)||{},entry={voice,backend,ok:!1,avgLatency:0,minLatency:0,avgRtf:0,avgAudio:0,wordCount:text?text.trim().split(/\s+/).length:0,lang:_meta.lang||_meta.language||"",flag:_meta.flag||"",gender:_meta.gender||"",error:""},rowRunResults=[];for(let ri=0;ri<runs&&!batchStopped;ri++)try{const t0=performance.now(),blob=await fetchTtsPreviewBlob(voice,text,"wav","",backend),lat=Math.round(performance.now()-t0);let dur=0;try{const ac=new(window.AudioContext||window.webkitAudioContext);dur=(await ac.decodeAudioData(await blob.arrayBuffer())).duration,ac.close()}catch{}rowRunResults.push({lat,dur})}catch(e){entry.error=e.message;break}if(rowRunResults.length){entry.ok=!0,entry.avgLatency=rowRunResults.reduce((sum,r)=>sum+r.lat,0)/rowRunResults.length,entry.minLatency=Math.min(...rowRunResults.map(r=>r.lat));const durRows=rowRunResults.filter(r=>r.dur>0);entry.avgAudio=durRows.length?durRows.reduce((sum,r)=>sum+r.dur,0)/durRows.length:0;const rtfArr=durRows.map(r=>r.lat/1e3/r.dur);if(entry.avgRtf=rtfArr.length?rtfArr.reduce((sum,value)=>sum+value,0)/rtfArr.length:0,entry.avgRtf>0){const prev=perfHistoryLoad().filter(e=>e.backend===backend&&e.voice===voice&&typeof e.avgRtf=="number");if(prev.length){const prevRtf=prev[prev.length-1].avgRtf,delta=entry.avgRtf-prevRtf,pct=Math.abs(delta/Math.max(prevRtf,.01))*100;pct<5?entry.trend={cls:"perf-trend-stable",label:"stable"}:delta<0?entry.trend={cls:"perf-trend-better",label:`${pct.toFixed(0)}% faster`}:entry.trend={cls:"perf-trend-worse",label:`${pct.toFixed(0)}% slower`}}perfHistoryAdd({ts:Date.now(),backend,voice,textLen:text.length,...batchHistoryExtraFields(voice,backend),avgLatencyMs:entry.avgLatency,minLatencyMs:entry.minLatency,maxLatencyMs:Math.max(...rowRunResults.map(r=>r.lat)),avgRtf:entry.avgRtf,runCount:rowRunResults.length,allOk:!0})}}batchResults.push(entry),renderBatchResults()}batchProgBar.style.width="100%",batchProgLabel.textContent=batchStopped?`Stopped after ${batchResults.length} voice${batchResults.length!==1?"s":""}.`:`Done - ${batchResults.length} voice${batchResults.length!==1?"s":""} benchmarked.`,batchStopBtn.disabled=!0,batchRunBtn.disabled=!1,syncRunSelectedLabel(),renderPerfHistory();const ok=batchResults.filter(r=>r.ok),best=ok.slice().sort((a,b)=>a.avgRtf-b.avgRtf)[0];toast(`Batch done: ${ok.length}/${batchResults.length} OK`+(best?`, best RTF ${best.avgRtf.toFixed(2)} (${best.voice})`:""),ok.length<batchResults.length?"error":"success");const syncable=batchResults.filter(r=>r.ok&&r.avgRtf>0&&r.avgAudio>0);if(syncable.length&&typeof window.loadVoiceLibrary=="function"){const benchText=(((_e2=$("perf-text"))==null?void 0:_e2.value)||"").trim();await Promise.allSettled(syncable.map(r=>fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:r.voice,benchmark:{ok:!0,text:benchText,elapsed_sec:r.avgLatency/1e3,audio_sec:r.avgAudio,speed:1/r.avgRtf,rtf:r.avgRtf,ttfa_ms:r.minLatency}})}))),await window.loadVoiceLibrary()}}batchRunBtn.addEventListener("click",runBatchBenchmark),perfRunSelectedBtn==null||perfRunSelectedBtn.addEventListener("click",runBatchBenchmark),batchStopBtn.addEventListener("click",()=>{batchStopped=!0,batchStopBtn.disabled=!0,batchProgLabel.textContent="Stopping after current voice..."})}(),function(){let initialized=!1,benchmarkDataLoaded=!1,sttEngines=[],turnHistoryCount=0;const q=id=>document.getElementById(id),fmtMs=ms=>ms==null?"\u2014":ms>=1e3?(ms/1e3).toFixed(2)+"s":Math.round(ms)+"ms",fmtSec=sec=>sec==null?"\u2014":Number(sec).toFixed(Number(sec)<10?2:1)+"s",esc=s=>typeof escHtml=="function"?escHtml(s):String(s!=null?s:"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[c]),say=(msg,type="")=>typeof toast=="function"?toast(msg,type):console.log(msg);function setBenchTab(name){document.querySelectorAll(".bench-tab").forEach(btn=>btn.classList.toggle("active",btn.dataset.benchTab===name)),document.querySelectorAll(".bench-pane").forEach(pane=>pane.classList.toggle("active",pane.dataset.benchPane===name))}async function fetchJson(url,opts){const r=await fetch(url,opts);if(!r.ok){let detail=r.statusText||`HTTP ${r.status}`;try{const d=await r.json();detail=typeof d.detail=="string"?d.detail:JSON.stringify(d.detail||d)}catch{try{detail=await r.text()}catch{}}throw new Error(detail||`HTTP ${r.status}`)}return r.json()}function dedupeEngines(items){const seen=new Set,out=[];for(const item of items){const key=`${item.url||""}|${item.model||""}|${item.id||item.label||""}`;seen.has(key)||!item.url||(seen.add(key),out.push(item))}return out}function benchmarkModelList(engine){const values=[],add=v=>{const s=String(v||"").trim();s&&!values.includes(s)&&values.push(s)};return add(engine.model),(engine.models||[]).forEach(add),values.length||add("whisper-1"),values}async function fetchBenchmarkModels(engine){if(!(engine!=null&&engine.url))return[];try{const d=await fetchJson("/api/engine-models?type=stt&url="+encodeURIComponent(engine.url));return Array.isArray(d.models)?d.models.filter(Boolean):[]}catch(e){return console.warn("[benchmark models]",engine.label||engine.id,e),[]}}async function hydrateBenchmarkModels(engines){await Promise.all(engines.map(async engine=>{const found=await fetchBenchmarkModels(engine);engine.models=benchmarkModelList({...engine,models:found.length?found:engine.models}),engine.models.includes(engine.model)||(engine.model=engine.models[0]||engine.model||"whisper-1")}))}function renderBenchmarkModelOptions(index,filter=""){const engine=sttEngines[index],picker=document.querySelector(`.bench-model-picker[data-index="${index}"]`),list=picker==null?void 0:picker.querySelector(".bench-model-options");if(!engine||!list)return;const qv=String(filter||"").trim().toLowerCase(),models=benchmarkModelList(engine).filter(m=>!qv||m.toLowerCase().includes(qv));list.innerHTML=models.length?models.map(m=>`
<button type="button" class="bench-model-option${m===engine.model?" selected":""}" data-model="${esc(m)}">
<span>${esc(m)}</span>${m===engine.model?'<span class="mdi mdi-check"></span>':""}
</button>`).join(""):'<div class="bench-model-empty">No matching models</div>'}function setBenchmarkModel(index,model){var _a2,_b2;const engine=sttEngines[index];if(!engine)return;engine.model=String(model||"").trim()||engine.model||"whisper-1";const picker=document.querySelector(`.bench-model-picker[data-index="${index}"]`),hidden=picker==null?void 0:picker.querySelector(".bench-engine-model"),label=picker==null?void 0:picker.querySelector(".bench-model-label");hidden&&(hidden.value=engine.model),label&&(label.textContent=engine.model),(_a2=picker==null?void 0:picker.querySelector(".bench-model-menu"))==null||_a2.setAttribute("hidden",""),(_b2=picker==null?void 0:picker.querySelector(".bench-model-trigger"))==null||_b2.classList.remove("open")}function closeBenchmarkModelPickers(except=null){document.querySelectorAll(".bench-model-picker").forEach(picker=>{var _a2,_b2;except&&picker===except||((_a2=picker.querySelector(".bench-model-menu"))==null||_a2.setAttribute("hidden",""),(_b2=picker.querySelector(".bench-model-trigger"))==null||_b2.classList.remove("open"))})}function bindBenchmarkModelPickers(){document.querySelectorAll(".bench-model-picker").forEach(picker=>{var _a2;picker.addEventListener("click",ev=>ev.stopPropagation());const index=Number(picker.dataset.index),trigger=picker.querySelector(".bench-model-trigger"),menu=picker.querySelector(".bench-model-menu"),search=picker.querySelector(".bench-model-search");renderBenchmarkModelOptions(index),trigger==null||trigger.addEventListener("click",ev=>{ev.preventDefault(),ev.stopPropagation();const opening=menu==null?void 0:menu.hasAttribute("hidden");closeBenchmarkModelPickers(picker),opening?(menu==null||menu.removeAttribute("hidden"),trigger.classList.add("open"),search.value="",renderBenchmarkModelOptions(index),setTimeout(()=>search==null?void 0:search.focus(),0)):(menu==null||menu.setAttribute("hidden",""),trigger.classList.remove("open"))}),search==null||search.addEventListener("input",()=>renderBenchmarkModelOptions(index,search.value)),(_a2=picker.querySelector(".bench-model-options"))==null||_a2.addEventListener("click",ev=>{const opt=ev.target.closest(".bench-model-option");opt&&setBenchmarkModel(index,opt.dataset.model||"")})})}async function loadBenchmarkSttEngines(){const list=q("bench-stt-engines"),count=q("bench-stt-engine-count");if(!list)return;list.innerHTML='<div class="bench-engine-empty">Checking STT engines...</div>';let settings={};const items=[];try{settings=await fetchJson("/api/settings")}catch{}try{((await fetchJson("/api/stt-backends")).backends||[]).forEach(b=>items.push({id:b.id,backend:b.id,label:b.label||b.id,url:b.url,model:b.models&&b.models[0]||b.model||"whisper-1",models:b.models||[],available:!!b.available,source:"configured"}))}catch(e){console.warn("[benchmark stt backends]",e)}try{((await fetchJson("/api/local-containers")).containers||[]).filter(c=>c.role==="stt"||c.role==="stt+tts").forEach(c=>{var _a2;const url=c.port?`http://host.docker.internal:${c.port}`:"",model=((_a2=settings==null?void 0:settings.engine_local_models)==null?void 0:_a2["dc-"+c.name])||c.model||"whisper-1";items.push({id:c.name,label:c.label||c.name,url,model,models:c.models||[],available:c.running===!0||c.status==="running",source:c.stack||"docker"})})}catch(e){console.warn("[benchmark local containers]",e)}if(sttEngines=dedupeEngines(items),count&&(count.textContent=sttEngines.length?`Loading models for ${sttEngines.length} engine${sttEngines.length===1?"":"s"}...`:"No engines found."),await hydrateBenchmarkModels(sttEngines),count&&(count.textContent=sttEngines.length?`${sttEngines.length} engine${sttEngines.length===1?"":"s"} loaded.`:"No engines found."),!sttEngines.length){list.innerHTML='<div class="bench-engine-empty">No STT engines found. Check Engines -> Speech to Text.</div>';return}list.innerHTML=sttEngines.map((e,i)=>`
<label class="bench-engine-item ${e.available?"is-online":"is-offline"}">
<input type="checkbox" class="bench-stt-engine-check" data-index="${i}" ${e.available?"checked":""}>
<span class="bench-engine-main">
<strong>${esc(e.label)}</strong>
<small>${esc(e.url||"")}</small>
</span>
<span class="bench-engine-state">${e.available?"Running":"Stopped"}</span>
<div class="bench-model-picker" data-index="${i}">
<button type="button" class="bench-model-trigger" title="Choose model">
<span class="bench-model-label">${esc(e.model||benchmarkModelList(e)[0]||"whisper-1")}</span>
<span class="mdi mdi-chevron-down"></span>
</button>
<div class="bench-model-menu" hidden>
<div class="bench-model-search-row"><span class="mdi mdi-magnify"></span><input class="bench-model-search" type="search" placeholder="Search models..."></div>
<div class="bench-model-options"></div>
</div>
<input class="bench-engine-model" data-index="${i}" type="hidden" value="${esc(e.model||benchmarkModelList(e)[0]||"whisper-1")}">
</div>
</label>`).join(""),bindBenchmarkModelPickers()}async function loadBenchmarkVoiceLibrary(){const sel=q("bench-stt-library-voice");if(!sel)return;const picker=window.BenchmarkVoicePicker;picker?picker.populate("bench-stt-library-voice",[],{placeholder:"Loading voices...",empty:"Loading voices..."}):sel.innerHTML='<option value="">Loading voices...</option>';try{const usable=(await fetchJson("/api/voices")||[]).filter(v=>v&&v.enabled!==!1&&v.has_ref&&(v.transcript||"").trim()).sort((a,b)=>String(a.display_name||a.id).localeCompare(String(b.display_name||b.id))),items=usable.map(v=>({id:v.id,label:v.display_name||v.name||v.id,meta:v}));picker?picker.populate("bench-stt-library-voice",items,{placeholder:"-- choose from voice library --",empty:"No library voices with reference transcripts found"}):sel.innerHTML='<option value="">-- choose from voice library --</option>'+usable.map(v=>`<option value="${esc(v.id)}">${esc(v.display_name||v.name||v.id)}${v.duration?` (${Number(v.duration).toFixed(1)}s)`:""}</option>`).join("");const note=q("bench-stt-source-note");note&&(note.textContent=usable.length?`${usable.length} voices with reference text available.`:"No library voices with reference transcripts found.")}catch(e){picker?picker.populate("bench-stt-library-voice",[],{placeholder:"Voice library unavailable",empty:"Voice library unavailable"}):sel.innerHTML='<option value="">Voice library unavailable</option>';const note=q("bench-stt-source-note");note&&(note.textContent="Could not load voice library: "+e.message)}}async function useBenchmarkLibraryVoice(){const sel=q("bench-stt-library-voice"),voiceId=(sel==null?void 0:sel.value)||"";if(!voiceId){say("Choose a voice library sample first","error");return}const btn=q("bench-stt-load-voice"),note=q("bench-stt-source-note");btn&&(btn.disabled=!0),note&&(note.textContent="Loading voice sample...");try{const d=await fetchJson("/api/voice-load",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId})});q("bench-stt-source-id")&&(q("bench-stt-source-id").value=d.id||""),q("bench-stt-ref-text")&&(q("bench-stt-ref-text").value=d.transcript||""),q("bench-stt-audio")&&(q("bench-stt-audio").value=""),q("bench-stt-ref-file")&&(q("bench-stt-ref-file").value=""),note&&(note.textContent=`${d.voice_id||voiceId} loaded from library${d.duration?` (${Number(d.duration).toFixed(1)}s)`:""}.`),say("Voice library sample loaded","success")}catch(e){note&&(note.textContent="Voice load failed."),say("Voice load failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function selectedSttEngines(){return[...document.querySelectorAll(".bench-stt-engine-check:checked")].map(cb=>{const i=Number(cb.dataset.index),item={...sttEngines[i]},modelInput=document.querySelector(`.bench-engine-model[data-index="${i}"]`);return item.model=(modelInput==null?void 0:modelInput.value.trim())||item.model||"whisper-1",item}).filter(Boolean)}async function runSttBenchmark(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2;const audio=(_b2=(_a2=q("bench-stt-audio"))==null?void 0:_a2.files)==null?void 0:_b2[0],sourceId=((_c2=q("bench-stt-source-id"))==null?void 0:_c2.value)||"",refFile=(_e2=(_d2=q("bench-stt-ref-file"))==null?void 0:_d2.files)==null?void 0:_e2[0],refText=((_f2=q("bench-stt-ref-text"))==null?void 0:_f2.value)||"",engines=selectedSttEngines();if(!audio&&!sourceId){say("Choose an audio file or load a voice library sample first","error");return}if(!refFile&&!refText.trim()){say("Add a reference .txt or paste reference text","error");return}if(!engines.length){say("Select at least one STT engine","error");return}const btn=q("bench-stt-run"),st=q("bench-stt-status"),tbody=(_g2=q("bench-stt-table"))==null?void 0:_g2.querySelector("tbody");btn&&(btn.disabled=!0),st&&(st.textContent=`Running ${engines.length} STT benchmark${engines.length===1?"":"s"}...`),tbody&&(tbody.innerHTML=`<tr><td colspan="6" class="bench-empty">Benchmarking ${engines.length} engines...</td></tr>`);try{const fd=new FormData;audio&&fd.append("audio",audio,audio.name),sourceId&&fd.append("source_id",sourceId),refFile&&fd.append("reference_file",refFile,refFile.name),fd.append("reference_text",refText),fd.append("engines_json",JSON.stringify(engines));const d=await fetchJson("/api/stt-benchmark",{method:"POST",body:fd});renderSttResults(d),st&&(st.textContent=`Finished ${((_h2=d.results)==null?void 0:_h2.length)||0} STT engines.`),say("STT benchmark complete","success")}catch(e){st&&(st.textContent="STT benchmark failed"),tbody&&(tbody.innerHTML=`<tr><td colspan="6" class="bench-empty perf-err">${esc(e.message)}</td></tr>`),say("STT benchmark failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function renderSttResults(d){var _a2;const tbody=(_a2=q("bench-stt-table"))==null?void 0:_a2.querySelector("tbody"),summary=q("bench-stt-summary"),rows=d.results||[];summary&&(summary.innerHTML=[["Engines",String(rows.length)],["Reference words",String(d.reference_words||0)],["Best time",d.best_time_sec==null?"\u2014":fmtSec(d.best_time_sec)],["Best accuracy",d.best_accuracy==null?"\u2014":d.best_accuracy.toFixed(1)+"%"]].map(([label,val])=>`<span class="perf-stat"><strong>${esc(val)}</strong> ${esc(label)}</span>`).join("")),tbody&&(tbody.innerHTML=rows.map(r=>{const acc=r.accuracy==null?"\u2014":`${Number(r.accuracy).toFixed(1)}%`,out=r.ok?r.output||"":`\u26A0 ${r.error||"failed"}`;return`<tr class="${r.ok?"":"perf-row-error"}">
<td><strong>${esc(r.engine)}</strong><small>${esc(r.url||"")}</small></td>
<td>${esc(r.model||"")}</td>
<td><span class="bench-device ${String(r.device).includes("CUDA")?"gpu":String(r.device).includes("CPU")?"cpu":""}">${esc(r.device||"Unknown")}</span></td>
<td>${fmtSec(r.time_sec)}</td>
<td class="${r.ok?"perf-ok":"perf-err"}">${esc(acc)}</td>
<td class="bench-output" title="${esc(out)}">${esc(out)}</td>
</tr>`}).join(""))}async function runTtsBenchmark(){var _a2,_b2,_c2,_d2;const text=((_a2=q("bench-tts-text"))==null?void 0:_a2.value.trim())||"";if(!text){say("Enter a sample sentence first","error");return}const activeOnly=(((_b2=q("bench-tts-scope"))==null?void 0:_b2.value)||"active")==="active",btn=q("bench-tts-run"),st=q("bench-tts-status"),tbody=(_c2=q("bench-tts-table"))==null?void 0:_c2.querySelector("tbody");btn&&(btn.disabled=!0),st&&(st.textContent="Running TTS benchmark..."),tbody&&(tbody.innerHTML='<tr><td colspan="7" class="bench-empty">Benchmarking voices...</td></tr>');try{const d=await fetchJson("/api/voices/benchmark",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,active_only:activeOnly})});renderTtsResults(d),st&&(st.textContent=`Finished ${d.benchmarked||0} voices.`),say("TTS benchmark complete",(_d2=d.errors)!=null&&_d2.length?"error":"success")}catch(e){st&&(st.textContent="TTS benchmark failed"),tbody&&(tbody.innerHTML=`<tr><td colspan="7" class="bench-empty perf-err">${esc(e.message)}</td></tr>`),say("TTS benchmark failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function renderTtsResults(d){var _a2;const tbody=(_a2=q("bench-tts-table"))==null?void 0:_a2.querySelector("tbody"),summary=q("bench-tts-summary"),rows=d.voices||[],okRows=rows.map(r=>r.benchmark).filter(b=>b&&b.ok),avg=key=>{const vals=okRows.map(b=>Number(b[key])).filter(Number.isFinite);return vals.length?vals.reduce((a,b)=>a+b,0)/vals.length:null};summary&&(summary.innerHTML=[["Voices",String(rows.length)],["OK",String(okRows.length)],["Avg TTFA",avg("ttfa_ms")==null?"\u2014":Math.round(avg("ttfa_ms"))+"ms"],["Avg total",avg("elapsed_sec")==null?"\u2014":fmtSec(avg("elapsed_sec"))]].map(([label,val])=>`<span class="perf-stat"><strong>${esc(val)}</strong> ${esc(label)}</span>`).join("")),tbody&&(tbody.innerHTML=rows.length?rows.map(row=>{const b=row.benchmark||{},status2=b.ok?b.realtime_ok===!1?"Slow":"OK":b.error||"Failed";return`<tr class="${b.ok?"":"perf-row-error"}">
<td><strong>${esc(row.voice_id)}</strong></td>
<td>${b.ttfa_ms==null?"\u2014":Math.round(b.ttfa_ms)+"ms"}</td>
<td>${fmtSec(b.elapsed_sec)}</td>
<td>${fmtSec(b.audio_sec)}</td>
<td>${b.rtf==null?"\u2014":Number(b.rtf).toFixed(2)}</td>
<td>${b.speed==null?"\u2014":Number(b.speed).toFixed(2)+"x"}</td>
<td class="${b.ok?b.realtime_ok===!1?"perf-slow":"perf-ok":"perf-err"}" title="${esc((b.advice||[]).join(" "))}">${esc(status2)}</td>
</tr>`}).join(""):'<tr><td colspan="7" class="bench-empty">No voices benchmarked.</td></tr>')}async function initTurnControls(){try{const settings=await fetchJson("/api/settings");q("bench-turn-llm-url")&&(q("bench-turn-llm-url").value=settings.conv_llm_url||settings.llm_url||"")}catch{}try{const d=await fetchJson("/api/stt-backends"),sel=q("bench-turn-stt");sel&&(sel.innerHTML=(d.backends||[]).map(b=>`<option value="${esc(b.id)}" ${b.available?"":"disabled"}>${b.available?"\u2713":"\u2717"} ${esc(b.label)}</option>`).join("")||'<option value="configured">Configured</option>')}catch{}try{typeof refreshTtsBackendAvailability=="function"&&await refreshTtsBackendAvailability();const sel=q("bench-turn-tts-backend"),backends=(window._ttsBackends||(typeof _ttsBackends!="undefined"?_ttsBackends:[])||[]).filter(Boolean);sel&&(sel.innerHTML=backends.map(b=>`<option value="${esc(b.id)}" ${b.available?"":"disabled"}>${b.available?"\u2713":"\u2717"} ${esc(b.label)}</option>`).join("")||'<option value="voice_clone">Voice Clone</option>')}catch{}}async function fetchTurnModels(){var _a2;const btn=q("bench-turn-fetch-llm"),sel=q("bench-turn-llm-model"),url=((_a2=q("bench-turn-llm-url"))==null?void 0:_a2.value.trim())||"";btn&&(btn.disabled=!0);try{const models=(await fetchJson("/api/conversation/llm-models"+(url?"?url="+encodeURIComponent(url):""))).models||[];sel&&(sel.innerHTML=models.length?models.map(m=>`<option value="${esc(m)}">${esc(m)}</option>`).join(""):'<option value="">No models found</option>')}catch(e){sel&&(sel.innerHTML='<option value="">Fetch failed</option>'),say("Model fetch failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}async function fetchTurnVoices(){var _a2;const btn=q("bench-turn-fetch-voices"),sel=q("bench-turn-voice"),backend=((_a2=q("bench-turn-tts-backend"))==null?void 0:_a2.value)||"voice_clone",picker=window.BenchmarkVoicePicker;btn&&(btn.disabled=!0);try{const raw=await fetchJson("/api/tts-voices?backend="+encodeURIComponent(backend)),items=(Array.isArray(raw)?raw:[]).map(v=>{const id=typeof backendVoiceId=="function"?backendVoiceId(v):typeof v=="string"?v:v.id||v.voice||v.name;return id?{id,label:id,meta:(window._voices||[]).find(x=>x&&x.id===id)||(typeof v=="object"?v:null)}:null}).filter(Boolean);picker?picker.populate("bench-turn-voice",items,{placeholder:"Fetch voices",empty:"No voices"}):sel&&(sel.innerHTML=items.length?items.map(v=>`<option value="${esc(v.id)}">${esc(v.id)}</option>`).join(""):'<option value="">No voices</option>')}catch(e){picker?picker.populate("bench-turn-voice",[],{placeholder:"Fetch failed",empty:"Fetch failed"}):sel&&(sel.innerHTML='<option value="">Fetch failed</option>'),say("Voice fetch failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function updateTurnStats(stats){const max=stats.total_ms||1;[["stt",stats.stt_ms],["ttft",stats.llm_ttft_ms],["llm",stats.llm_total_ms],["tts",stats.tts_ms],["total",stats.total_ms]].forEach(([key,ms])=>{const val=q("bench-turn-val-"+key),fill=q("bench-turn-fill-"+key);val&&(val.textContent=fmtMs(ms)),fill&&(fill.style.width=max>0?Math.min(100,(ms||0)/max*100)+"%":"0%")})}function addTurnLog(role,text){var _a2;const log=q("bench-turn-log");if(!log)return null;(_a2=log.querySelector(".conv-chat-welcome"))==null||_a2.remove();const wrap=document.createElement("div");wrap.className=`conv-bubble-wrap conv-bubble-wrap--${role}`;const bubble=document.createElement("div");return bubble.className=`conv-bubble conv-bubble--${role}`,bubble.textContent=text||"",wrap.appendChild(bubble),log.appendChild(wrap),log.scrollTop=log.scrollHeight,bubble}function addTurnHistory(total,ok){var _a2;const hist=q("bench-turn-history");if(!hist)return;(_a2=hist.querySelector(".conv-history-empty"))==null||_a2.remove(),turnHistoryCount++;const item=document.createElement("div");item.className="conv-hist-item",item.innerHTML=`<span class="conv-hist-num">#${turnHistoryCount}</span><span class="${ok?"conv-hist-ok":"conv-hist-err"}"><span class="mdi ${ok?"mdi-check-circle-outline":"mdi-alert-outline"}"></span></span><span class="conv-hist-time ${ok?"conv-hist-ok":"conv-hist-err"}">${fmtMs(total)}</span>`,hist.prepend(item)}async function runTurnBenchmark(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2;const audio=(_b2=(_a2=q("bench-turn-audio"))==null?void 0:_a2.files)==null?void 0:_b2[0],text=((_c2=q("bench-turn-text"))==null?void 0:_c2.value.trim())||"";if(!audio&&!text){say("Choose turn audio or enter fallback text","error");return}const btn=q("bench-turn-run"),st=q("bench-turn-status");btn&&(btn.disabled=!0),st&&(st.textContent="Running conversation turn...");const t0=Date.now(),userBubble=addTurnLog("user",text||"Transcribing audio..."),assistantBubble=addTurnLog("assistant","...");let assistantText="",lastStats=null;try{const fd=new FormData;audio&&fd.append("audio",audio,audio.name),text&&fd.append("text",text),fd.append("stt_backend",((_d2=q("bench-turn-stt"))==null?void 0:_d2.value)||"configured"),fd.append("llm_url",((_e2=q("bench-turn-llm-url"))==null?void 0:_e2.value.trim())||""),fd.append("llm_model",((_f2=q("bench-turn-llm-model"))==null?void 0:_f2.value)||""),fd.append("tts_backend",((_g2=q("bench-turn-tts-backend"))==null?void 0:_g2.value)||"voice_clone"),fd.append("tts_voice",((_h2=q("bench-turn-voice"))==null?void 0:_h2.value)||""),fd.append("system_prompt",((_i2=q("bench-turn-system"))==null?void 0:_i2.value.trim())||"You are a helpful voice assistant."),fd.append("history","[]");const resp=await fetch("/api/conversation/turn",{method:"POST",body:fd});if(!resp.ok)throw new Error("Server error "+resp.status);const reader=resp.body.getReader(),dec=new TextDecoder;let buf="";for(;;){const{done,value}=await reader.read();if(done)break;buf+=dec.decode(value,{stream:!0});const lines=buf.split(`
`);buf=lines.pop();for(const line of lines){if(!line.startsWith("data:"))continue;let evt;try{evt=JSON.parse(line.slice(5).trim())}catch{continue}if(evt.type==="transcript"&&userBubble&&(userBubble.textContent=evt.text||"(empty)"),evt.type==="token"&&(assistantText+=evt.delta||"",assistantBubble&&(assistantBubble.textContent=assistantText)),evt.type==="llm_done"&&(assistantText=evt.text||assistantText,assistantBubble&&(assistantBubble.textContent=assistantText)),evt.type==="audio"&&evt.b64){const bytes=Uint8Array.from(atob(evt.b64),c=>c.charCodeAt(0)),url=URL.createObjectURL(new Blob([bytes],{type:evt.mime||"audio/wav"})),audioEl=document.createElement("audio");audioEl.controls=!0,audioEl.src=url,audioEl.addEventListener("ended",()=>URL.revokeObjectURL(url),{once:!0}),(_j2=q("bench-turn-log"))==null||_j2.appendChild(audioEl)}if(evt.type==="stats"&&(lastStats=evt,updateTurnStats(evt)),evt.type==="error")throw new Error(`[${evt.stage||"turn"}] ${evt.message||"Unknown error"}`)}}const total=(_k2=lastStats==null?void 0:lastStats.total_ms)!=null?_k2:Date.now()-t0;addTurnHistory(total,!0),st&&(st.textContent=`Finished in ${fmtMs(total)}.`),say("Turn benchmark complete","success")}catch(e){assistantBubble&&(assistantBubble.textContent=e.message),addTurnHistory(Date.now()-t0,!1),st&&(st.textContent="Turn benchmark failed"),say("Turn benchmark failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function bindBenchmarkSection(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2;initialized||!q("bench-stt-run")||(initialized=!0,document.querySelectorAll(".bench-tab").forEach(btn=>btn.addEventListener("click",()=>setBenchTab(btn.dataset.benchTab))),window.BenchmarkVoicePicker&&(BenchmarkVoicePicker.upgrade("bench-stt-library-voice",{placeholder:"-- choose from voice library --",empty:"No library voices with reference transcripts found"}),BenchmarkVoicePicker.upgrade("perf-voice-select",{placeholder:"-- select after fetch --",empty:"No voices"}),BenchmarkVoicePicker.upgrade("bench-turn-voice",{placeholder:"Fetch voices",empty:"No voices"})),document.addEventListener("click",()=>closeBenchmarkModelPickers()),(_a2=q("bench-stt-refresh"))==null||_a2.addEventListener("click",loadBenchmarkSttEngines),(_b2=q("bench-stt-load-voice"))==null||_b2.addEventListener("click",useBenchmarkLibraryVoice),(_c2=q("bench-stt-audio"))==null||_c2.addEventListener("change",()=>{q("bench-stt-source-id")&&(q("bench-stt-source-id").value="")}),(_d2=q("bench-stt-run"))==null||_d2.addEventListener("click",runSttBenchmark),(_e2=q("bench-tts-run"))==null||_e2.addEventListener("click",runTtsBenchmark),(_f2=q("bench-turn-fetch-llm"))==null||_f2.addEventListener("click",fetchTurnModels),(_g2=q("bench-turn-fetch-voices"))==null||_g2.addEventListener("click",fetchTurnVoices),(_h2=q("bench-turn-run"))==null||_h2.addEventListener("click",runTurnBenchmark),(_i2=q("bench-turn-tts-backend"))==null||_i2.addEventListener("change",()=>{window.BenchmarkVoicePicker?BenchmarkVoicePicker.populate("bench-turn-voice",[],{placeholder:"Fetch voices",empty:"No voices"}):q("bench-turn-voice")&&(q("bench-turn-voice").innerHTML='<option value="">Fetch voices</option>')}))}function loadBenchmarkSectionData(){bindBenchmarkSection(),!(benchmarkDataLoaded||!q("bench-stt-run"))&&(benchmarkDataLoaded=!0,loadBenchmarkSttEngines(),loadBenchmarkVoiceLibrary(),initTurnControls())}window.loadBenchmarkSectionData=loadBenchmarkSectionData,bindBenchmarkSection(),!initialized&&document.body&&new MutationObserver(()=>bindBenchmarkSection()).observe(document.body,{childList:!0,subtree:!0})}();let sttTtsSourceId=null,sttTtsOutputBlob=null,_sttBackends=[],sttTtsRecorder=null,sttTtsRecordStream=null,sttTtsRecordChunks=[],sttTtsRecordTimer=null,sttTtsRecordSecs=0;function sttTtsSelectedSttBackend(){var _a2;return((_a2=$("stt-tts-stt-backend"))==null?void 0:_a2.value)||"configured"}function sttBackendOptionHtml(selected="configured"){var _a2;if(!_sttBackends.length)return'<option value="configured">Configured Whisper/STT</option>';const preferred=_sttBackends.some(b=>b.id===selected&&b.available)?selected:((_a2=_sttBackends.find(b=>b.available))==null?void 0:_a2.id)||selected;return _sttBackends.map(b=>{const suffix=b.available?"":" (unavailable)",disabled=b.available?"":" disabled";return`<option value="${escHtml(b.id)}" ${b.id===preferred?"selected":""}${disabled}>${escHtml(b.label+suffix)}</option>`}).join("")}function updateSttBackendHelp(){const selected=sttTtsSelectedSttBackend(),b=_sttBackends.find(item=>item.id===selected)||_sttBackends.find(item=>item.available)||null,help=$("stt-tts-stt-help");if(help){if(!b){help.textContent="No STT engine status loaded yet.";return}help.innerHTML=sttBackendHelpHtml(b)}}async function refreshSttBackends(selected=""){try{_sttBackends=((await fetch("/api/stt-backends").then(r=>r.json())).backends||[]).filter(b=>b&&b.id)}catch{_sttBackends=[]}["stt-tts-stt-backend","clone-stt-backend"].forEach(id=>{const sel=$(id);if(!sel)return;const prev=selected||sel.value||"configured";sel.innerHTML=sttBackendOptionHtml(prev),sel.disabled=!_sttBackends.some(b=>b.available)}),updateSttBackendHelp(),typeof window.updateStatusBar=="function"&&window.updateStatusBar(),typeof window.refreshStatusBarEngines=="function"&&window.refreshStatusBarEngines()}function sttTtsSelectedBackend(){var _a2;return((_a2=$("stt-tts-backend-select"))==null?void 0:_a2.value)||""}function sttTtsDownload(blob,name){if(!blob)return;const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=name,a.click()}async function sttTtsUploadFile(file){if(!file)return;$("stt-tts-source-status").textContent="Uploading "+file.name+"...",sttTtsSourceId=null,sttTtsOutputBlob=null,$("stt-tts-transcribe-btn").disabled=!0,$("stt-tts-copy-preview-btn").disabled=!0,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0;const fd=new FormData;fd.append("file",file);try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();sttTtsSourceId=d.id;const audio=$("stt-tts-source-audio");audio.src="/api/audio/"+encodeURIComponent(d.id),audio.style.display="",$("stt-tts-source-status").textContent=`${d.filename||file.name} loaded (${Number(d.duration||0).toFixed(1)} s).`,$("stt-tts-transcribe-btn").disabled=!1,toast("Speech audio loaded","success")}catch(e){$("stt-tts-source-status").textContent="Upload failed.",toast("STT source upload failed: "+e.message,"error")}}async function sttTtsFetchVoices(){const backend=sttTtsSelectedBackend();if(!backend)throw new Error("No available TTS backend");const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());let voices=Array.isArray(rawVoices)?rawVoices:[];if(shouldFilterBackendVoices(backend)){const activeIds=await activeLibraryVoiceIds();voices=voices.filter(v=>activeIds.has(backendVoiceId(v)))}const sel=$("stt-tts-voice-select"),prev=sel.value;return sel.innerHTML='<option value="">-- select after fetch --</option>',voices.forEach(v=>{const id=backendVoiceId(v),opt=document.createElement("option");opt.value=opt.textContent=id,sel.appendChild(opt)}),prev&&voices.some(v=>backendVoiceId(v)===prev)&&(sel.value=prev),voices.length}(_ba=$("stt-tts-file"))==null||_ba.addEventListener("change",async()=>{const input=$("stt-tts-file");input.files&&input.files.length&&await sttTtsUploadFile(input.files[0]),input.value=""}),(_ca=$("stt-tts-refresh-stt-btn"))==null||_ca.addEventListener("click",async()=>{const btn=$("stt-tts-refresh-stt-btn");btn.disabled=!0;try{await refreshSttBackends(sttTtsSelectedSttBackend()),toast("STT engines refreshed","success")}finally{btn.disabled=!1}}),(_da=$("stt-tts-stt-backend"))==null||_da.addEventListener("change",updateSttBackendHelp);function sttTtsSetRecording(on){$("stt-tts-rec-start").disabled=on,$("stt-tts-rec-stop").disabled=!on}function sttTtsStopTracks(){sttTtsRecordStream&&sttTtsRecordStream.getTracks().forEach(t=>t.stop()),sttTtsRecordStream=null}(_ea=$("stt-tts-rec-start"))==null||_ea.addEventListener("click",async()=>{try{sttTtsRecordStream=await requestMicrophoneStream(),sttTtsRecordChunks=[],sttTtsRecordSecs=0,$("stt-tts-rec-time").textContent="0:00",$("stt-tts-source-status").textContent="Recording...",sttTtsSetRecording(!0),sttTtsRecordTimer=setInterval(()=>{sttTtsRecordSecs++,$("stt-tts-rec-time").textContent=Math.floor(sttTtsRecordSecs/60)+":"+String(sttTtsRecordSecs%60).padStart(2,"0")},1e3),sttTtsRecorder=new MediaRecorder(sttTtsRecordStream,{audioBitsPerSecond:256e3}),sttTtsRecorder.ondataavailable=e=>{e.data.size&&sttTtsRecordChunks.push(e.data)},sttTtsRecorder.onstop=async()=>{clearInterval(sttTtsRecordTimer),sttTtsRecordTimer=null,sttTtsSetRecording(!1),sttTtsStopTracks();const mime=sttTtsRecorder.mimeType||"audio/webm",blob=new Blob(sttTtsRecordChunks,{type:mime}),ext=mime.includes("ogg")?".ogg":".webm";if(!blob.size){$("stt-tts-source-status").textContent="Recording was empty.",toast("Recording was empty","error");return}await sttTtsUploadFile(new File([blob],"stt-recording"+ext,{type:mime}))},sttTtsRecorder.start(100),toast("Recording started","success")}catch(e){sttTtsSetRecording(!1),sttTtsStopTracks();const message=await microphoneErrorMessage(e);$("stt-tts-source-status").textContent=message,toast(message,"error")}}),(_fa=$("stt-tts-rec-stop"))==null||_fa.addEventListener("click",()=>{sttTtsRecorder&&sttTtsRecorder.state!=="inactive"&&sttTtsRecorder.stop()}),(_ga=$("stt-tts-transcribe-btn"))==null||_ga.addEventListener("click",async()=>{if(!sttTtsSourceId){toast("Load speech audio first","error");return}const btn=$("stt-tts-transcribe-btn");btn.disabled=!0,$("stt-tts-source-status").textContent="Transcribing...";try{const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:sttTtsSourceId,backend:sttTtsSelectedSttBackend()})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("stt-tts-text").value=d.text||"",$("stt-tts-copy-preview-btn").disabled=!(d.text||"").trim();const used=d.backend?" via "+d.backend:"";$("stt-tts-source-status").textContent="Transcription ready"+used+".",typeof updateRefineButtonState=="function"&&updateRefineButtonState(),toast("Transcription ready","success")}catch(e){$("stt-tts-source-status").textContent="Transcription failed.",toast("STT failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_ha=$("stt-tts-copy-preview-btn"))==null||_ha.addEventListener("click",()=>{const text=$("stt-tts-text").value.trim();text&&($("preview-text-area").value=text,switchTab("generation"),toast("Copied transcription to TTS Generation","success"))}),(_ia=$("stt-tts-backend-select"))==null||_ia.addEventListener("change",()=>{$("stt-tts-voice-select").innerHTML='<option value="">-- select after fetch --</option>',sttTtsOutputBlob=null,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0,updateBackendHelp()}),(_ja=$("stt-tts-fetch-voices-btn"))==null||_ja.addEventListener("click",async()=>{const btn=$("stt-tts-fetch-voices-btn");btn.disabled=!0;try{const count=await sttTtsFetchVoices();toast("Fetched "+count+" voices","success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_ka=$("stt-tts-generate-btn"))==null||_ka.addEventListener("click",async()=>{const backend=sttTtsSelectedBackend(),voice=$("stt-tts-voice-select").value,text=$("stt-tts-text").value.trim(),instruct=$("stt-tts-style-instruction").value.trim();if(!backend){toast("No available TTS backend","error");return}if(!voice){toast("Select a TTS voice","error");return}if(!text){toast("Transcribe or enter text first","error");return}const btn=$("stt-tts-generate-btn");btn.disabled=!0,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0;try{const source=await createTtsAudioSource(voice,text,backend,$("stt-tts-playback-mode").value,instruct);sttTtsOutputBlob=source.blob;const audio=$("stt-tts-output-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("stt-tts-save-mp3-btn").disabled=!1,$("stt-tts-save-wav-btn").disabled=source.streaming,toast(source.streaming?"Streaming synthesized speech":"Synthesized speech ready","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_la=$("stt-tts-save-mp3-btn"))==null||_la.addEventListener("click",async()=>{const backend=sttTtsSelectedBackend(),voice=$("stt-tts-voice-select").value,text=$("stt-tts-text").value.trim(),instruct=$("stt-tts-style-instruction").value.trim();if(!backend||!voice||!text)return;const btn=$("stt-tts-save-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend);sttTtsDownload(blob,(voice||"stt_tts")+"_stt_tts.mp3"),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_ma=$("stt-tts-save-wav-btn"))==null||_ma.addEventListener("click",()=>{sttTtsOutputBlob&&sttTtsDownload(sttTtsOutputBlob,($("stt-tts-voice-select").value||"stt_tts")+"_stt_tts.wav")});function parseScript(text){const rawLines=text.split(`
`),result=[];let state="action",currentSpeaker=null,dialogBuffer=[],actionBuffer=[];const FADE_IN_RE=/^FADE\s+IN[\s:.\-]*$/i,FIRST_SCENE_RE=/^(?:[A-Z]{0,3}\d{1,4}[A-Z]?\s+)?(INT\.|EXT\.|INT\.\/EXT\.|EXT\.\/INT\.|I\/E\.)/i;let scanLines=rawLines;const firstIdx=rawLines.findIndex(l=>{const s=l.trim();return FADE_IN_RE.test(s)||FIRST_SCENE_RE.test(s)});firstIdx>0&&(scanLines=rawLines.slice(firstIdx));function flushDialog(){if(currentSpeaker&&dialogBuffer.length){const t=dialogBuffer.join(" ").trim();t&&result.push({type:"dialog",speaker:currentSpeaker,text:t,isDirection:!1,emotion:""})}dialogBuffer=[]}function flushAction(){if(actionBuffer.length){const t=actionBuffer.join(" ").trim();t&&result.push({type:"action",speaker:"",text:t,isDirection:!0}),actionBuffer=[]}}for(const rawLine of scanLines){const isPageBreak=rawLine.startsWith("\f"),line=isPageBreak?rawLine.slice(1).trim():rawLine.trim();if(isPageBreak){flushDialog(),flushAction();const pageNum=line&&/^\d+$/.test(line)?parseInt(line,10):null;result.push({type:"pagebreak",speaker:"",text:"",page:pageNum,isDirection:!0}),state="action",currentSpeaker=null;continue}if(!line){flushDialog(),flushAction(),state==="dialog"&&(state="action",currentSpeaker=null);continue}if(/\d{1,2}\/\d{1,2}\/\d{2,4}/.test(line)&&line.length<=60||/^[A-Z]{0,3}\d{1,4}[A-Z]?$/.test(line)||/^\(?CONTINUED\)?:?$/i.test(line)||/^[A-Z]{0,3}\d{1,4}[A-Z]?\s+CONTINUED:?(\s*[A-Z]{0,3}\d{1,4}[A-Z]?)?$/i.test(line)||/^(?:[A-Z]{0,3}\d{1,4}[A-Z]?\s+)?OMITTED(?:\s*[A-Z]{0,3}\d{1,4}[A-Z]?)?$/i.test(line)){flushDialog(),flushAction(),state==="dialog"&&(state="action",currentSpeaker=null);continue}if(line.startsWith("#")){flushDialog(),flushAction();const dir=line.slice(1).trim();dir&&result.push({type:"direction",speaker:"",text:dir,isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^\[.*\]$/.test(line)){state==="dialog"&&(flushDialog(),state="character"),result.push({type:"direction",speaker:currentSpeaker||"",text:line.slice(1,-1),isDirection:!0});continue}{const m=line.match(/^(?:([A-Z]{0,3}\d{1,4}[A-Z]?)\s+)?((?:INT\.\/EXT\.|EXT\.\/INT\.|I\/E\.|INT\.|EXT\.).*)$/i);if(m){flushDialog(),flushAction();let head=m[2].trim();m[1]&&head.toUpperCase().endsWith(m[1].toUpperCase())&&(head=head.slice(0,head.length-m[1].length).trim()),result.push({type:"scene",speaker:"",text:head.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}}if(/^ACT\s+(I{1,4}|V?I{0,3}|[1-9][0-9]?|ONE|TWO|THREE|FOUR|FIVE|SIX|SEVEN|EIGHT|NINE|TEN)(\b.*)?$/i.test(line)){flushDialog(),flushAction(),result.push({type:"act",speaker:"",text:line.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^SCENE\s+(I{1,4}|V?I{0,3}|[1-9][0-9]?|ONE|TWO|THREE|FOUR|FIVE|SIX|SEVEN|EIGHT|NINE|TEN)(\b.*)?$/i.test(line)){flushDialog(),flushAction(),result.push({type:"scene",speaker:"",text:line.toUpperCase(),isDirection:!0}),state="action",currentSpeaker=null;continue}if(/^(FADE\s+(IN|OUT|TO)|CUT\s+TO|SMASH\s+CUT|MATCH\s+CUT|DISSOLVE\s+TO|BLACKOUT|LIGHTS\s+(UP|DOWN|FADE)|CURTAIN|INTERMISSION|END\s+OF\s+(PLAY|ACT))[.:]?\s*$/i.test(line)){flushDialog(),flushAction(),result.push({type:"transition",speaker:"",text:line,isDirection:!0}),state="action",currentSpeaker=null;continue}const colonMatch=line.match(/^([\p{Lu}][\p{Lu}0-9 _\-ß]{0,39}):\s+(.+)$/u);if(colonMatch&&colonMatch[1].trim().length<=24&&colonMatch[1].trim().split(/\s+/).length<=3){flushDialog(),flushAction(),currentSpeaker=colonMatch[1].trim(),dialogBuffer=[colonMatch[2].trim()],state="dialog";continue}if(/^\(.*\)$/.test(line)){state==="dialog"&&(flushDialog(),state="character"),result.push({type:"direction",speaker:currentSpeaker||"",text:line,isDirection:!0});continue}const nameRaw=line.replace(/\s*\([^)]*\)\s*$/,"").trim();if(nameRaw.length>=2&&nameRaw.length<=42&&nameRaw===nameRaw.toUpperCase()&&/^[\p{Lu}][\p{Lu}0-9 '.\-ß]+$/u.test(nameRaw)&&!/^\d+$/.test(nameRaw)&&!/\.$/.test(nameRaw)){flushDialog(),flushAction(),currentSpeaker=nameRaw,state="character";continue}if(state==="character"){dialogBuffer=[line],state="dialog";continue}if(state==="dialog"){dialogBuffer.push(line);continue}actionBuffer.push(line),state="action"}return flushDialog(),flushAction(),result}function detectCharacters(lines){const speakers=[...new Set(lines.filter(l=>l.type==="dialog").map(l=>l.speaker))],cast={};return speakers.forEach((sp,i)=>{cast[sp]={voice:"",color:SPEAKER_COLORS[i%SPEAKER_COLORS.length],instruct:"",voiceData:null}}),cast}const SPEAKER_COLORS=["#89b4fa","#a6e3a1","#f38ba8","#fab387","#f9e2af","#cba6f7","#89dceb","#74c7ec"],REH_EMOTIONS=[{value:"",emoji:"\u{1F610}",label:"Neutral"},{value:"happy, cheerful and upbeat",emoji:"\u{1F60A}",label:"Happy"},{value:"sad, melancholy, somber",emoji:"\u{1F622}",label:"Sad"},{value:"angry, forceful, aggressive",emoji:"\u{1F620}",label:"Angry"},{value:"whisper, hushed and intimate",emoji:"\u{1F92B}",label:"Whisper"},{value:"excited, enthusiastic, energetic",emoji:"\u{1F929}",label:"Excited"},{value:"scared, nervous, trembling voice",emoji:"\u{1F628}",label:"Scared"},{value:"sarcastic, dry, ironic delivery",emoji:"\u{1F60F}",label:"Sarcastic"},{value:"dramatic, theatrical, intense",emoji:"\u{1F3AD}",label:"Dramatic"},{value:"gentle, warm, tender",emoji:"\u{1F970}",label:"Gentle"},{value:"confused, uncertain, hesitant",emoji:"\u{1F615}",label:"Confused"},{value:"bored, flat, disinterested",emoji:"\u{1F611}",label:"Bored"},{value:"surprised, shocked, astonished",emoji:"\u{1F632}",label:"Surprised"},{value:"confident, authoritative, bold",emoji:"\u{1F4AA}",label:"Confident"},{value:"mysterious, dark, ominous",emoji:"\u{1F311}",label:"Mysterious"},{value:"romantic, loving, passionate",emoji:"\u2764\uFE0F",label:"Romantic"},{value:"playful, teasing, mischievous",emoji:"\u{1F608}",label:"Playful"},{value:"calm, composed, measured",emoji:"\u{1F9D8}",label:"Calm"},{value:"commanding, authoritative, military",emoji:"\u2694\uFE0F",label:"Commanding"},{value:"grieving, tearful, broken",emoji:"\u{1F62D}",label:"Grieving"}];window.REH_EMOTIONS=REH_EMOTIONS;let rehCustomEmotions=[];try{rehCustomEmotions=JSON.parse(localStorage.getItem("reh-custom-emotions")||"[]")}catch{}function getEmotionInfo(value){if(!value)return{emoji:"",label:"Pick tone"};const found=[...REH_EMOTIONS,...rehCustomEmotions].find(e=>e.value===value);return found?{emoji:found.emoji,label:found.label}:{emoji:"\u2728",label:value.length>14?value.slice(0,13)+"\u2026":value}}function renderMarkdownInline(text){let s=escHtml(text);return s=s.replace(/\*\*([^*\n]+?)\*\*/g,"<strong>$1</strong>"),s=s.replace(/\*([^*\n]+?)\*/g,"<em>$1</em>"),s=s.replace(/__([^_\n]+?)__/g,"<u>$1</u>"),s=s.replace(/~~([^~\n]+?)~~/g,"<del>$1</del>"),s=s.replace(/==([^=\n]+?)==/g,'<mark class="reh-hl">$1</mark>'),s}function stripMarkdown(text){return text.replace(/\*\*([^*\n]+?)\*\*/g,"$1").replace(/\*([^*\n]+?)\*/g,"$1").replace(/__([^_\n]+?)__/g,"$1").replace(/~~([^~\n]+?)~~/g,"$1").replace(/==([^=\n]+?)==/g,"$1")}const rehState={lines:[],cast:{},lineIndex:0,clips:[],voices:[],backend:"",playing:!1,repeat:!1,savedId:null,synthCache:new Map,staleLines:new Set,synthCancelled:!1,synthRunning:!1,skipDescriptions:!1,narratorVoice:"",practiceStart:null,practiceEnd:null,bulkMode:!1,bulkSel:new Set,bulkAnchor:null,showHidden:!1,recStream:null,recAudioCtx:null,recAnalyser:null,recSourceNode:null,recGainNode:null,recDestStream:null,recMeterRaf:null,recWaveRing:null,mediaRec:null,recChunks:[],recTimer:null,recSecs:0,lastRecBlob:null};window.rehState=rehState;async function rehDbGetAll(){const r=await fetch("/api/rehearsals");if(!r.ok)throw new Error("rehDbGetAll failed: "+r.status);return(await r.json()).rehearsals||[]}async function rehDbGetById(id){const r=await fetch("/api/rehearsals/"+id);if(r.status!==404){if(!r.ok)throw new Error("rehDbGetById failed: "+r.status);return r.json()}}async function rehDbAdd(record){const r=await fetch("/api/rehearsals",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(_rehSanitize(record))});if(!r.ok)throw new Error("rehDbAdd failed: "+r.status);return(await r.json()).id}async function rehDbPut(record){if(!record.id)return rehDbAdd(record);const r=await fetch("/api/rehearsals/"+record.id,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(_rehSanitize(record))});if(!r.ok)throw new Error("rehDbPut failed: "+r.status)}async function rehDbDelete(id){const r=await fetch("/api/rehearsals/"+id,{method:"DELETE"});if(!r.ok)throw new Error("rehDbDelete failed: "+r.status)}function _rehSanitize(rec){const out={...rec};return out.clips&&(out.clips=(out.clips||[]).map(c=>({lineIndex:c.lineIndex,speaker:c.speaker,type:c.type}))),out}(async function(){try{if((await rehDbGetAll()).length>0)return;const idbRecs=await _rehIdbGetAll().catch(()=>[]);if(!idbRecs.length)return;const r=await fetch("/api/rehearsals/migrate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(idbRecs.map(_rehSanitize))});if(r.ok){const d=await r.json();console.log(`[rehearser] migrated ${d.imported} records from IndexedDB \u2192 SQLite`)}}catch(e){console.warn("[rehearser] migration skipped:",e)}})();function _rehIdbGetAll(){return new Promise(resolve=>{const req=indexedDB.open("reh-library",1);req.onerror=()=>resolve([]),req.onsuccess=e=>{const db=e.target.result;if(!db.objectStoreNames.contains("rehearsals")){db.close(),resolve([]);return}const all=db.transaction("rehearsals","readonly").objectStore("rehearsals").getAll();all.onsuccess=ev=>{db.close(),resolve(ev.target.result||[])},all.onerror=()=>{db.close(),resolve([])}}})}window.rehDbGetById=rehDbGetById,window.rehLoadRecord=loadRecord;async function clipsToJson(clips){return Promise.all(clips.map(async c=>{if(!c.blob)return{lineIndex:c.lineIndex,speaker:c.speaker,type:c.type};const ab=await c.blob.arrayBuffer(),u8=new Uint8Array(ab);let bin="";const CHUNK=8192;for(let i=0;i<u8.length;i+=CHUNK)bin+=String.fromCharCode(...u8.subarray(i,i+CHUNK));return{lineIndex:c.lineIndex,speaker:c.speaker,type:c.type,mime:c.blob.type,b64:btoa(bin)}}))}function clipsFromJson(clips){return clips.map(c=>{if(!c.b64)return c;const bin=atob(c.b64),u8=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)u8[i]=bin.charCodeAt(i);return{...c,blob:new Blob([u8],{type:c.mime||"audio/webm"}),b64:void 0,mime:void 0}})}function rehCurrentRecord(){var _a2,_b2,_c2;const cast={};Object.entries(rehState.cast).forEach(([sp,c])=>{cast[sp]={voice:c.voice,color:c.color,instruct:c.instruct||"",lang:c.lang||"",gender:c.gender||"",tags:c.tags||"",soul:c.soul||"",ignored:!!c.ignored,hidden:!!c.hidden}});const emotions={},notes={},ignored={},hidden={};return rehState.lines.forEach((l,i)=>{l.type==="dialog"&&l.emotion&&(emotions[i]=l.emotion),l.type==="dialog"&&l.note&&(notes[i]=l.note),l.ignored&&(ignored[i]=!0),l.hidden&&(hidden[i]=!0)}),{title:((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||((_b2=$("reh-page-title"))==null?void 0:_b2.textContent)||"Untitled",script:rehState.lines.length?linesToScriptText():((_c2=$("reh-script-text"))==null?void 0:_c2.value.trim())||"",cast,emotions,notes,ignored,hidden,backend:rehState.backend,narratorVoice:rehState.narratorVoice,lineIndex:rehState.lineIndex,clips:rehState.clips.map(c=>({lineIndex:c.lineIndex,speaker:c.speaker,type:c.type,blob:c.blob||null})),updated:new Date}}function linesToScriptText(){return rehState.lines.map(line=>{switch(line.type){case"act":case"transition":return`
`+line.text+`
`;case"scene":return`
`+line.text+`
`;case"action":return`
`+line.text+`
`;case"dialog":return`
`+line.speaker+`
`+line.text+`
`;case"direction":return line.speaker?line.text+`
`:`
`+line.text+`
`;case"pagebreak":return line.page?`
\f${line.page}
`:`
\f
`;default:return""}}).join("").trim()}async function saveToLibrary(){const rec=rehCurrentRecord();if(rehState.savedId)rec.id=rehState.savedId,rec.created||(rec.created=new Date),await rehDbPut(rec);else{rec.created=new Date;const newId=await rehDbAdd(rec);rehState.savedId=newId}if(toast("Saved to library","success"),rehState._keepSavedIdOnce=!0,typeof castWriteBack=="function")try{await castWriteBack(rec.title,rehState.cast)}catch{}await renderLibraryList()}async function rehAutoSaveImport(title){var _a2,_b2;const text=(((_a2=$("reh-script-text"))==null?void 0:_a2.value)||"").trim();if(!text)return null;title&&$("reh-script-title")&&!$("reh-script-title").value.trim()&&($("reh-script-title").value=title);const rec={title:((_b2=$("reh-script-title"))==null?void 0:_b2.value.trim())||title||"Untitled",script:text,cast:{},emotions:{},notes:{},ignored:{},hidden:{},backend:rehState.backend||"",narratorVoice:rehState.narratorVoice||"",lineIndex:0,clips:[],created:new Date,updated:new Date};try{const id=await rehDbAdd(rec);return rehState.savedId=id,rehState._keepSavedIdOnce=!0,await renderLibraryList(),id}catch{return null}}async function exportToFile(){const rec=rehCurrentRecord();rec.version=1,rec.created=rec.created||new Date,rec.clips=await clipsToJson(rec.clips);const json=JSON.stringify(rec,null,2),blob=new Blob([json],{type:"application/json"}),a=document.createElement("a"),title=(rec.title||"rehearsal").replace(/[^a-z0-9_\- ]/gi,"_").slice(0,40);a.href=URL.createObjectURL(blob),a.download=title+".reh",a.click()}function exportFountain(){var _a2,_b2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||((_b2=$("reh-page-title"))==null?void 0:_b2.textContent)||"Untitled";let out=`Title: ${title}
`;rehState.lines.forEach(line=>{const t=stripMarkdown(line.text);switch(line.type){case"act":out+=`
`+t+`
`;break;case"scene":out+=`
`+t+`
`;break;case"action":out+=`
`+t+`
`;break;case"transition":out+=`
`+t+`
`;break;case"dialog":out+=`
`+line.speaker+`
`+t+`
`;break;case"direction":out+=(line.speaker?"":`
`)+t+`
`;break}});const blob=new Blob([out.trim()],{type:"text/plain"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(title.replace(/[^a-z0-9_\- ]/gi,"_")||"script")+".fountain",a.click(),toast("Exported as .fountain","success")}function exportFDX(){var _a2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled",x=s=>(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");let xml=`<?xml version="1.0" encoding="UTF-8" standalone="no" ?>
`;xml+=`<FinalDraft DocumentType="Script" Template="No" Version="1">
<Content>
`,rehState.lines.forEach(line=>{const t=x(stripMarkdown(line.text));switch(line.type){case"act":case"scene":xml+=` <Paragraph Type="Scene Heading"><Text>${t}</Text></Paragraph>
`;break;case"action":xml+=` <Paragraph Type="Action"><Text>${t}</Text></Paragraph>
`;break;case"transition":xml+=` <Paragraph Type="Transition"><Text>${t}</Text></Paragraph>
`;break;case"direction":xml+=` <Paragraph Type="Parenthetical"><Text>${t}</Text></Paragraph>
`;break;case"dialog":xml+=` <Paragraph Type="Character"><Text>${x(line.speaker)}</Text></Paragraph>
`,xml+=` <Paragraph Type="Dialogue"><Text>${t}</Text></Paragraph>
`;break}}),xml+=`</Content>
</FinalDraft>`;const blob=new Blob([xml],{type:"text/xml"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(title.replace(/[^a-z0-9_\- ]/gi,"_")||"script")+".fdx",a.click(),toast("Exported as .fdx (Final Draft XML)","success")}async function importFDX(file){const text=await file.text(),paras=new DOMParser().parseFromString(text,"text/xml").querySelectorAll("Paragraph");let result="";return paras.forEach(para=>{const type=para.getAttribute("Type")||"",t=[...para.querySelectorAll("Text")].map(el=>el.textContent).join("");if(t.trim())switch(type){case"Scene Heading":result+=`
`+t+`
`;break;case"Action":result+=`
`+t+`
`;break;case"Character":result+=`
`+t+`
`;break;case"Dialogue":result+=t+`
`;break;case"Parenthetical":result+=t+`
`;break;case"Transition":result+=`
`+t+`
`;break}}),result.trim()}function exportOSF(){var _a2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Untitled",x=s=>(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");let xml=`<?xml version="1.0" encoding="UTF-8"?>
<osf:script xmlns:osf="http://www.openscreenplayformat.org/ns/1.0">
`;xml+=` <osf:title>${x(title)}</osf:title>
<osf:content>
`,rehState.lines.forEach(line=>{const t=x(stripMarkdown(line.text));switch(line.type){case"act":case"scene":xml+=` <osf:scene-heading>${t}</osf:scene-heading>
`;break;case"action":xml+=` <osf:action>${t}</osf:action>
`;break;case"transition":xml+=` <osf:transition>${t}</osf:transition>
`;break;case"direction":xml+=` <osf:parenthetical speaker="${x(line.speaker)}">${t}</osf:parenthetical>
`;break;case"dialog":xml+=` <osf:dialogue>
<osf:character>${x(line.speaker)}</osf:character>
<osf:text>${t}</osf:text>
</osf:dialogue>
`;break}}),xml+=`</osf:content>
</osf:script>`;const blob=new Blob([xml],{type:"text/xml"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(title.replace(/[^a-z0-9_\- ]/gi,"_")||"script")+".osf",a.click(),toast("Exported as .osf (Open Screenplay Format)","success")}async function loadPdfJs(){window.pdfjsLib||(await new Promise((resolve,reject)=>{const s=document.createElement("script");s.src="/static/js/pdf/pdf.min.js",s.onload=resolve,s.onerror=reject,document.head.appendChild(s)}),pdfjsLib.GlobalWorkerOptions.workerSrc="/static/js/pdf/pdf.worker.min.js")}async function importPDFScript(file){toast("Loading PDF reader\u2026","success"),await loadPdfJs();const ab=await file.arrayBuffer(),pdf=await pdfjsLib.getDocument({data:ab}).promise,allLinesByPage=[],allLines=[];for(let p=1;p<=pdf.numPages;p++){const page=await pdf.getPage(p),content=await page.getTextContent(),pageW2=page.getViewport({scale:1}).width,rows=new Map;content.items.forEach(item=>{if(!item.str)return;const y=Math.round(item.transform[5]/3)*3;rows.has(y)||rows.set(y,[]),rows.get(y).push({x:item.transform[4],w:item.width||0,str:item.str})});const pageLines=[];[...rows.entries()].sort((a,b)=>b[0]-a[0]).forEach(([,items])=>{items.sort((a,b)=>a.x-b.x);const text=items.map(i=>i.str).join("").replace(/\s+/g," ").trim();if(!text)return;const x=items[0].x,last=items[items.length-1],right=last.x+(last.w||0);pageLines.push({x,right,pageW:pageW2,text}),allLines.push({x,right,pageW:pageW2,text})}),allLinesByPage.push(pageLines)}if(!allLines.length)return"";const leftMargin=Math.min(...allLines.map(l=>l.x)),pageW=allLines[0].pageW||612,isNoise=t=>{const s=t.trim();return!!(/^\d{1,4}\.?$/.test(s)||/^\(?CONTINUED\)?:?$/i.test(s)||/^\d+\.$/.test(s)||/^(rev\.|revised|draft)\b/i.test(s)&&s.length<24)},out=[];let prevBlank=!0,prevWasAction=!1;const push=(line,blankBefore)=>{blankBefore&&!prevBlank&&out.push(""),out.push(line),prevBlank=!1},blank=()=>{prevBlank||(out.push(""),prevBlank=!0)},nonAction=()=>{prevWasAction=!1};for(let pageIdx=0;pageIdx<pdf.numPages;pageIdx++){pageIdx>0&&(blank(),out.push(`\f${pageIdx+1}`),blank());const pageLns=allLinesByPage[pageIdx]||[];for(const ln of pageLns){const t=ln.text.trim();if(!t||isNoise(t))continue;const indent=(ln.x-leftMargin)/pageW,rightFrac=ln.right/pageW,isCaps=t===t.toUpperCase()&&/[A-Z]/.test(t),wordCount=t.split(/\s+/).length;if(/^(INT|EXT|INT\.\/EXT|EXT\.\/INT|I\/E)[\. ]/i.test(t)){blank(),push(t.toUpperCase().replace(/\s+\d+[A-Z]?\.?$/,"")),blank(),nonAction();continue}if(/^(ACT|SCENE)\s+(\d+|[IVXLC]+|ONE|TWO|THREE|FOUR|FIVE)\b/i.test(t)){blank(),push(t.toUpperCase()),blank(),nonAction();continue}if(isCaps&&(rightFrac>.72||indent>.45)&&/(TO:|CUT|FADE|DISSOLVE|SMASH|MATCH|WIPE|BLACKOUT|INTERMISSION)\b/.test(t)){blank(),push(t),blank(),nonAction();continue}if(/^\(.*\)?$/.test(t)||indent>.18&&indent<.3&&/^\(/.test(t)){push(t.startsWith("(")?t:"("+t+")",!1),nonAction();continue}const nameCore=t.replace(/\s*\([^)]*\)\s*$/,"").trim();if(indent>.22&&isCaps&&wordCount<=6&&nameCore.length<=40&&/^[A-Z0-9 .'#\-]+$/.test(nameCore)){blank(),push(t.toUpperCase()),prevBlank=!1,nonAction();continue}if(indent>.1&&indent<.34){push(t,!1),nonAction();continue}prevWasAction?out.push(t):(blank(),push(t)),prevWasAction=!0}}return out.join(`
`).replace(/\n{3,}/g,`
`).trim()}async function importFromFile(file){try{const text=await file.text(),data=JSON.parse(text);data.clips=clipsFromJson(data.clips||[]),data.created=data.created?new Date(data.created):new Date,data.updated=new Date,delete data.id;const newId=await rehDbAdd(data);toast("Imported: "+(data.title||"Untitled"),"success"),await renderLibraryList(),loadRecord({...data,id:newId})}catch(e){toast("Import failed: "+e.message,"error")}}function loadRecord(rec){typeof navTo=="function"&&navTo("s-rehearser"),$("reh-script-text")&&($("reh-script-text").value=rec.script||""),$("reh-script-title")&&($("reh-script-title").value=rec.title||""),rehState.savedId=rec.id||null,rehState._keepSavedIdOnce=!0,rehState.backend=rec.backend||"",rehState.lineIndex=rec.lineIndex||0,rehState.clips=(rec.clips||[]).map(c=>({...c})),rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.narratorVoice=rec.narratorVoice||"";const lines=parseScript(rec.script||"");rec.emotions&&lines.forEach((l,i)=>{l.type==="dialog"&&rec.emotions[i]&&(l.emotion=rec.emotions[i])}),rec.notes&&lines.forEach((l,i)=>{l.type==="dialog"&&rec.notes[i]&&(l.note=rec.notes[i])}),rec.ignored&&lines.forEach((l,i)=>{rec.ignored[i]&&(l.ignored=!0)}),rec.hidden&&lines.forEach((l,i)=>{rec.hidden[i]&&(l.hidden=!0)}),rehState.lines=lines,rehState.cast={};const detected=detectCharacters(lines);Object.entries(detected).forEach(([sp,def])=>{var _a2,_b2,_c2;const saved=(_a2=rec.cast)==null?void 0:_a2[sp];rehState.cast[sp]={voice:(_b2=saved==null?void 0:saved.voice)!=null?_b2:def.voice,color:(_c2=saved==null?void 0:saved.color)!=null?_c2:def.color,instruct:(saved==null?void 0:saved.instruct)||"",lang:(saved==null?void 0:saved.lang)||"",gender:(saved==null?void 0:saved.gender)||"",tags:(saved==null?void 0:saved.tags)||"",soul:(saved==null?void 0:saved.soul)||"",ignored:!!(saved!=null&&saved.ignored),hidden:!!(saved!=null&&saved.hidden),voiceData:saved!=null&&saved.voice&&saved.voice!=="me"?getVoiceData(saved.voice):null}}),renderCastList(),rehApplySharedCast(rec.title),refreshRehBackends().then(()=>{rec.backend&&$("reh-backend-select")&&($("reh-backend-select").value=rec.backend),rec.narratorVoice&&$("reh-narrator-voice")&&($("reh-narrator-voice").value=rec.narratorVoice)}),showPhase(2)}async function rehApplySharedCast(title){if(typeof castForProduction!="function"||!title)return;let roster;try{roster=await castForProduction(title)}catch{return}if(!roster)return;let changed=!1;Object.keys(rehState.cast||{}).forEach(sp=>{const lookupName=sp===REH_NARRATOR_KEY?"narrator":String(sp).toLowerCase(),shared=roster[lookupName];if(!shared)return;const slot=rehState.cast[sp];shared.voice&&!slot.voice&&(slot.voice=shared.voice,slot.voiceData=shared.voice!=="me"&&typeof getVoiceData=="function"?getVoiceData(shared.voice):null,changed=!0,sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=shared.voice)),shared.gender&&!slot.gender&&(slot.gender=shared.gender,changed=!0),shared.soul&&!slot.soul&&(slot.soul=shared.soul,changed=!0)}),changed&&renderCastList()}function bookCover(title){let h=0;for(let i=0;i<title.length;i++)h=(h*31+title.charCodeAt(i))%360;const h2=(h+40)%360;return{c1:`hsl(${h}, 55%, 42%)`,c2:`hsl(${h2}, 58%, 30%)`}}let rehLibView=localStorage.getItem("reh-lib-view")||"shelf";function rehTitleKey(title){return String(title||"").replace(/\s+/g," ").trim().toLowerCase()}function rehUniqueLibraryRecords(records){const byTitle=new Map,ordered=[...records].sort((a,b)=>{const at=new Date(a.updated||0).getTime(),bt=new Date(b.updated||0).getTime();return at!==bt?bt-at:(b.id||0)-(a.id||0)});for(const rec of ordered){const bucketKey=rehTitleKey(rec.title)||`__reh__${rec.id}`;byTitle.has(bucketKey)||byTitle.set(bucketKey,rec)}return[...byTitle.values()].sort((a,b)=>{const at=new Date(a.updated||0).getTime(),bt=new Date(b.updated||0).getTime();return at!==bt?bt-at:(b.id||0)-(a.id||0)})}async function renderLibraryList(){const list=$("reh-library-list");if(!list)return;let all;try{all=await rehDbGetAll()}catch{all=[]}all=rehUniqueLibraryRecords(all),list.classList.toggle("list-view",rehLibView==="list");const vt=$("reh-lib-view-toggle");if(vt&&(vt.innerHTML=`<span class="mdi mdi-${rehLibView==="list"?"view-grid":"view-list"}"></span>`),!all.length){list.innerHTML='<div class="reh-lib-empty"><span class="mdi mdi-book-open-outline"></span><p>No saved rehearsals yet.<br>Parse a script below or drop a file to start.</p></div>';return}list.innerHTML=all.map(rec=>{const speakers=Object.keys(rec.cast||{}),meCount=speakers.filter(sp=>{var _a2;return((_a2=rec.cast[sp])==null?void 0:_a2.voice)==="me"}).length,clipCount=(rec.clips||[]).filter(c=>c.type==="me"&&c.blob).length,total=parseScript(rec.script||"").filter(l=>l.type==="dialog").length,pct=total?Math.round((rec.lineIndex||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"\u2014",isCurrent=rec.id===rehState.savedId,title=rec.title||"Untitled",{c1,c2}=bookCover(title),avatars=speakers.slice(0,5).map(sp=>`<span style="background:${rec.cast[sp].color}">${sp[0].toUpperCase()}</span>`).join("");return`<div class="reh-book" data-id="${rec.id}" style="--bk1:${c1};--bk2:${c2}" title="${escHtml(title)}">
${isCurrent?'<span class="reh-book-active-tag">active</span>':""}
<div class="reh-book-actions">
<button class="reh-book-act reh-lib-export" data-id="${rec.id}" title="Export .reh"><span class="mdi mdi-export"></span></button>
<button class="reh-book-act reh-lib-delete" data-id="${rec.id}" title="Delete"><span class="mdi mdi-delete-outline"></span></button>
</div>
<div class="reh-book-title">${escHtml(title)}</div>
<div class="reh-book-meta">
<div class="reh-book-avatars">${avatars}</div>
${total} lines \xB7 ${speakers.length} cast${meCount?" \xB7 "+meCount+" me":""}${clipCount?" \xB7 "+clipCount+"\u{1F3A4}":""}
<div class="reh-book-progress"><div style="width:${pct}%"></div></div>
<div style="margin-top:4px;opacity:.8">${pct}% \xB7 ${date}</div>
</div>
</div>`}).join(""),list.querySelectorAll(".reh-book").forEach(book=>book.addEventListener("click",async e=>{if(e.target.closest(".reh-book-act"))return;const id=parseInt(book.dataset.id),rec=await rehDbGetById(id);rec?loadRecord(rec):toast("Rehearsal not found","error")})),list.querySelectorAll(".reh-lib-export").forEach(btn=>btn.addEventListener("click",async e=>{e.stopPropagation();const id=parseInt(btn.dataset.id),rec=await rehDbGetById(id);if(!rec){toast("Rehearsal not found","error");return}rec.version=1,rec.clips=await clipsToJson(rec.clips||[]);const json=JSON.stringify(rec,null,2),a=document.createElement("a"),t=(rec.title||"rehearsal").replace(/[^a-z0-9_\- ]/gi,"_").slice(0,40);a.href=URL.createObjectURL(new Blob([json],{type:"application/json"})),a.download=t+".reh",a.click()})),list.querySelectorAll(".reh-lib-delete").forEach(btn=>btn.addEventListener("click",async e=>{e.stopPropagation();const bookEl=btn.closest(".reh-book");if(bookEl.querySelector(".reh-book-del-confirm"))return;const confirmOverlay=document.createElement("div");confirmOverlay.className="reh-book-del-confirm",confirmOverlay.style.cssText="position:absolute; inset:0; background:rgba(0,0,0,0.85); color:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; border-radius:inherit; z-index:10; padding:12px; text-align:center; box-sizing:border-box;",confirmOverlay.innerHTML=`
<div style="font-weight:600; margin-bottom:8px; font-size:14px;">Delete Rehearsal?</div>
<div style="font-size:11px; opacity:0.8; margin-bottom:12px; line-height:1.4;">This cannot be undone.</div>
<div style="display:flex; gap:8px;">
<button class="btn-secondary btn-sm" id="btn-cancel-del" style="background:rgba(255,255,255,0.15); border:none; color:#fff; padding:6px 12px;">Cancel</button>
<button class="btn-primary btn-sm" id="btn-confirm-del" style="background:var(--red,#ef4444); border:none; color:#fff; padding:6px 12px;">Delete</button>
</div>
`,confirmOverlay.addEventListener("click",ce=>ce.stopPropagation()),confirmOverlay.querySelector("#btn-cancel-del").addEventListener("click",ce=>{ce.stopPropagation(),confirmOverlay.remove()}),confirmOverlay.querySelector("#btn-confirm-del").addEventListener("click",async ce=>{ce.stopPropagation(),await rehDbDelete(parseInt(btn.dataset.id)),rehState.savedId===parseInt(btn.dataset.id)&&(rehState.savedId=null),renderLibraryList()}),bookEl.appendChild(confirmOverlay)}))}(_na=$("reh-lib-view-toggle"))==null||_na.addEventListener("click",()=>{rehLibView=rehLibView==="shelf"?"list":"shelf",localStorage.setItem("reh-lib-view",rehLibView),renderLibraryList()});function getVoiceData(voiceId){return(window._voices||[]).find(v=>v.id===voiceId)||null}function _rehVoiceVisibleId(voiceId,include=""){if(!voiceId)return!1;if(include&&voiceId===include)return!0;const v=getVoiceData(voiceId);return!v||v.enabled!==!1}function voiceAvatarHtml(voiceId,color,size=32){const v=getVoiceData(voiceId),s=size+"px",radius=Math.round(size/2);if(v!=null&&v.has_picture)return`<img src="/api/voice/picture/${encodeURIComponent(voiceId)}" class="reh-char-img" style="width:${s};height:${s};border-radius:${radius}px;object-fit:cover;flex-shrink:0" alt="${escHtml(voiceId)}">`;const ic=v!=null&&v.avatar&&window.VOICE_AVATAR_ICONS?window.VOICE_AVATAR_ICONS[v.avatar]:null;if(ic)return`<span class="reh-char-img" style="width:${s};height:${s};border-radius:${radius}px;background:${color};color:#fff;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><span class="mdi ${ic}" style="font-size:${Math.round(size*.56)}px"></span></span>`;const initial=(voiceId||"?")[0].toUpperCase();return`<span class="reh-char-img" style="width:${s};height:${s};border-radius:${radius}px;background:${color};color:#fff;font-weight:700;font-size:${Math.round(size*.45)}px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0">${initial}</span>`}function _rehCharAvatarHtml(speaker,voiceId,color,size=32){const rec=(_rehLibCharsCache||[]).find(r=>String(r.name||"").trim().toLowerCase()===String(speaker||"").trim().toLowerCase());if(rec&&rec.image){const s=size+"px",radius=Math.round(size/2);return`<img src="/api/characters/${encodeURIComponent(rec.id)}/image" class="reh-char-img" style="width:${s};height:${s};border-radius:${radius}px;object-fit:cover;flex-shrink:0" alt="${escHtml(speaker)}">`}return voiceAvatarHtml(voiceId,color,size)}function showPhase(n){const impex=$("reh-impex-panel");impex&&(impex.hidden=!0);for(let i=1;i<=4;i++){const el=$("reh-phase-"+i);el&&(el.hidden=i!==n)}syncPhaseTabs(n),typeof window.onRehearserPhaseChange=="function"&&window.onRehearserPhaseChange(n)}window.showRehImpEx=function(){var _a2;stopPlay(),stopRehMic();for(let i=1;i<=4;i++){const el=$("reh-phase-"+i);el&&(el.hidden=!0)}const panel=$("reh-impex-panel");panel&&(panel.hidden=!1);const note=$("reh-impex-export-note");note&&(note.textContent=rehState.lines.length?`Current session: "${((_a2=$("reh-script-title"))==null?void 0:_a2.value)||"Untitled"}" \xB7 ${rehState.lines.filter(l=>l.type==="dialog").length} lines`:"No active session \u2014 load a script first to export.")};function syncPhaseTabs(n){const hasScript=rehState.lines.length>0,hasClips=rehState.clips.length>0;document.querySelectorAll(".reh-subtab").forEach(tab=>{const p=parseInt(tab.dataset.phase);tab.classList.toggle("active",p===n);let enabled=p===1||p===2&&hasScript||p===3&&hasScript||p===4&&(hasClips||n===4);tab.disabled=!enabled})}document.querySelectorAll(".reh-subtab").forEach(tab=>{tab.addEventListener("click",()=>{if(tab.disabled)return;const p=parseInt(tab.dataset.phase);if(p!==3&&(stopPlay(),stopRehMic(),hideRecOverlay()),p===1&&renderLibraryList(),p===3&&rehState.lines.length){buildScriptPage(),showPhase(3),highlightCurrentLine();return}p===4&&renderSummary(),showPhase(p)})}),(_oa=$("reh-file-input"))==null||_oa.addEventListener("change",function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];if(!f)return;const guess=f.name.replace(/\.[^.]+$/,"").replace(/[-_]+/g," ").trim(),r=new FileReader;r.onload=async e=>{$("reh-script-text").value=e.target.result,await rehAutoSaveImport(guess),toast("Script loaded & saved to your library \u2014 click Parse & cast","success")},r.readAsText(f),this.value=""}),(_pa=$("reh-pdf-input"))==null||_pa.addEventListener("change",async function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];if(f){this.value="";try{const text=await importPDFScript(f);$("reh-script-text")&&($("reh-script-text").value=text);const guess=f.name.replace(/\.pdf$/i,"").replace(/[-_]+/g," ").trim();await rehAutoSaveImport(guess),toast("PDF imported & saved to your library \u2014 check formatting, then click Parse & cast","success")}catch(e){toast("PDF import failed: "+e.message,"error")}}}),(_qa=$("reh-fdx-input"))==null||_qa.addEventListener("change",async function(){var _a2,_b2;const f=(_a2=this.files)==null?void 0:_a2[0];if(f){this.value="";try{const text=await importFDX(f);if($("reh-script-text")&&($("reh-script-text").value=text),!((_b2=$("reh-script-title"))!=null&&_b2.value.trim())){const guess=f.name.replace(/\.fdx$/i,"").replace(/[-_]+/g," ").trim();$("reh-script-title")&&($("reh-script-title").value=guess)}toast("FDX imported \u2014 click Parse & cast","success")}catch(e){toast("FDX import failed: "+e.message,"error")}}});async function rehImportAnyFile(f){if(!f)return;const name=f.name.toLowerCase(),setTitle=strip=>{var _a2;if(!((_a2=$("reh-script-title"))!=null&&_a2.value.trim())){const guess=f.name.replace(strip,"").replace(/[-_]+/g," ").trim();$("reh-script-title")&&($("reh-script-title").value=guess)}};try{if(name.endsWith(".reh")||name.endsWith(".json")){await importFromFile(f);return}if(name.endsWith(".pdf")){toast("Reading PDF\u2026","success");const text2=await importPDFScript(f);$("reh-script-text")&&($("reh-script-text").value=text2),setTitle(/\.pdf$/i),await rehAutoSaveImport(),toast("PDF imported & saved to your library \u2014 check formatting, then Parse & cast","success");return}if(name.endsWith(".fdx")||name.endsWith(".osf")||name.endsWith(".xml")){const text2=await importFDX(f);$("reh-script-text")&&($("reh-script-text").value=text2),setTitle(/\.(fdx|osf|xml)$/i),await rehAutoSaveImport(),toast("Script imported & saved to your library \u2014 click Parse & cast","success");return}const text=await f.text();$("reh-script-text")&&($("reh-script-text").value=text),setTitle(/\.(txt|md|fountain)$/i),await rehAutoSaveImport(),toast("Script loaded & saved to your library \u2014 click Parse & cast","success")}catch(e){toast("Import failed: "+e.message,"error")}}(function(){const dz=$("reh-dropzone"),phase=$("reh-phase-1");if(!dz||!phase)return;let depth=0;const isFileDrag=e=>e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files");phase.addEventListener("dragenter",e=>{isFileDrag(e)&&(e.preventDefault(),depth++,dz.classList.add("dragover"),dz.scrollIntoView({behavior:"smooth",block:"nearest"}))}),phase.addEventListener("dragover",e=>{isFileDrag(e)&&e.preventDefault()}),phase.addEventListener("dragleave",e=>{isFileDrag(e)&&(depth=Math.max(0,depth-1),depth===0&&dz.classList.remove("dragover"))}),phase.addEventListener("drop",async e=>{var _a2;if(!isFileDrag(e))return;e.preventDefault(),depth=0,dz.classList.remove("dragover");const f=(_a2=e.dataTransfer.files)==null?void 0:_a2[0];await rehImportAnyFile(f)})})(),(_ra=$("reh-parse-btn"))==null||_ra.addEventListener("click",()=>{var _a2;const text=(_a2=$("reh-script-text"))==null?void 0:_a2.value.trim();if(!text){toast("Paste or upload a script first","error");return}if(rehState.lines=parseScript(text),!rehState.lines.filter(l=>l.type==="dialog").length){toast("No dialog found. Use screenplay format (CAPS name + dialog) or CHAR: text.","error");return}const detected=detectCharacters(rehState.lines);Object.entries(detected).forEach(([sp])=>{rehState.cast[sp]&&(detected[sp]={...detected[sp],...rehState.cast[sp]})}),rehState.cast=detected,rehState._keepSavedIdOnce?rehState._keepSavedIdOnce=!1:rehState.savedId=null,rehState.clips=[],rehState.lineIndex=0,rehState.synthCache.clear(),rehDecodedBuffers.clear(),renderCastList(),showPhase(2),refreshRehBackends()});function castAvatarHtml(sp){const c=rehState.cast[sp];if(!c)return"";if(c.voice&&c.voice!=="me"){const vd=getVoiceData(c.voice);if(vd!=null&&vd.has_picture)return`<img src="/api/voice/picture/${encodeURIComponent(c.voice)}" style="width:36px;height:36px;border-radius:50%;object-fit:cover;border:2px solid ${c.color}" alt="">`;const ic=vd!=null&&vd.avatar&&window.VOICE_AVATAR_ICONS?window.VOICE_AVATAR_ICONS[vd.avatar]:null;if(ic)return`<span style="width:36px;height:36px;border-radius:50%;background:${c.color};color:#fff;display:inline-flex;align-items:center;justify-content:center;border:2px solid ${c.color}"><span class="mdi ${ic}" style="font-size:20px"></span></span>`}const initial=sp[0].toUpperCase();return`<span style="width:36px;height:36px;border-radius:50%;background:${c.color};color:#fff;font-weight:700;font-size:16px;display:inline-flex;align-items:center;justify-content:center;border:2px solid ${c.color}">${initial}</span>`}let _rehAudEl=null,_rehAudBtn=null;function _rehStopAudition(){if(_rehAudEl)try{_rehAudEl.pause()}catch{}if(_rehAudBtn){_rehAudBtn.classList.remove("playing");const i=_rehAudBtn.querySelector(".mdi");i&&(i.className="mdi mdi-play")}_rehAudBtn=null}function _rehAudition(url,btn){if(!url){toast("No preview audio for this voice","error");return}if(_rehAudBtn===btn&&_rehAudEl&&!_rehAudEl.paused){_rehStopAudition();return}_rehStopAudition(),_rehAudEl||(_rehAudEl=new Audio,_rehAudEl.addEventListener("ended",_rehStopAudition)),_rehAudEl.src=url,_rehAudEl.play().then(()=>{_rehAudBtn=btn,btn.classList.add("playing");const i=btn.querySelector(".mdi");i&&(i.className="mdi mdi-stop")}).catch(()=>toast("Could not play preview","error"))}const _REH_PREVIEW_TEXT="Hello \u2014 this is how this voice sounds.",_rehSearchCache={};function _rehVoiceRow({name,meta,playUrl,playVoice,isCurrent,useAttrs}){const play=`<button type="button" class="reh-vo-play"${playUrl?` data-url="${escHtml(playUrl)}"`:""}${playVoice?` data-voice="${escHtml(playVoice)}"`:""} title="Preview voice"><span class="mdi mdi-play"></span></button>`,right=isCurrent?'<span class="reh-vo-badge"><span class="mdi mdi-check-circle"></span> Selected</span>':`<button type="button" class="btn-secondary btn-sm reh-vo-use"${useAttrs||""}>Use this</button>`;return`<div class="reh-vo-row${isCurrent?" current":""}">${play}
<div class="reh-vo-info"><span class="reh-vo-name">${escHtml(name)}</span><span class="reh-vo-meta">${escHtml(meta)}</span></div>
${right}</div>`}function castOnlinePanelHtml(sp,c){const o=c.online;if(!o||!Array.isArray(o.candidates)||c.voice==="me")return"";const curCand=o.candidates.find(x=>x.voice_id&&x.voice_id===c.voice);let curRow;if(curCand)curRow=_rehVoiceRow({name:curCand.title||c.voice,meta:[curCand.gender,curCand.language].filter(Boolean).join(" \xB7 ")||"fish.audio",playUrl:curCand.sample_audio,isCurrent:!0});else if(c.voice){const vd=getVoiceData(c.voice);curRow=_rehVoiceRow({name:(vd==null?void 0:vd.name)||c.voice,meta:(vd==null?void 0:vd.group)||(vd==null?void 0:vd.tag)||"library voice",playVoice:c.voice,isCurrent:!0})}else curRow='<div class="reh-vo-row"><div class="reh-vo-info"><span class="reh-vo-name">No voice assigned</span><span class="reh-vo-meta">pick one below</span></div></div>';const alts=o.candidates.map((cand,i)=>({cand,i})).filter(({cand})=>cand.voice_id!==c.voice||!cand.voice_id),altRows=alts.map(({cand,i})=>_rehVoiceRow({name:cand.title||"Voice",meta:[cand.gender,cand.language].filter(Boolean).join(" \xB7 ")||"fish.audio",playUrl:cand.sample_audio,useAttrs:` data-act="alt" data-idx="${i}"`})).join("");return`<div class="reh-cc-online">
<div class="reh-cc-online-head"><span class="mdi mdi-earth"></span> Voice \u2014 try &amp; change
${alts.length?`<button type="button" class="reh-vo-toggle">${alts.length} match${alts.length!==1?"es":""} \u25BE</button>`:""}</div>
<div class="reh-vo-list">${curRow}</div>
${alts.length?`<div class="reh-vo-list reh-vo-alts" hidden>${altRows}</div>`:""}
<div class="reh-vo-tools">
<button type="button" class="reh-vo-tool" data-tool="local"><span class="mdi mdi-bookshelf"></span> From library</button>
<button type="button" class="reh-vo-tool" data-tool="search"><span class="mdi mdi-magnify"></span> Search online</button>
</div>
<div class="reh-vo-tool-panel reh-vo-local-panel" hidden>
<input type="search" class="reh-vo-local-input" placeholder="Filter your library voices\u2026" autocomplete="off">
<div class="reh-vo-results reh-vo-local-results"></div>
</div>
<div class="reh-vo-tool-panel reh-vo-search-panel" hidden>
<div class="reh-vo-search-row"><input type="search" class="reh-vo-search-input" placeholder="Search fish.audio (name, style, language)\u2026" autocomplete="off"><button type="button" class="btn-secondary btn-sm reh-vo-search-go">Search</button></div>
<div class="reh-vo-results reh-vo-search-results"><div class="reh-vo-hint">Type a query and hit Search to browse fish.audio voices.</div></div>
</div>
</div>`}async function _rehPreviewLocal(voiceId,btn){var _a2;const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a TTS backend first","error");return}if(_rehAudBtn===btn&&_rehAudEl&&!_rehAudEl.paused){_rehStopAudition();return}_rehStopAudition();const icon=btn.querySelector(".mdi");btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{const blob=await fetchTtsPreviewBlob(voiceId,_REH_PREVIEW_TEXT,"wav","",backend);_rehAudEl||(_rehAudEl=new Audio,_rehAudEl.addEventListener("ended",_rehStopAudition)),_rehAudEl.src=URL.createObjectURL(blob),await _rehAudEl.play(),btn.classList.remove("loading"),_rehAudBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _rehPickOneLiner(sp){const isNarr=sp===REH_NARRATOR_KEY,pool=rehState.lines.filter(l=>isNarr?l.type!=="dialog"&&l.type!=="pagebreak"&&!l.ignored:l.type==="dialog"&&l.speaker===sp).map(l=>({text:stripMarkdown(l.text||"").trim(),emotion:l.emotion||""})).filter(l=>l.text.length>=4);if(!pool.length)return null;const best=pool.map(l=>{const len=l.text.length;let score=len>=20&&len<=110?3:len<20?1:0;return/[.!?]["']?$/.test(l.text)&&(score+=1),score-=Math.abs(len-60)/120,{l,score}}).sort((a,b)=>b.score-a.score)[0].l;return{text:best.text.slice(0,220),emotion:best.emotion}}async function _rehPreviewCastLine(sp,btn){var _a2;const c=rehState.cast[sp];if(!c||!c.voice||c.voice==="me"){toast("Assign a voice first","error");return}const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a TTS backend first","error");return}if(_rehAudBtn===btn&&_rehAudEl&&!_rehAudEl.paused){_rehStopAudition();return}_rehStopAudition();const pick=_rehPickOneLiner(sp);if(!pick){toast("No lines to preview yet","error");return}const icon=btn.querySelector(".mdi");btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{const blob=await fetchTtsPreviewBlob(c.voice,_rehInlineTone(pick.text,pick.emotion),"wav",_buildInstruct(c.instruct,pick.emotion,c.voice),backend);_rehAudEl||(_rehAudEl=new Audio,_rehAudEl.addEventListener("ended",_rehStopAudition)),_rehAudEl.src=URL.createObjectURL(blob),await _rehAudEl.play(),btn.classList.remove("loading"),_rehAudBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _rehAssignLocal(sp,voiceId){const c=rehState.cast[sp];c&&(_rehStopAudition(),c.voice=voiceId,c.voiceData=getVoiceData(voiceId),c.online&&(c.online.picked=c.online.candidates.findIndex(x=>x.voice_id===voiceId)),renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect())}function _rehRenderLocalResults(card,sp,q){var _a2;const box=card.querySelector(".reh-vo-local-results");if(!box)return;const cur=(_a2=rehState.cast[sp])==null?void 0:_a2.voice,ql=(q||"").trim().toLowerCase(),matches=rehState.voices.filter(v=>{if(!_rehVoiceVisibleId(v,cur)||v===cur)return!1;if(!ql)return!0;const vd=getVoiceData(v);return v.toLowerCase().includes(ql)||((vd==null?void 0:vd.name)||"").toLowerCase().includes(ql)||((vd==null?void 0:vd.group)||"").toLowerCase().includes(ql)}).slice(0,40);if(!matches.length){box.innerHTML='<div class="reh-vo-hint">No library voices match.</div>';return}box.innerHTML=matches.map(v=>{const vd=getVoiceData(v);return _rehVoiceRow({name:(vd==null?void 0:vd.name)||v,meta:(vd==null?void 0:vd.group)||(vd==null?void 0:vd.tag)||"library voice",playVoice:v,useAttrs:` data-act="local" data-voice="${escHtml(v)}"`})}).join("")}async function _rehSearchOnline(card,sp,q){var _a2,_b2;const box=card.querySelector(".reh-vo-search-results");if(!box)return;if(q=(q||"").trim(),!q){box.innerHTML='<div class="reh-vo-hint">Enter a search term first.</div>';return}box.innerHTML='<div class="reh-vo-hint"><span class="reh-imsdb-spinner"></span> Searching fish.audio\u2026</div>';const lang=(_b2=REH_FISH_LANG[((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||"English"])!=null?_b2:"en";let items=[];try{const qs=[`search=${encodeURIComponent(q)}`,`language=${lang}`,"page_size=10","sort_by=score"];items=(await fetch("/api/fishaudio/voices?"+qs.join("&")).then(r=>r.json())).items||[]}catch{}const cands=items.filter(v=>v.sample_audio).slice(0,10).map(v=>({title:v.title,sample_audio:v.sample_audio,image:v.image||"",gender:v.gender||"",language:v.language||lang||"",description:v.description||"",sample_text:v.sample_text||v.default_text||""}));if(_rehSearchCache[sp]=cands,!cands.length){box.innerHTML=`<div class="reh-vo-hint">No playable voices found for \u201C${escHtml(q)}\u201D.</div>`;return}box.innerHTML=cands.map((cand,i)=>_rehVoiceRow({name:cand.title||"Voice",meta:[cand.gender,cand.language].filter(Boolean).join(" \xB7 ")||"fish.audio",playUrl:cand.sample_audio,useAttrs:` data-act="search" data-idx="${i}"`})).join("")}async function _rehUseSearchResult(sp,idx,btn){const c=rehState.cast[sp];if(!c)return;const cand=(_rehSearchCache[sp]||[])[idx];if(!cand)return;_rehStopAudition();const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span>';try{const vid=await _rehImportFishCandidate(sp,cand);cand.voice_id=vid,c.voice=vid,c.voiceData=getVoiceData(vid),c.online||(c.online={candidates:[],picked:0}),c.online.candidates.push(cand),c.online.picked=c.online.candidates.length-1,typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect(),toast(`Switched ${sp} to ${cand.title||"voice"}`,"success")}catch(err){btn.disabled=!1,btn.innerHTML=orig,toast("Import failed: "+err.message,"error")}}const _rehFishImported={};async function _rehImportFishCandidate(sp,cand){const key=(cand.sample_audio||cand.title||"").toLowerCase();if(key&&_rehFishImported[key]){const id=_rehFishImported[key];return rehState.voices.includes(id)||rehState.voices.push(id),id}const title=(cand.title||"").toLowerCase().trim(),existing=title&&(window._voices||[]).find(v=>(v.group==="fish-audio"||v.tag==="fish-audio")&&(v.name||"").toLowerCase().trim()===title);if(existing)return key&&(_rehFishImported[key]=existing.id),rehState.voices.includes(existing.id)||rehState.voices.push(existing.id),existing.id;const vid=`${(cand.language||"EN").slice(0,2).toUpperCase()}_${(typeof _umlautSafe=="function"?_umlautSafe(cand.title||sp):cand.title||sp).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,36)||"Voice"}`,d=await fetch("/api/quick-import-voice",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:vid,audio_url:cand.sample_audio,transcript:cand.sample_text||""})}).then(r=>r.json());if(!d.voice_id)throw new Error("import returned no id");return typeof saveMeta=="function"&&await saveMeta(d.voice_id,{name:cand.title||sp,tag:"fish-audio",group:"fish-audio",origin:"cloned",gender:(cand.gender||"").charAt(0).toUpperCase(),note:(cand.description||"").slice(0,180)}).catch(e=>logErr("fish saveMeta",e)),cand.image&&await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:d.voice_id,image_url:cand.image})}).catch(e=>logErr("fish picture",e)),rehState.voices.includes(d.voice_id)||rehState.voices.push(d.voice_id),key&&(_rehFishImported[key]=d.voice_id),d.voice_id}async function _rehUseCandidate(sp,idx,btn){const c=rehState.cast[sp],o=c&&c.online;if(!o)return;const cand=o.candidates[idx];if(!cand)return;if(_rehStopAudition(),cand.voice_id){c.voice=cand.voice_id,c.voiceData=getVoiceData(cand.voice_id),o.picked=idx,renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect();return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span>';try{const vid=await _rehImportFishCandidate(sp,cand);cand.voice_id=vid,c.voice=vid,c.voiceData=getVoiceData(vid),o.picked=idx,typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),renderCastList(),typeof populateNarratorSelect=="function"&&populateNarratorSelect(),toast(`Switched ${sp} to ${cand.title||"voice"}`,"success")}catch(err){btn.disabled=!1,btn.innerHTML=orig,toast("Import failed: "+err.message,"error")}}const REH_NARRATOR_KEY="\u{1F4D6}NARRATOR";function _ensureNarrator(){rehState.cast[REH_NARRATOR_KEY]||(rehState.cast[REH_NARRATOR_KEY]={voice:rehState.narratorVoice||"",color:"#6b7280",instruct:"",voiceData:rehState.narratorVoice?getVoiceData(rehState.narratorVoice):null,_isNarrator:!0});const n=rehState.cast[REH_NARRATOR_KEY];n.voice?(rehState.narratorVoice=n.voice,n.voiceData||(n.voiceData=getVoiceData(n.voice))):rehState.narratorVoice&&(n.voice=rehState.narratorVoice,n.voiceData=getVoiceData(rehState.narratorVoice))}const REH_CAST_LANGS=["English","German","Auto","French","Spanish","Italian","Portuguese","Dutch","Polish"],REH_CAST_GENDERS=[["","\u2014"],["F","\u2640 Female"],["M","\u2642 Male"],["N","\u26A5 Diverse"]];function _castApplyToLines(sp,fn){rehState.lines.forEach((l,i)=>{l.speaker===sp&&l.type==="dialog"&&fn(l,i)})}function _safeDomId(value){let out="";const s=String(value||"voice");for(let i=0;i<s.length;i++)out+=s.charCodeAt(i).toString(36)+"-";return out||"voice"}let _rehLibCharsCache=null,_rehLibCharsCacheBook=null;function _rehEnsureLibCharsCache(scriptTitle){!scriptTitle||_rehLibCharsCacheBook===scriptTitle||typeof clGetAllByTagOrBook!="function"||(_rehLibCharsCacheBook=scriptTitle,clGetAllByTagOrBook(scriptTitle).then(recs=>{_rehLibCharsCache=recs||[],renderCastList(),typeof renderCastStrip=="function"&&renderCastStrip(),typeof buildScriptPage=="function"&&buildScriptPage()}).catch(()=>{}))}function renderCastList(){var _a2;const list=$("reh-cast-list");if(!list)return;!(window._voices||[]).length&&typeof loadVoiceLibrary=="function"&&!rehState._castLibFetch&&(rehState._castLibFetch=!0,loadVoiceLibrary().then(()=>renderCastList()).catch(()=>{})),_ensureNarrator();const narr=REH_NARRATOR_KEY,lineCount=sp=>rehState.lines.filter(l=>l.speaker===sp&&l.type==="dialog").length,allOthers=Object.keys(rehState.cast).filter(s=>s!==narr),others=_castSortFilter(allOthers,lineCount),speakers=[narr,...others],scriptTitle=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"";_rehEnsureLibCharsCache(scriptTitle);const libRecByName=new Map((_rehLibCharsCache||[]).map(r=>[String(r.name||"").trim().toLowerCase(),r])),emotionsFor=sp=>{const seen=new Map;return rehState.lines.forEach(l=>{l.speaker===sp&&l.type==="dialog"&&l.emotion&&!seen.has(l.emotion)&&seen.set(l.emotion,getEmotionInfo(l.emotion))}),[...seen.values()]};list.innerHTML=speakers.map(sp=>{var _a3,_b2;const c=rehState.cast[sp],isNarr=sp===narr,isMe=c.voice==="me",label=isNarr?"Narrator":sp,n=lineCount(sp),sub=isNarr?"scene headings &amp; descriptions":`${n} line${n!==1?"s":""}${scriptTitle?` \xB7 ${escHtml(scriptTitle)}`:""}`,cardCls="reh-cast-card"+(isNarr?" reh-cast-narrator":"")+(c.ignored?" reh-cast-ignored":"")+(c.hidden?" reh-cast-hidden-c":""),langSel=REH_CAST_LANGS.map(l=>`<option${(c.lang||"")===l?" selected":""}>${l}</option>`).join(""),genSel=REH_CAST_GENDERS.map(([v,t])=>`<option value="${v}"${(c.gender||"")===v?" selected":""}>${t}</option>`).join(""),voiceSel=`<select id="${"reh-voice-sel-"+_safeDomId(sp)}" class="reh-voice-sel" data-speaker="${escHtml(sp)}"${isMe?' style="display:none"':""}>
<option value="">\u2014 ${isNarr?"none (skip descriptions)":"assign a voice"} \u2014</option>
${_rehAllVoiceIds(c.voice).map(v=>`<option value="${escHtml(v)}"${v===c.voice?" selected":""}>${escHtml(v)}</option>`).join("")}
</select>`,voiceName=!isMe&&c.voice?((_a3=getVoiceData(c.voice))==null?void 0:_a3.name)||c.voice:isMe?"Your mic":"",libRec=isNarr?null:libRecByName.get(String(sp).trim().toLowerCase());if(libRec){const libVoice=typeof libRec.voice=="object"?((_b2=libRec.voice)==null?void 0:_b2.id)||"":libRec.voice||"";libVoice&&(c.voice=libVoice)}const emotions=isNarr?[]:emotionsFor(sp),emotionsHtml=emotions.length?`<div class="reh-cc-emotions">${emotions.map(info=>`<span class="reh-cc-emo-chip">${info.emoji} ${escHtml(info.label)}</span>`).join("")}</div>`:"",controlsHtml=`<div class="reh-cc-controls">
<label class="reh-cc-me-row" title="Record this character with your own mic instead of a TTS voice"><input type="checkbox" class="reh-me-check" data-speaker="${escHtml(sp)}" ${isMe?"checked":""}><span><span class="mdi mdi-microphone"></span> I play this</span></label>
${!isMe&&c.voice?`<button type="button" class="reh-cc-sample-btn" data-speaker="${escHtml(sp)}" title="Hear a sample line in this voice" aria-label="Hear a sample line in this voice"><span class="mdi mdi-play"></span> Hear a line</button>`:""}
<span class="reh-cc-actions-spacer"></span>
<button class="reh-cc-iconbtn${c.ignored?" active":""}" data-act="ignore" title="Ignore \u2014 grey out &amp; skip this character\u2019s lines" aria-label="Ignore this character\u2019s lines"><span class="mdi mdi-eye-off-outline"></span></button>
<button class="reh-cc-iconbtn${c.hidden?" active":""}" data-act="hide" title="Hide this character\u2019s lines from the script" aria-label="Hide this character\u2019s lines"><span class="mdi mdi-minus-circle-outline"></span></button>
<button class="reh-cc-iconbtn reh-cc-del" data-act="delete" title="Delete this character &amp; all their lines" aria-label="Delete this character"><span class="mdi mdi-trash-can-outline"></span></button>
</div>
${emotionsHtml}`;return libRec?`<div class="${cardCls} reh-cast-card-libcard" data-speaker="${escHtml(sp)}" data-char-id="${escHtml(libRec.id)}" style="--cc-color:${c.color}">
${typeof _charCardHtml=="function"?_charCardHtml(libRec,_rehLibCharsCache||[]):""}
<div class="reh-cc-body">${controlsHtml}</div>
</div>`:`<div class="${cardCls}" data-speaker="${escHtml(sp)}" style="--cc-color:${c.color}">
<div class="reh-cc-head">
<div class="reh-cc-avatar">${castAvatarHtml(sp)}</div>
<div class="reh-cc-titles">
<div class="reh-cc-name" style="color:${c.color}">${isNarr?'<span class="mdi mdi-book-open-page-variant"></span> ':""}${escHtml(label)}</div>
<div class="reh-cc-sub">${sub}</div>
<div class="reh-cc-voicechip${!isMe&&!c.voice?" empty":""}"><span class="mdi mdi-${isMe?"microphone":"account-music-outline"}"></span> ${escHtml(voiceName||"No voice assigned")}</div>
</div>
</div>
<div class="reh-cc-body"${isMe?' style="display:none"':""}>
<div class="reh-cc-field reh-cc-voice"><label>Voice</label>${voiceSel}</div>
${controlsHtml}
<details class="reh-cc-more">
<summary><span class="mdi mdi-tune-variant"></span> More options</summary>
<div class="reh-cc-extra">
${isNarr?"":castOnlinePanelHtml(sp,c)}
${isNarr?"":`<div class="reh-cc-grid">
<div class="reh-cc-field"><label>Language</label><select class="reh-cc-lang">${langSel}</select></div>
<div class="reh-cc-field"><label>Gender</label><select class="reh-cc-gender">${genSel}</select></div>
<div class="reh-cc-field"><label>Tags</label><input type="text" class="reh-cc-tags" value="${escHtml(c.tags||"")}" placeholder="e.g. villain, raspy"></div>
</div>`}
<div class="reh-cc-field"><label>Speaking style \xB7 voice-design prompt</label>
<input type="text" class="reh-cast-instruct" value="${escHtml(c.instruct||"")}" placeholder="${isNarr?"Narrator style: calm, measured, cinematic storyteller\u2026":"British accent, commanding baritone, quietly menacing\u2026"}"></div>
${isNarr?"":`<details class="reh-cc-soul"${c.soul?" open":""}>
<summary><span class="mdi mdi-script-text-outline"></span> Character soul \xB7 LLM brief
<button class="btn-secondary btn-sm reh-cc-develop" type="button" title="Let the LLM read the script &amp; flesh out this character"><span class="mdi mdi-auto-fix"></span> Develop</button>
</summary>
<textarea class="reh-cc-soul-text" placeholder="Backstory, motivation, vocal manner\u2026 (used as the LLM brief when designing this voice)">${escHtml(c.soul||"")}</textarea>
</details>`}
</div>
</details>
</div>
</div>`}).join("");const countEl=$("reh-cast-count");if(countEl){const shown=others.length,total=allOthers.length;countEl.textContent=shown===total?`${total} ${total===1?"character":"characters"} + narrator`:`${shown} of ${total} characters`}if(typeof _wireCharCards=="function"&&(_rehLibCharsCache||[]).length){const recsById=new Map(_rehLibCharsCache.map(r=>[r.id,r]));_wireCharCards(list,recsById,_rehLibCharsCache,renderCastList,{container:list,onBack:renderCastList})}_wireCastControls(),applyCastView();const card=el=>el.closest(".reh-cast-card"),spOf=el=>card(el).dataset.speaker;window.VoicePicker&&list.querySelectorAll(".reh-voice-sel[id]").forEach(sel=>{const cur=sel.value;VoicePicker.upgrade(sel.id),cur&&VoicePicker.setValue(sel.id,cur)}),list.querySelectorAll(".reh-me-check").forEach(cb=>cb.addEventListener("change",function(){var _a3;const sp=spOf(this);rehState.cast[sp].voice=this.checked?"me":((_a3=card(this).querySelector(".reh-voice-sel"))==null?void 0:_a3.value)||"",rehState.cast[sp].voiceData=this.checked?null:getVoiceData(rehState.cast[sp].voice),renderCastList()})),list.querySelectorAll(".reh-voice-sel").forEach(sel=>sel.addEventListener("change",function(){const sp=this.dataset.speaker;rehState.cast[sp].voice=this.value,rehState.cast[sp].voiceData=getVoiceData(this.value),delete rehState.cast[sp].online,sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=this.value),renderCastList()})),list.querySelectorAll(".reh-cast-instruct").forEach(inp=>inp.addEventListener("input",function(){rehState.cast[spOf(this)].instruct=this.value})),list.querySelectorAll(".reh-cc-lang").forEach(s=>s.addEventListener("change",function(){rehState.cast[spOf(this)].lang=this.value})),list.querySelectorAll(".reh-cc-gender").forEach(s=>s.addEventListener("change",function(){rehState.cast[spOf(this)].gender=this.value})),list.querySelectorAll(".reh-cc-tags").forEach(i=>i.addEventListener("input",function(){rehState.cast[spOf(this)].tags=this.value})),list.querySelectorAll(".reh-cc-soul-text").forEach(t=>t.addEventListener("input",function(){rehState.cast[spOf(this)].soul=this.value})),list.querySelectorAll(".reh-cc-develop").forEach(b=>b.addEventListener("click",function(e){e.preventDefault(),_castDevelop(spOf(this),this)})),list.querySelectorAll(".reh-cc-iconbtn").forEach(b=>b.addEventListener("click",function(){const sp=spOf(this),act=this.dataset.act,c=rehState.cast[sp];act==="ignore"?(c.ignored=!c.ignored,_castApplyToLines(sp,l=>l.ignored=c.ignored),renderCastList()):act==="hide"?(c.hidden=!c.hidden,_castApplyToLines(sp,l=>l.hidden=c.hidden),renderCastList()):act==="delete"&&_castDeleteCharacter(sp)})),list._voDelegated||(list._voDelegated=!0,list.addEventListener("click",e=>{var _a3,_b2,_c2,_d2,_e2,_f2;const sampleBtn=e.target.closest(".reh-cc-sample-btn");if(sampleBtn){e.preventDefault(),_rehPreviewCastLine(sampleBtn.dataset.speaker,sampleBtn);return}const playBtn=e.target.closest(".reh-vo-play");if(playBtn){e.preventDefault(),playBtn.dataset.voice?_rehPreviewLocal(playBtn.dataset.voice,playBtn):_rehAudition(playBtn.dataset.url,playBtn);return}const toggle=e.target.closest(".reh-vo-toggle");if(toggle){e.preventDefault();const alts=(_a3=toggle.closest(".reh-cc-online"))==null?void 0:_a3.querySelector(".reh-vo-alts");alts&&(alts.hidden=!alts.hidden,toggle.textContent=toggle.textContent.replace(/[▾▴]\s*$/,"")+(alts.hidden?"\u25BE":"\u25B4"));return}const tool=e.target.closest(".reh-vo-tool");if(tool){e.preventDefault();const panel=tool.closest(".reh-cc-online"),want=tool.dataset.tool,localP=panel.querySelector(".reh-vo-local-panel"),searchP=panel.querySelector(".reh-vo-search-panel"),showLocal=want==="local"&&((_b2=localP==null?void 0:localP.hidden)!=null?_b2:!0),showSearch=want==="search"&&((_c2=searchP==null?void 0:searchP.hidden)!=null?_c2:!0);if(localP&&(localP.hidden=!showLocal),searchP&&(searchP.hidden=!showSearch),panel.querySelectorAll(".reh-vo-tool").forEach(t=>t.classList.toggle("active",t.dataset.tool==="local"&&showLocal||t.dataset.tool==="search"&&showSearch)),showLocal){const card2=e.target.closest(".reh-cast-card");_rehRenderLocalResults(card2,spOf(tool),""),(_d2=card2.querySelector(".reh-vo-local-input"))==null||_d2.focus()}showSearch&&((_e2=panel.querySelector(".reh-vo-search-input"))==null||_e2.focus());return}const goBtn=e.target.closest(".reh-vo-search-go");if(goBtn){e.preventDefault();const card2=e.target.closest(".reh-cast-card");_rehSearchOnline(card2,spOf(goBtn),(_f2=card2.querySelector(".reh-vo-search-input"))==null?void 0:_f2.value);return}const use=e.target.closest(".reh-vo-use");if(use){e.preventDefault();const sp=spOf(use),act=use.dataset.act;act==="local"?_rehAssignLocal(sp,use.dataset.voice):act==="search"?_rehUseSearchResult(sp,parseInt(use.dataset.idx,10),use):_rehUseCandidate(sp,parseInt(use.dataset.idx,10),use)}}),list.addEventListener("input",e=>{const li=e.target.closest(".reh-vo-local-input");li&&_rehRenderLocalResults(e.target.closest(".reh-cast-card"),spOf(li),li.value)}),list.addEventListener("keydown",e=>{const si=e.target.closest(".reh-vo-search-input");si&&e.key==="Enter"&&(e.preventDefault(),_rehSearchOnline(e.target.closest(".reh-cast-card"),spOf(si),si.value))}))}function applyCastView(){const list=$("reh-cast-list");if(!list)return;const view=rehState.castView==="list"?"list":"card";list.classList.toggle("reh-cast-view-card",view==="card"),list.classList.toggle("reh-cast-view-list",view==="list"),document.querySelectorAll("#reh-cast-view-toggle .reh-view-btn").forEach(b=>b.classList.toggle("active",b.dataset.view===view))}try{rehState.castView=localStorage.getItem("reh-cast-view")||"card"}catch{rehState.castView="card"}rehState.castFilter={search:"",gender:"",lang:""};try{rehState.castSort=JSON.parse(localStorage.getItem("reh-cast-sort"))||{by:"name",dir:"asc"}}catch{rehState.castSort={by:"name",dir:"asc"}}function _wireCastControls(){const toggle=$("reh-cast-view-toggle");if(!toggle||toggle._wired)return;toggle._wired=!0,toggle.querySelectorAll(".reh-view-btn").forEach(b=>b.addEventListener("click",()=>{rehState.castView=b.dataset.view;try{localStorage.setItem("reh-cast-view",b.dataset.view)}catch{}applyCastView()}));const search=$("reh-cast-search"),fg=$("reh-cast-filter-gender"),fl=$("reh-cast-filter-lang"),sortSel=$("reh-cast-sort"),dirBtn=$("reh-cast-sort-dir"),setDirIcon=()=>{dirBtn&&(dirBtn.dataset.dir=rehState.castSort.dir,dirBtn.querySelector(".mdi").className="mdi mdi-sort-"+(rehState.castSort.dir==="desc"?"descending":"ascending"))},persist=()=>{try{localStorage.setItem("reh-cast-sort",JSON.stringify(rehState.castSort))}catch{}};sortSel&&(sortSel.value=rehState.castSort.by),setDirIcon(),search==null||search.addEventListener("input",()=>{rehState.castFilter.search=search.value.trim(),renderCastList(),search.focus()}),fg==null||fg.addEventListener("change",()=>{rehState.castFilter.gender=fg.value,renderCastList()}),fl==null||fl.addEventListener("change",()=>{rehState.castFilter.lang=fl.value,renderCastList()}),sortSel==null||sortSel.addEventListener("change",()=>{rehState.castSort.by=sortSel.value,rehState.castSort.dir=sortSel.value==="lines"?"desc":"asc",setDirIcon(),persist(),renderCastList()}),dirBtn==null||dirBtn.addEventListener("click",()=>{rehState.castSort.dir=rehState.castSort.dir==="desc"?"asc":"desc",setDirIcon(),persist(),renderCastList()})}function _castSortFilter(speakers,lineCount){const f=rehState.castFilter||{},s=rehState.castSort||{by:"name",dir:"asc"},out=speakers.filter(sp=>{const c=rehState.cast[sp]||{};if(f.search){const q=f.search.toLowerCase();if(!sp.toLowerCase().includes(q)&&!(c.tags||"").toLowerCase().includes(q))return!1}return!(f.gender&&(c.gender||"")!==f.gender||f.lang&&(c.lang||"")!==f.lang)}),byName=(a,b)=>a.localeCompare(b,void 0,{sensitivity:"base"}),cmp={name:byName,gender:(a,b)=>(rehState.cast[a].gender||"~").localeCompare(rehState.cast[b].gender||"~")||byName(a,b),lang:(a,b)=>(rehState.cast[a].lang||"~").localeCompare(rehState.cast[b].lang||"~")||byName(a,b),lines:(a,b)=>lineCount(a)-lineCount(b)||byName(a,b),tag:(a,b)=>(rehState.cast[a].tags||"~").localeCompare(rehState.cast[b].tags||"~")||byName(a,b)}[s.by]||byName;return out.sort(cmp),s.dir==="desc"&&out.reverse(),out}function _castDeleteCharacter(sp){const n=rehState.lines.filter(l=>l.speaker===sp&&l.type==="dialog").length;if(confirm(`Delete \u201C${sp}\u201D and their ${n} line${n!==1?"s":""}? This cannot be undone.`)){for(let i=rehState.lines.length-1;i>=0;i--)rehState.lines[i].speaker===sp&&rehState.lines[i].type==="dialog"&&(rehState.lines.splice(i,1),_reindexLineState(i));delete rehState.cast[sp],renderCastList(),rehState.lines.length&&buildScriptPage(),toast(`Removed ${sp}`,"success")}}async function _castDevelop(sp,btn){var _a2,_b2,_c2,_d2,_e2;const script=((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText();if(!script){toast("Load a script first","error");return}const c=rehState.cast[sp],orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span> Developing\u2026';try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script,names:[sp],llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:c.lang||((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const info=((_e2=(await r.json()).characters)==null?void 0:_e2[0])||{};info.gender&&(c.gender=String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/,"N")),info.description&&(c.soul=info.description,c.instruct=c.instruct||info.description),renderCastList(),toast(`Developed ${sp}`,"success")}catch(e){btn.disabled=!1,btn.innerHTML=orig,toast("Develop failed: "+e.message,"error")}}async function refreshRehBackends(){rehInitLlmField();const sel=$("reh-backend-select");if(!sel)return;let backends=typeof availableTtsBackends=="function"?availableTtsBackends():[];!backends.length&&typeof refreshTtsBackendAvailability=="function"&&(await refreshTtsBackendAvailability().catch(()=>{}),backends=typeof availableTtsBackends=="function"?availableTtsBackends():[]),!backends.length&&typeof _ttsBackends!="undefined"&&Array.isArray(_ttsBackends)&&(backends=_ttsBackends);const prev=sel.value||rehState.backend;if(sel.innerHTML=backends.length?backends.map(b=>`<option value="${escHtml(b.id)}">${escHtml(b.label)}</option>`).join(""):'<option value="">No backend available</option>',prev&&[...sel.options].some(o=>o.value===prev))sel.value=prev;else{const pick=["fishspeech","voice_clone","customvoice","voice_design"].find(id=>[...sel.options].some(o=>o.value===id));pick&&(sel.value=pick)}rehState.backend=sel.value||"",_checkToneStyleSupport()}(window._ttsRefreshHooks=window._ttsRefreshHooks||[]).push(()=>{const sel=$("reh-backend-select");if(!sel)return;const backends=typeof availableTtsBackends=="function"?availableTtsBackends():[];if(!backends.length)return;const prev=sel.value||rehState.backend;sel.innerHTML=backends.map(b=>`<option value="${escHtml(b.id)}">${escHtml(b.label)}</option>`).join(""),prev&&[...sel.options].some(o=>o.value===prev)?sel.value=prev:sel.options.length&&(sel.value=sel.options[0].value),rehState.backend=sel.value||""});function _rehAllVoiceIds(include){const ids=new Set((rehState.voices||[]).filter(id=>_rehVoiceVisibleId(id,include)));return(window._voices||[]).forEach(v=>{v&&v.id&&(v.enabled!==!1||v.id===include)&&ids.add(v.id)}),include&&ids.add(include),[...ids].sort((a,b)=>a.localeCompare(b,void 0,{sensitivity:"base"}))}function populateNarratorSelect(){const sel=$("reh-narrator-voice");if(!sel)return;const cur=rehState.narratorVoice||sel.value;sel.innerHTML='<option value="">\u2014 none (skip or pause) \u2014</option>'+_rehAllVoiceIds(cur).map(v=>`<option value="${escHtml(v)}"${v===cur?" selected":""}>${escHtml(v)}</option>`).join("")}(_sa=$("reh-fetch-voices-btn"))==null||_sa.addEventListener("click",async()=>{var _a2;const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a backend first","error");return}$("reh-fetch-voices-btn").disabled=!0;try{(!window._voices||!window._voices.length)&&typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{});const raw=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());rehState.voices=(Array.isArray(raw)?raw.map(v=>typeof v=="string"?v:v.id||String(v)):[]).filter(id=>_rehVoiceVisibleId(id)),renderCastList(),populateNarratorSelect(),toast("Fetched "+rehState.voices.length+" voices","success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("reh-fetch-voices-btn").disabled=!1}}),(_ta=$("reh-narrator-voice"))==null||_ta.addEventListener("change",function(){rehState.narratorVoice=this.value}),(_ua=$("reh-back-1-btn"))==null||_ua.addEventListener("click",()=>showPhase(1));const _BUILD_INSTRUCT_TEMPLATES={DE:e=>`Sprich in einem ${e} Tonfall.`,EN:e=>`Speak in a ${e} manner.`},_BUILD_INSTRUCT_LANG_NAMES={DE:"German",FR:"French",ES:"Spanish",IT:"Italian",PT:"Portuguese",NL:"Dutch",PL:"Polish"};function _buildAccentClause(langCode){const langName=_BUILD_INSTRUCT_LANG_NAMES[langCode];return langName?`Speak with an authentic native ${langName} accent \u2014 not American-accented, not an English speaker doing ${langName}.`:langCode==="EN"?"English with a neutral British or international accent, explicitly not American/US-accented.":""}function _buildInstruct(voiceProfile,emotion,voiceId){const p=(voiceProfile||"").trim(),e=(emotion||"").trim(),langCode=String(voiceId||"").split("_")[0].toUpperCase(),accent=_buildAccentClause(langCode);if(!e&&!p&&!accent)return"";const effectiveEmotion=(e&&langCode!=="EN"&&typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(e):"")||e,emotionTmpl=_BUILD_INSTRUCT_TEMPLATES.EN;return(e?[emotionTmpl(effectiveEmotion),p].filter(Boolean):[p,accent].filter(Boolean)).join(" ")}function _rehBackendIsFish(){var _a2;let id="";try{id=typeof backendById=="function"&&((_a2=backendById(rehState.backend))==null?void 0:_a2.id)||rehState.backend||""}catch{id=rehState.backend||""}return/fish/i.test(id)}const _REH_EMOTION_DE_EN={w\u00FCtend:"angry",zornig:"angry",erz\u00FCrnt:"angry",ver\u00E4rgert:"annoyed",gereizt:"irritated",traurig:"sad",melancholisch:"melancholic",betr\u00FCbt:"sad",niedergeschlagen:"dejected",\u00E4ngstlich:"scared",furchtsam:"fearful",ver\u00E4ngstigt:"frightened",panisch:"panicked",nerv\u00F6s:"nervous",fr\u00F6hlich:"happy",gl\u00FCcklich:"happy",freudig:"joyful",heiter:"cheerful",vergn\u00FCgt:"delighted",fl\u00FCsternd:"whispering",leise:"quiet",ged\u00E4mpft:"hushed",aufgeregt:"excited",begeistert:"enthusiastic",euphorisch:"euphoric",\u00FCberrascht:"surprised",erstaunt:"astonished",verbl\u00FCfft:"amazed",genervt:"annoyed",frustriert:"frustrated",verzweifelt:"desperate",hoffnungslos:"hopeless",resigniert:"resigned",entschlossen:"determined",entschieden:"decisive",selbstbewusst:"confident",stolz:"proud",arrogant:"arrogant",sch\u00FCchtern:"shy",verlegen:"embarrassed",unsicher:"uncertain",ironisch:"sarcastic",sarkastisch:"sarcastic",sp\u00F6ttisch:"mocking",h\u00F6hnisch:"scornful",ver\u00E4chtlich:"contemptuous",ernst:"serious",streng:"stern",autorit\u00E4r:"authoritative",befehlend:"commanding",sanft:"gentle",z\u00E4rtlich:"tender",liebevoll:"loving",warm:"warm",kalt:"cold",distanziert:"distant",gleichg\u00FCltig:"indifferent",gelangweilt:"bored",geheimnisvoll:"mysterious",unheimlich:"eerie",d\u00FCster:"ominous",bedrohlich:"threatening",dramatisch:"dramatic",theatralisch:"theatrical",pathetisch:"melodramatic",ruhig:"calm",gelassen:"composed",besonnen:"measured",schockiert:"shocked",entsetzt:"horrified",fassungslos:"stunned",z\u00F6gernd:"hesitant",verwirrt:"confused",ratlos:"bewildered",unschl\u00FCssig:"undecided",weinend:"tearful",schluchzend:"sobbing",trauernd:"grieving",gebrochen:"broken",schroff:"curt",barsch:"gruff",grob:"rough",abweisend:"dismissive",freundlich:"friendly",herzlich:"warm",einladend:"welcoming",spielerisch:"playful",neckend:"teasing",frech:"cheeky",schelmisch:"mischievous",romantisch:"romantic",sehns\u00FCchtig:"longing",verliebt:"infatuated",triumphierend:"triumphant",siegessicher:"victorious",erleichtert:"relieved",beruhigt:"reassured",schuldbewusst:"guilty",reum\u00FCtig:"remorseful",neugierig:"curious",interessiert:"interested",m\u00FCde:"weary",ersch\u00F6pft:"exhausted",wehm\u00FCtig:"wistful",nostalgisch:"nostalgic",bemerkend:"remarking",feststellend:"noting",sachlich:"matter-of-fact",n\u00FCchtern:"plain",flehend:"pleading",bittend:"imploring",warnend:"warning",mahnend:"admonishing",trotzig:"defiant",rebellisch:"rebellious",erschrocken:"startled",verst\u00F6rt:"disturbed"};function _rehEmotionEnglishTag(emotion){const raw=(emotion||"").trim();if(!raw)return"";const lower=raw.split(/[,;]\s*/)[0].trim().toLowerCase();if(_REH_EMOTION_DE_EN[lower])return _REH_EMOTION_DE_EN[lower];const stem=lower.replace(/(e|er|es|en|em)$/,"");if(stem.length>=4){for(const key in _REH_EMOTION_DE_EN)if(key.startsWith(stem))return _REH_EMOTION_DE_EN[key]}return/^[a-z\- ]+$/.test(lower)?lower:""}function _rehInlineTone(text,emotion){if(!_rehBackendIsFish()||/^\s*\[/.test(text))return text;const tag=_rehEmotionEnglishTag(emotion);return tag?`[${tag}] ${text}`:text}const REH_LANG_CODE={English:"EN",German:"DE",French:"FR",Spanish:"ES",Italian:"IT",Auto:"EN"};function rehDefaultLlmUrl(){try{if(typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url)return _appSettings.llm_url}catch{}return"http://localhost:11434/v1"}function rehCollectLlmEndpoints(){const seen=new Set,results=[],add=(url,label)=>{url&&(url=url.trim(),!(!url||seen.has(url))&&(seen.add(url),results.push({url,label:label||url})))};return add(rehDefaultLlmUrl(),"Active LLM"),document.querySelectorAll(".llm-local-url-inp, [data-llm-local-key]").forEach(inp=>{var _a2,_b2,_c2;const v=(_a2=inp.value)==null?void 0:_a2.trim(),def=inp.dataset.llmLocalDefault,key=inp.dataset.llmLocalKey||inp.dataset.dcUrlKey||"",card=inp.closest('[class*="llm-local-card"], [class*="llm-local"]'),name=((_c2=(_b2=card==null?void 0:card.querySelector(".llm-local-name"))==null?void 0:_b2.textContent)==null?void 0:_c2.trim())||key;add(v||def,name)}),document.querySelectorAll(".dc-url-inp").forEach(inp=>{var _a2,_b2,_c2;const card=inp.closest('[class*="llm-local-card"]');if(!card)return;const name=((_b2=(_a2=card.querySelector(".llm-local-name"))==null?void 0:_a2.textContent)==null?void 0:_b2.trim())||"";add(((_c2=inp.value)==null?void 0:_c2.trim())||inp.dataset.dcDefault,name)}),[["http://localhost:11434/v1","Ollama"],["http://localhost:8000/v1","vLLM"],["http://localhost:1234/v1","LM Studio"],["http://localhost:28080/v1","llama-swap"],["http://localhost:14000/v1","LiteLLM"]].forEach(([u,l])=>add(u,l)),results}function rehInitLlmField(){const u=$("reh-llm-url");if(!u)return;u.value||(u.value=rehDefaultLlmUrl());const dl=$("reh-llm-url-list");dl&&(dl.innerHTML=rehCollectLlmEndpoints().map(e=>`<option value="${e.url.replace(/"/g,"&quot;")}">${e.label}</option>`).join(""))}(_va=$("reh-llm-refresh"))==null||_va.addEventListener("click",async()=>{var _a2;const url=((_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim())||rehDefaultLlmUrl(),sel=$("reh-llm-model");if(sel){sel.innerHTML='<option value="">Loading\u2026</option>';try{const models=(await(await fetch("/api/conversation/llm-models?url="+encodeURIComponent(url))).json()).models||[];sel.innerHTML='<option value="">\u2014 default \u2014</option>'+models.map(m=>`<option value="${escHtml(m)}">${escHtml(m)}</option>`).join("");const want=typeof _appSettings!="undefined"&&_appSettings?_appSettings.llm_model:"";want&&models.includes(want)&&(sel.value=want),toast(models.length?`Found ${models.length} models`:"No models found",models.length?"success":"error")}catch(e){sel.innerHTML='<option value="">\u2014 default \u2014</option>',toast("Could not list models: "+e.message,"error")}}});const REH_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"};function _pickVoiceAvatar(gender,desc,speaker){const d=((desc||"")+" "+(speaker||"")).toLowerCase();return/\b(robot|android|synthetic|artificial|computer|machine|cyborg|a\.?i\.?|operating system|\bos\b|digital|hologram|drone)\b/.test(d)?"robot":/\b(animal|creature|beast|dragon|monster|cat|dog|wolf|lion|bird|horse|dino|dinosaur|alien)\b/.test(d)?"animal":gender==="M"?"male":gender==="F"?"female":"neutral"}let rehDesignCancelled=!1;(_wa=$("reh-autodesign-cancel"))==null||_wa.addEventListener("click",()=>{rehDesignCancelled=!0}),(_xa=$("reh-autodesign-btn"))==null||_xa.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2;if(!((_a2=$("reh-backend-select"))==null?void 0:_a2.value)){toast("Select a TTS backend first","error");return}const speakers=Object.keys(rehState.cast);if(!speakers.length){toast("No characters to design for","error");return}const script=((_b2=$("reh-script-text"))==null?void 0:_b2.value.trim())||linesToScriptText(),llmUrl=((_c2=$("reh-llm-url"))==null?void 0:_c2.value.trim())||rehDefaultLlmUrl(),llmModel=((_d2=$("reh-llm-model"))==null?void 0:_d2.value)||"",language=((_e2=$("reh-design-lang"))==null?void 0:_e2.value)||"English",langCode=REH_LANG_CODE[language]||"EN",scriptTitle=((_f2=$("reh-script-title"))==null?void 0:_f2.value.trim())||"Script",tag=(typeof _umlautSafe=="function"?_umlautSafe(scriptTitle):scriptTitle).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,24)||"Script",btn=$("reh-autodesign-btn"),prog=$("reh-autodesign-progress"),fill=$("reh-autodesign-fill"),label=$("reh-autodesign-label");btn.disabled=!0,rehDesignCancelled=!1,prog&&(prog.hidden=!1);const setProg=(d,t,msg)=>{fill&&(fill.style.width=(t?d/t*100:0)+"%"),label&&(label.textContent=msg||`${d} / ${t}`)};setProg(0,speakers.length,"Analyzing script with LLM\u2026");let characters;try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script,names:speakers,llm_url:llmUrl,model:llmModel,language})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}characters=(await r.json()).characters||[]}catch(e){toast("Character analysis failed: "+e.message,"error"),btn.disabled=!1,prog&&(prog.hidden=!0);return}const byName={};characters.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)});let done=0;try{const designOnly=speakers.filter(sp=>{var _a3;return((_a3=rehState.cast[sp])==null?void 0:_a3.voice)!=="me"});for(const sp of designOnly){if(rehDesignCancelled){toast("Cancelled","error");break}if(!rehState.cast[sp]){done++;continue}const info=byName[sp.toUpperCase().trim()]||{},gender=(info.gender||"N").toUpperCase().charAt(0).replace(/[^MFN]/,"N")||"N",desc=info.description||`A ${info.age||"adult"} ${gender==="M"?"male":gender==="F"?"female":""} character named ${sp}, natural expressive voice.`,sampleLine=((_g2=rehState.lines.find(l=>l.type==="dialog"&&l.speaker===sp))==null?void 0:_g2.text)||`Hello, I am ${sp}.`,safeName=(typeof _umlautSafe=="function"?_umlautSafe(sp):sp).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,24)||"Char",voiceId=`${langCode}_${gender}_${safeName}_${tag}`.slice(0,90);rehMarkCastDesigning(sp,"designing",null,{gender,language,voiceId,desc,age:info.age||"",step:"Generating voice audio\u2026"}),setProg(done,designOnly.length,`Designing ${sp}\u2026 (${done+1}/${designOnly.length})`);try{const dr=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({instruct:desc,sample_text:stripMarkdown(sampleLine).slice(0,300),language,gender,dialogue:!1})});if(!dr.ok){const e=await dr.json().catch(()=>({}));throw new Error(e.detail||dr.statusText)}const dd=await dr.json(),sr=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:dd.id,voice_id:voiceId,transcript:stripMarkdown(sampleLine).slice(0,300)})});if(!sr.ok){const e=await sr.json().catch(()=>({}));throw new Error(e.detail||sr.statusText)}const saved=await sr.json(),charName=sp===REH_NARRATOR_KEY?"Narrator":sp;typeof saveMeta=="function"&&await saveMeta(saved.voice_id,{gender,name:charName,avatar:_pickVoiceAvatar(gender,desc,sp),origin:"designed",group:`Rehearser: ${scriptTitle}`,note:`Rehearser \xB7 ${scriptTitle} \xB7 ${charName} \u2014 ${desc.slice(0,180)}`,transcript:stripMarkdown(sampleLine).slice(0,300),tag:scriptTitle}).catch(()=>{}),rehState.cast[sp].voice=saved.voice_id,rehState.cast[sp].instruct=rehState.cast[sp].instruct||desc,rehState.cast[sp].soul=rehState.cast[sp].soul||[info.age?`Age: ${info.age}`:"",desc].filter(Boolean).join(" \xB7 "),rehState.cast[sp].voiceData=null,rehState.voices.includes(saved.voice_id)||rehState.voices.push(saved.voice_id),rehMarkCastDesigning(sp,"done")}catch(e){rehMarkCastDesigning(sp,"err",e.message)}done++,setProg(done,designOnly.length)}typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),rehDesignCancelled||toast(`Designed ${done} voice${done!==1?"s":""} \u2014 tagged "${tag}" + Rehearser`,"success")}catch(e){toast("Design all failed: "+((e==null?void 0:e.message)||e),"error")}finally{renderCastList(),populateNarratorSelect(),prog&&(prog.hidden=!0),btn.disabled=!1}});function rehWriteCharacterNote(sp,info){const c=rehState.cast[sp];if(!c||!info)return;info.gender&&!c.gender&&(c.gender=String(info.gender).toUpperCase().charAt(0).replace(/[^MFN]/,"N"));const bits=[];info.age&&bits.push(`Age: ${info.age}`),info.description&&bits.push(info.description);const note=bits.join(" \xB7 ");note&&!c.soul&&(c.soul=note),info.description&&!c.instruct&&(c.instruct=info.description)}async function rehResearchCast(speakers){var _a2,_b2,_c2,_d2;const names=speakers.filter(sp=>sp!==REH_NARRATOR_KEY);if(!names.length)return{};let chars=[];try{const r=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText(),names,llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});r.ok&&(chars=(await r.json()).characters||[])}catch{}const byName={};return chars.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)}),speakers.forEach(sp=>rehWriteCharacterNote(sp,byName[sp.toUpperCase().trim()])),byName}(_ya=$("reh-matchlib-btn"))==null||_ya.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2;const btn=$("reh-matchlib-btn"),speakers=Object.keys(rehState.cast).filter(sp=>rehState.cast[sp].voice!=="me");if(!speakers.length){toast("No characters to match","error");return}let lib=(window._voices||[]).filter(v=>v.enabled!==!1);if(!lib.length)try{lib=(await fetch("/api/voices").then(r=>r.json())).filter(v=>v.enabled!==!1)}catch{}if(!lib.length){toast("Your voice library is empty \u2014 clone, design or import some voices first","error");return}const candidates=lib.map(v=>({id:v.id,gender:v.gender||"",language:v.lang||"",tags:v.tag||"",description:(v.note||v.name||"").slice(0,140)})),nameFor=sp=>sp===REH_NARRATOR_KEY?"Narrator":sp,byDisplay={};speakers.forEach(sp=>{byDisplay[nameFor(sp).toUpperCase().trim()]=sp});const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span> Matching\u2026';try{const r=await fetch("/api/match-characters-voices",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_a2=$("reh-script-text"))==null?void 0:_a2.value.trim())||linesToScriptText(),names:speakers.map(nameFor),voices:candidates,llm_url:((_b2=$("reh-llm-url"))==null?void 0:_b2.value.trim())||rehDefaultLlmUrl(),model:((_c2=$("reh-llm-model"))==null?void 0:_c2.value)||"",language:((_d2=$("reh-design-lang"))==null?void 0:_d2.value)||"English"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const assignments=(await r.json()).assignments||[],validIds=new Set(candidates.map(c=>c.id));let n=0;assignments.forEach(a=>{const sp=byDisplay[String(a.name||"").toUpperCase().trim()];sp&&a.voice_id&&validIds.has(a.voice_id)&&(rehState.cast[sp].voice=a.voice_id,rehState.cast[sp].voiceData=getVoiceData(a.voice_id),rehState.voices.includes(a.voice_id)||rehState.voices.push(a.voice_id),sp===REH_NARRATOR_KEY&&(rehState.narratorVoice=a.voice_id),n++)}),btn.innerHTML='<span class="reh-imsdb-spinner"></span> Researching characters\u2026',await rehResearchCast(speakers),renderCastList(),populateNarratorSelect(),toast(n?`Matched ${n} character${n!==1?"s":""} & added notes`:"No good matches \u2014 try \u201CDesign all voices\u201D instead",n?"success":"error")}catch(e){toast("Match failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}});const REH_FISH_LANG={English:"en",German:"de",French:"fr",Spanish:"es",Italian:"it",Portuguese:"pt",Dutch:"nl",Auto:""};(_za=$("reh-matchonline-btn"))==null||_za.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2;const btn=$("reh-matchonline-btn"),speakers=Object.keys(rehState.cast).filter(sp=>rehState.cast[sp].voice!=="me"&&sp!==REH_NARRATOR_KEY);if(!speakers.length){toast("No characters to match","error");return}const lang=(_b2=REH_FISH_LANG[((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||"English"])!=null?_b2:"en",prog=$("reh-autodesign-progress"),fill=$("reh-autodesign-fill"),label=$("reh-autodesign-label"),setProg=(d,t,msg)=>{fill&&(fill.style.width=(t?d/t*100:0)+"%"),label&&(label.textContent=msg||`${d} / ${t}`)},orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span> Analyzing\u2026',prog&&(prog.hidden=!1),rehDesignCancelled=!1;try{let chars=[];try{const ar=await fetch("/api/analyze-characters",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({script:((_c2=$("reh-script-text"))==null?void 0:_c2.value.trim())||linesToScriptText(),names:speakers,llm_url:((_d2=$("reh-llm-url"))==null?void 0:_d2.value.trim())||rehDefaultLlmUrl(),model:((_e2=$("reh-llm-model"))==null?void 0:_e2.value)||"",language:((_f2=$("reh-design-lang"))==null?void 0:_f2.value)||"English"})});ar.ok&&(chars=(await ar.json()).characters||[])}catch{}const byName={};chars.forEach(c=>{c.name&&(byName[String(c.name).toUpperCase().trim()]=c)});const G={M:"male",F:"female",N:"neutral"};let done=0,n=0;for(let i=0;i<speakers.length;i++){if(rehDesignCancelled){toast("Cancelled","error");break}const sp=speakers[i];setProg(done,speakers.length,`Matching ${sp}\u2026 (${done+1}/${speakers.length})`);const info=byName[sp.toUpperCase().trim()]||{};rehWriteCharacterNote(sp,info);const gender=G[(info.gender||"").toUpperCase().charAt(0)]||"",qs=[`search=${encodeURIComponent(sp)}`,`language=${lang}`,"page_size=8","sort_by=score"];let items=[];try{items=(await fetch("/api/fishaudio/voices?"+qs.join("&")).then(r=>r.json())).items||[]}catch{}let pick=items.find(v=>v.sample_audio),nameHit=!!pick;if(!pick){const fb=[`language=${lang}`,gender?`gender=${gender}`:"","page_size=12","sort_by=score",`page=${i%4+1}`].filter(Boolean);try{items=(await fetch("/api/fishaudio/voices?"+fb.join("&")).then(r=>r.json())).items||[]}catch{}pick=items.find(v=>v.sample_audio)}let cands=items.filter(v=>v.sample_audio).slice(0,6).map(v=>({title:v.title,sample_audio:v.sample_audio,image:v.image||"",gender:v.gender||"",language:v.language||lang||"",description:v.description||"",sample_text:v.sample_text||v.default_text||""}));if(!nameHit&&cands.length>1){const taken=new Set(Object.values(rehState.cast).map(c=>{var _a3,_b3,_c3;return(_c3=(_b3=(_a3=c.online)==null?void 0:_a3.candidates)==null?void 0:_b3[c.online.picked])==null?void 0:_c3.sample_audio}).filter(Boolean));cands=[...cands.slice(i%cands.length),...cands.slice(0,i%cands.length)].sort((a,b)=>(taken.has(a.sample_audio)?1:0)-(taken.has(b.sample_audio)?1:0))}if(cands.length)try{const vid=await _rehImportFishCandidate(sp,cands[0]);cands[0].voice_id=vid,rehState.cast[sp].voice=vid,rehState.cast[sp].voiceData=getVoiceData(vid),rehState.cast[sp].online={candidates:cands,picked:0},n++}catch(e){logErr("match-online import "+sp,e)}done++,setProg(done,speakers.length)}typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary().catch(()=>{}),renderCastList(),populateNarratorSelect(),toast(n?`Imported & matched ${n} online voice${n!==1?"s":""}`:"No online matches found",n?"success":"error")}catch(e){toast("Online match failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,prog&&(prog.hidden=!0)}});function rehMarkCastDesigning(sp,state,msg,info){var _a2;const row=document.querySelector(`.reh-cast-row[data-speaker="${CSS.escape(sp)}"]`);if(!row)return;let badge=row.querySelector(".reh-cast-design-badge");badge||(badge=document.createElement("span"),badge.className="reh-cast-design-badge",(_a2=row.querySelector("strong"))==null||_a2.after(badge)),badge.className="reh-cast-design-badge"+(state==="done"?" done":state==="err"?" err":""),badge.textContent=state==="designing"?"\u2728 designing\u2026":state==="done"?"\u2713 designed":"\u2717 failed",msg&&(badge.title=msg),row.classList.toggle("reh-cast-designing",state==="designing");const wrap=row.closest("div");let panel=wrap==null?void 0:wrap.querySelector(".reh-cast-design-panel");if(state==="designing"&&info){panel||(panel=document.createElement("div"),panel.className="reh-cast-design-panel",wrap.appendChild(panel));const genderIcon=info.gender==="M"?"\u2642":info.gender==="F"?"\u2640":"\u26A7",genderLabel=info.gender==="M"?"Male":info.gender==="F"?"Female":"Neutral";panel.innerHTML=`
<div class="reh-cdp-meta">
<span class="reh-cdp-chip reh-cdp-gender">${genderIcon} ${escHtml(genderLabel)}</span>
<span class="reh-cdp-chip reh-cdp-lang"><span class="mdi mdi-translate"></span> ${escHtml(info.language||"EN")}</span>
<span class="reh-cdp-chip reh-cdp-name"><span class="mdi mdi-account-outline"></span> ${escHtml(info.voiceId||sp)}</span>
${info.age?`<span class="reh-cdp-chip">${escHtml(info.age)}</span>`:""}
</div>
<div class="reh-cdp-desc">${escHtml(info.desc||"")}</div>
<div class="reh-cdp-step"><span class="reh-imsdb-spinner"></span> <span class="reh-cdp-step-txt">${escHtml(info.step||"Designing voice\u2026")}</span></div>`}else if(state==="done"&&panel){const descEl=panel.querySelector(".reh-cdp-desc"),metaEl=panel.querySelector(".reh-cdp-meta"),stepEl=panel.querySelector(".reh-cdp-step");if(stepEl&&stepEl.remove(),descEl){const t=descEl.textContent;t.length>160&&(descEl.textContent=t.slice(0,157)+"\u2026")}panel.classList.add("done")}else state==="err"&&panel&&panel.remove()}(_Aa=$("reh-start-btn"))==null||_Aa.addEventListener("click",()=>{var _a2,_b2;const backend=(_a2=$("reh-backend-select"))==null?void 0:_a2.value;if(!backend){toast("Select a backend first","error");return}rehState.backend=backend,rehState.narratorVoice=((_b2=$("reh-narrator-voice"))==null?void 0:_b2.value)||"",rehState.lineIndex=0,rehState.clips=[],rehState.playing=!1,rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.practiceStart=null,rehState.practiceEnd=null,buildScriptPage(),showPhase(3),highlightCurrentLine()});const REH_STAGE_FONT_KEY="ttsvc_reh_stage_scale",REH_CAST_COLLAPSED_KEY="ttsvc_reh_cast_collapsed";function rehStageScale(){const v=parseFloat(localStorage.getItem(REH_STAGE_FONT_KEY)||"1");return isNaN(v)?1:Math.max(.7,Math.min(2,v))}function rehApplyStageFont(){const scale=String(rehStageScale()),wrap=document.querySelector(".reh-page-wrap");wrap&&wrap.style.setProperty("--reh-stage-scale",scale);const page=$("reh-a4-page");page&&page.style.setProperty("--reh-stage-scale",scale)}function rehStageFontStep(delta){const next=Math.max(.7,Math.min(2,Math.round((rehStageScale()+delta)*100)/100));localStorage.setItem(REH_STAGE_FONT_KEY,String(next)),rehApplyStageFont()}function rehApplyCastCollapsed(){const row=$("reh-cast-row"),stage=$("reh-stage-area");if(!row)return;const collapsed=localStorage.getItem(REH_CAST_COLLAPSED_KEY)==="1";row.classList.toggle("is-collapsed",collapsed),stage&&stage.classList.toggle("side-collapsed",collapsed);const btn=$("reh-cast-toggle");if(btn){btn.setAttribute("aria-expanded",String(!collapsed)),btn.title=collapsed?"Expand character list":"Collapse to avatars";const icon=btn.querySelector(".mdi");icon&&(icon.className="mdi "+(collapsed?"mdi-chevron-right":"mdi-chevron-left"))}}function rehToggleCast(){const collapsed=localStorage.getItem(REH_CAST_COLLAPSED_KEY)==="1";localStorage.setItem(REH_CAST_COLLAPSED_KEY,collapsed?"0":"1"),rehApplyCastCollapsed()}(_Ba=$("reh-font-inc"))==null||_Ba.addEventListener("click",()=>rehStageFontStep(.1)),(_Ca=$("reh-font-dec"))==null||_Ca.addEventListener("click",()=>rehStageFontStep(-.1)),(_Da=$("reh-cast-toggle"))==null||_Da.addEventListener("click",rehToggleCast);function renderCastStrip(){var _a2,_b2,_c2;const castStrip=$("reh-cast-strip");if(!castStrip)return;const scriptTitle=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"";_rehEnsureLibCharsCache(scriptTitle);const names=Object.keys(rehState.cast),lineCountFor=sp=>rehState.lines.filter(l=>l.speaker===sp&&l.type==="dialog").length,libRecByNameStage=new Map((_rehLibCharsCache||[]).map(r=>[String(r.name||"").trim().toLowerCase(),r])),sortMode=((_b2=$("reh-cast-side-sort"))==null?void 0:_b2.value)||localStorage.getItem("reh_cast_side_sort")||"lines",query=(((_c2=$("reh-cast-side-search"))==null?void 0:_c2.value)||"").trim().toLowerCase();let rows=names.map(sp=>{const displayName=sp===REH_NARRATOR_KEY?"Narrator":sp,libRec=sp===REH_NARRATOR_KEY?null:libRecByNameStage.get(String(sp).trim().toLowerCase()),fallbackName=displayName==="Narrator"?displayName:displayName.replace(/\w\S*/g,w=>w[0].toUpperCase()+w.slice(1).toLowerCase());return{sp,displayName:(libRec==null?void 0:libRec.name)||fallbackName,n:lineCountFor(sp),libRec}});query&&(rows=rows.filter(r=>r.displayName.toLowerCase().includes(query))),rows.sort((a,b)=>sortMode==="name"?a.displayName.localeCompare(b.displayName):b.n-a.n||a.displayName.localeCompare(b.displayName)),castStrip.innerHTML=rows.map(({sp,displayName,n,libRec})=>{const c=rehState.cast[sp],isMe=c.voice==="me",avatarHtml=libRec!=null&&libRec.image?`<span class="ab-char-dot ab-char-dot-img" style="border-color:${c.color}"><img src="${libRec.image}" alt=""></span>`:`<span class="ab-char-dot" style="background:${c.color}">${escHtml((displayName||"?")[0].toUpperCase())}</span>`;return`<div class="ab-char-item" data-speaker="${escHtml(sp)}" title="${escHtml(displayName)} \u2014 ${isMe?"me":c.voice||"no voice"}">
${avatarHtml}
<span class="ab-char-name">${escHtml(displayName)}</span>
${isMe?'<span class="mdi mdi-microphone" style="font-size:11px;color:var(--subtext)"></span>':""}
<b class="ab-char-count">${n}</b>
</div>`}).join(""),castStrip.querySelectorAll(".ab-char-item").forEach(item=>{item.addEventListener("click",()=>{const line=document.querySelector(`.reh-block[data-speaker="${CSS.escape(item.dataset.speaker)}"]`);line&&line.scrollIntoView({behavior:"smooth",block:"center"})})});const lbl=$("reh-cast-toggle-label");lbl&&(lbl.textContent=`Characters (${names.length})`);const searchInp=$("reh-cast-side-search"),sortSel=$("reh-cast-side-sort");searchInp&&!searchInp.dataset.wired&&(searchInp.dataset.wired="1",searchInp.addEventListener("input",()=>renderCastStrip())),sortSel&&!sortSel.dataset.wired&&(sortSel.dataset.wired="1",sortSel.value=sortMode,sortSel.addEventListener("change",()=>{localStorage.setItem("reh_cast_side_sort",sortSel.value),renderCastStrip()}))}function buildScriptPage(){var _a2,_b2;const titleEl=$("reh-page-title");titleEl&&(titleEl.textContent=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Script"),renderCastStrip(),rehApplyStageFont(),rehApplyCastCollapsed(),typeof _lineAudioSyncDots=="function"&&_lineAudioSyncDots().catch(()=>{});const linesEl=$("reh-script-lines");linesEl&&(linesEl.innerHTML=rehState.lines.map((line,i)=>{const isCached=rehState.synthCache.has(i),isStale=rehState.staleLines.has(i),synthDot=`<span class="${isStale?"reh-synth-dot stale":"reh-synth-dot"}" id="reh-syd-${i}" style="${isCached||isStale?"":"display:none"}" title="${isStale?"Tone changed \u2014 needs re-synthesis":"Pre-synthesized"}"></span>`,reSynthBtn=`<button class="reh-resynth-btn" id="reh-rsb-${i}" title="Re-synthesize this line" ${isStale?"":"hidden"}><span class="mdi mdi-refresh"></span></button>`,editBtn=`<button class="reh-edit-btn" data-index="${i}" title="Edit text (or double-click)"><span class="mdi mdi-pencil-outline"></span></button>`,note=rehState.lines[i].note||"",noteArea=`<div class="reh-note-area" data-index="${i}">
${editBtn}
<button class="reh-note-btn${note?" has-note":""}" title="${note?"Edit note":"Add note"}"><span class="mdi mdi-note-edit-outline"></span></button>
<textarea class="reh-note-ta" placeholder="Personal note (not spoken)\u2026" spellcheck="false">${escHtml(note)}</textarea>
</div>`;if(line.hidden&&!(rehState.bulkMode&&rehState.showHidden))return"";const _bSel=rehState.bulkSel.has(i),bulkCheck=rehState.bulkMode?`<span class="reh-bulk-check${_bSel?" checked":""}" data-bulk="${i}" title="Select line"><span class="mdi ${_bSel?"mdi-checkbox-marked":"mdi-checkbox-blank-outline"}"></span></span>`:"",lineFlags=(line.ignored?" reh-line-ignored":"")+(line.hidden?" reh-line-hidden":"")+(_bSel?" reh-selected":"");if(typeof audiobookIsChapter=="function"&&audiobookIsChapter(line)){const label=(typeof stripMarkdown=="function"?stripMarkdown(line.text||""):line.text||"").trim();return`<div class="reh-chapter-marker${lineFlags}" data-index="${i}">
${bulkCheck}
<span class="reh-chapter-marker-line"></span>
<span class="reh-chapter-marker-label">${label?escHtml(label):"Chapter"}</span>
<span class="reh-chapter-marker-line"></span>
${editBtn} ${synthDot}
</div>`}switch(line.type){case"act":return`<div class="reh-act${lineFlags}" data-index="${i}" style="position:relative">${bulkCheck}${escHtml(line.text)} ${editBtn} ${synthDot}</div>`;case"scene":return`<div class="reh-scene${lineFlags}" data-index="${i}">
${bulkCheck}<span class="mdi mdi-film" style="flex-shrink:0"></span>
<span style="flex:1">${escHtml(line.text)}</span>
${editBtn} ${synthDot}
</div>`;case"action":{const narrPlayBtn=`<button type="button" class="reh-line-play-avatar reh-action-play-btn" data-index="${i}" title="Play or pause this line" aria-label="Play or pause this line"><span class="mdi mdi-play"></span></button>`;return`<div class="reh-action-block${lineFlags}" data-index="${i}">
${bulkCheck}${narrPlayBtn}<span class="reh-action-text">${renderMarkdownInline(line.text)}</span>
${editBtn} ${synthDot}
</div>`}case"pagebreak":{const pbLabel=line.page?`\u2014 Page ${line.page} \u2014`:"\u2014 Page break \u2014";return`<div class="reh-block-pagebreak${lineFlags}" data-index="${i}">${bulkCheck}<span class="reh-pagebreak-label">${pbLabel}</span></div>`}case"transition":return`<div class="reh-transition${lineFlags}" data-index="${i}">${bulkCheck}${escHtml(line.text)}</div>`;case"direction":{const indent=line.speaker?"text-align:center;":"";return`<div class="reh-direction${lineFlags}" data-index="${i}" style="${indent}">${bulkCheck}<em>${escHtml(line.text)}</em></div>`}case"dialog":{const c=rehState.cast[line.speaker]||{voice:"",color:"#89b4fa"},isMe=c.voice==="me",emoInfo=getEmotionInfo(line.emotion||"");return`<div class="reh-block${lineFlags}${rehState.practiceEnd===i?" reh-range-end":""}${rehState.practiceStart===i?" reh-range-start":""}${isPracticeRange(i)?" reh-in-range":""}" data-index="${i}" data-speaker="${escHtml(line.speaker)}" style="cursor:pointer">
${bulkCheck}<button class="reh-gutter-btn" data-index="${i}" title="Click: play from here \xB7 Shift+click: set end marker">${rehState.practiceStart===i?"\u25B6":rehState.practiceEnd===i?"\u25A0":""}</button>
<div class="reh-block-inner">
<div class="reh-block-head">
<button type="button" class="reh-line-play-avatar" data-index="${i}" title="Play or pause this line" aria-label="Play or pause this line">${_rehCharAvatarHtml(line.speaker,isMe?"":c.voice,c.color,32)}</button>
<span class="reh-block-name" style="color:${c.color}">${escHtml(line.speaker)}</span>
<span class="reh-block-head-spacer"></span>
${isMe?'<span class="reh-block-badge reh-badge-me"><span class="mdi mdi-microphone"></span> Me</span>':'<span class="reh-block-badge reh-badge-tts"><span class="mdi mdi-speaker-outline"></span> TTS</span>'}
${isMe?"":`<button class="reh-emo-btn${line.emotion?" has-emotion":""}" data-index="${i}" title="Set speaking tone">
${emoInfo.emoji?emoInfo.emoji+" ":""}<span class="reh-emo-label">${escHtml(emoInfo.label)}</span>
<span class="mdi mdi-chevron-down" style="font-size:9px;opacity:.6"></span>
</button>`}
${synthDot}
${reSynthBtn}
</div>
<div class="reh-block-dialog" id="reh-diag-${i}">${renderMarkdownInline(line.text)}</div>
</div>
${noteArea}
</div>`}default:return""}}).join(""),linesEl.querySelectorAll(".reh-emo-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),openEmoPicker(parseInt(btn.dataset.index),btn)})}),linesEl.querySelectorAll(".reh-edit-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),startInlineEdit(parseInt(btn.dataset.index))})}),linesEl.querySelectorAll(".reh-block-dialog").forEach(el=>{el.addEventListener("dblclick",e=>{e.stopPropagation(),startInlineEdit(parseInt(el.closest("[data-index]").dataset.index))})}),linesEl.querySelectorAll(".reh-action-text").forEach(el=>{el.addEventListener("dblclick",e=>{e.stopPropagation(),startInlineEdit(parseInt(el.closest("[data-index]").dataset.index))})}),linesEl.querySelectorAll(".reh-line-play-avatar").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation();const idx=parseInt(btn.dataset.index);if(rehState.playing&&rehState.lineIndex===idx){pausePlay();return}stopPlay(),hideRecOverlay(),rehState.lineIndex=idx,highlightCurrentLine(),startPlay()})}),linesEl.querySelectorAll(".reh-gutter-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation();const idx=parseInt(btn.dataset.index);e.shiftKey?rehState.practiceEnd=rehState.practiceEnd===idx?null:idx:(stopPlay(),hideRecOverlay(),rehState.lineIndex=idx,highlightCurrentLine(),startPlay()),updatePracticeRange()})}),linesEl.querySelectorAll("[data-index]").forEach(el=>{el.addEventListener("click",e=>{if(e.target.closest(".reh-emo-btn, .reh-edit-btn, .reh-gutter-btn, .reh-note-btn, .reh-resynth-btn, .reh-line-play-avatar, textarea, select, .vp-root"))return;const idx=parseInt(el.dataset.index);if(rehState.bulkMode){_toggleBulkSel(idx,e.shiftKey);return}stopPlay(),hideRecOverlay(),rehState.lineIndex=idx,highlightCurrentLine()})}),linesEl.querySelectorAll(".reh-resynth-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),synthOneLine(parseInt(btn.id.replace("reh-rsb-","")))})}),linesEl.querySelectorAll(".reh-note-area").forEach(area=>{const idx=parseInt(area.dataset.index),btn=area.querySelector(".reh-note-btn"),ta=area.querySelector(".reh-note-ta");!btn||!ta||(ta.value.trim()&&ta.classList.add("open"),btn.addEventListener("click",e=>{e.stopPropagation(),ta.classList.toggle("open"),ta.classList.contains("open")&&ta.focus()}),ta.addEventListener("input",()=>{rehState.lines[idx].note=ta.value,btn.classList.toggle("has-note",!!ta.value.trim())}),ta.addEventListener("click",e=>e.stopPropagation()))}),linesEl.classList.toggle("reh-bulk-mode",rehState.bulkMode),(_b2=document.querySelector(".reh-page-wrap"))==null||_b2.classList.toggle("reh-bulk-mode",rehState.bulkMode),rehState.bulkMode&&linesEl.querySelectorAll(".reh-bulk-check").forEach(cb=>{cb.addEventListener("click",e=>{e.stopPropagation(),_toggleBulkSel(parseInt(cb.dataset.bulk),e.shiftKey)})}),applyPageMode(),_checkToneStyleSupport())}function _toggleBulkSel(i,range=!1){if(Number.isFinite(i)){if(range&&rehState.bulkAnchor!==null&&Number.isFinite(rehState.bulkAnchor)){const a=Math.min(rehState.bulkAnchor,i),b=Math.max(rehState.bulkAnchor,i),indices=[];for(let n=a;n<=b;n++)rehState.lines[n]&&!rehState.lines[n].hidden&&indices.push(n);const deselect=indices.length&&indices.every(n=>rehState.bulkSel.has(n));indices.forEach(n=>{deselect?rehState.bulkSel.delete(n):rehState.bulkSel.add(n),_refreshBulkLine(n)}),rehState.bulkAnchor=i}else rehState.bulkSel.has(i)?rehState.bulkSel.delete(i):rehState.bulkSel.add(i),rehState.bulkAnchor=i,_refreshBulkLine(i);_updateBulkCount()}}function _refreshBulkLine(i){const sel=rehState.bulkSel.has(i);document.querySelectorAll(`.reh-bulk-check[data-bulk="${i}"]`).forEach(cb=>{cb.classList.toggle("checked",sel);const icon=cb.querySelector(".mdi");icon&&(icon.className="mdi "+(sel?"mdi-checkbox-marked":"mdi-checkbox-blank-outline"))}),document.querySelectorAll(`[data-index="${i}"]`).forEach(el=>el.classList.toggle("reh-selected",sel))}function _updateBulkCount(){const el=$("reh-bulk-count");el&&(el.textContent=`${rehState.bulkSel.size} selected`)}function setBulkMode(on){rehState.bulkMode=on,on||(rehState.bulkSel.clear(),rehState.bulkAnchor=null);const bar=$("reh-bulk-bar");bar&&(bar.hidden=!on);const btn=$("reh-bulk-toggle");btn&&btn.classList.toggle("active",on),_updateBulkCount(),rehState.lines.length&&buildScriptPage()}function _bulkApply(fn,{keepSelection=!1}={}){if(!rehState.bulkSel.size){toast("No lines selected","error");return}[...rehState.bulkSel].forEach(i=>{const l=rehState.lines[i];l&&fn(l,i)}),keepSelection||rehState.bulkSel.clear(),_updateBulkCount(),buildScriptPage()}function _bulkDelete(){if(!rehState.bulkSel.size){toast("No lines selected","error");return}const victims=[...rehState.bulkSel].sort((a,b)=>b-a);victims.forEach(i=>{rehState.lines.splice(i,1),_reindexLineState(i)}),rehState.bulkSel.clear(),rehState.lineIndex>=rehState.lines.length&&(rehState.lineIndex=Math.max(0,rehState.lines.length-1)),_updateBulkCount(),buildScriptPage(),highlightCurrentLine(),toast(`Deleted ${victims.length} line${victims.length!==1?"s":""}`,"success")}function _reindexLineState(d){const shift=(collection,isMap)=>{const out=isMap?new Map:new Set;for(const entry of collection){const k=isMap?entry[0]:entry;if(k===d)continue;const nk=k>d?k-1:k;isMap?out.set(nk,entry[1]):out.add(nk)}return out};rehState.synthCache=shift(rehState.synthCache,!0),rehState.staleLines=shift(rehState.staleLines,!1);const sel=shift(rehState.bulkSel,!1);rehState.bulkSel.clear(),sel.forEach(v=>rehState.bulkSel.add(v)),rehState.practiceStart!=null&&rehState.practiceStart>d&&rehState.practiceStart--,rehState.practiceEnd!=null&&rehState.practiceEnd>d&&rehState.practiceEnd--,rehState.lineIndex>d&&rehState.lineIndex--}const PAGE_MODES=["auto","scroll","pdf"];let _pageMode=localStorage.getItem("reh-page-mode")||"pdf";function applyPageMode(){const wrap=document.querySelector(".reh-page-wrap"),a4=$("reh-a4-page");wrap&&(_pageMode==="scroll"?(wrap.querySelectorAll(".reh-paper").forEach(p=>{[...p.children].forEach(c=>{c.classList.contains("reh-paper-num")||a4==null||a4.appendChild(c)}),p.remove()}),wrap.classList.remove("paginated"),a4&&(a4.style.display=""),document.querySelectorAll(".reh-block-pagebreak").forEach(el=>el.style.display="none")):paginateScript(_pageMode==="pdf"?{respectBreaks:!0}:{respectBreaks:!1}),_syncPageModeBtn())}function _syncPageModeBtn(){const btn=$("reh-page-mode-btn");if(!btn)return;const icons={auto:"mdi-file-document-outline",scroll:"mdi-format-align-justify",pdf:"mdi-book-open-page-variant"},labels={auto:"A4 pages",scroll:"Scroll",pdf:"PDF pages"},nextMode=PAGE_MODES[(PAGE_MODES.indexOf(_pageMode)+1)%PAGE_MODES.length];btn.innerHTML=`<span class="mdi ${icons[nextMode]||icons.auto}"></span> ${labels[nextMode]||labels.auto}`,btn.title=`Currently: ${labels[_pageMode]} \u2014 click to switch to ${labels[nextMode]}`}function cyclePageMode(){const idx=PAGE_MODES.indexOf(_pageMode);_pageMode=PAGE_MODES[(idx+1)%PAGE_MODES.length],localStorage.setItem("reh-page-mode",_pageMode),applyPageMode(),rehState.lines.length&&buildScriptPage()}function paginateScript({respectBreaks=!1}={}){const wrap=document.querySelector(".reh-page-wrap"),linesEl=$("reh-script-lines"),titleEl=$("reh-page-title");if(!wrap||!linesEl)return;const blocks=[...linesEl.children];if(!blocks.length)return;wrap.classList.add("paginated");const a4=$("reh-a4-page");a4&&(a4.style.display="none"),wrap.querySelectorAll(".reh-paper").forEach(p=>p.remove());const PAGE_CONTENT=1027,TITLE_SPACE=64;let pageNum=0,page=null,used=0;const newPage=()=>{pageNum++,page=document.createElement("div"),page.className="reh-paper";const num=document.createElement("div");if(num.className="reh-paper-num",num.textContent=pageNum+".",page.appendChild(num),used=0,pageNum===1&&titleEl){const t=titleEl.cloneNode(!0);t.style.display="",page.appendChild(t),used+=TITLE_SPACE}wrap.appendChild(page)};newPage();for(const block of blocks){if(respectBreaks&&block.classList.contains("reh-block-pagebreak")){newPage();continue}page.appendChild(block);const cs=getComputedStyle(block),h=block.offsetHeight+(parseFloat(cs.marginTop)||0)+(parseFloat(cs.marginBottom)||0);!respectBreaks&&used+h>PAGE_CONTENT&&used>(pageNum===1?TITLE_SPACE:0)&&(newPage(),page.appendChild(block)),used+=h}}function isPracticeRange(idx){const s=rehState.practiceStart,e=rehState.practiceEnd;return s===null?!1:idx>=s&&(e===null||idx<=e)}function startInlineEdit(idx){const line=rehState.lines[idx];if(!line)return;const dialogEl=document.getElementById("reh-diag-"+idx),actionEl=document.querySelector(`[data-index="${idx}"] .reh-action-text`),sceneEl=document.querySelector(`.reh-scene[data-index="${idx}"] span[style*="flex:1"]`),targetEl=dialogEl||actionEl||sceneEl;if(!targetEl||targetEl.tagName==="TEXTAREA")return;const orig=line.text,ta=document.createElement("textarea");ta.value=orig,ta.style.cssText="width:100%;min-height:54px;font-size:14px;line-height:1.6;padding:5px 8px;border:1px solid var(--accent);border-top:none;border-radius:0 0 3px 3px;resize:vertical;background:#fffff8;font-family:inherit;display:block;box-sizing:border-box;outline:none;";const toolbar=document.createElement("div");toolbar.className="reh-fmt-toolbar",toolbar.innerHTML=`
<button class="reh-fmt-btn" data-b="**" title="Bold"><b>B</b></button>
<button class="reh-fmt-btn" data-b="*" title="Italic"><i>I</i></button>
<button class="reh-fmt-btn" data-b="__" title="Underline"><u>U</u></button>
<button class="reh-fmt-btn" data-b="~~" title="Strikethrough"><del>S</del></button>
<button class="reh-fmt-btn" data-b="==" title="Highlight" style="background:#fef3c7">H</button>
<kbd class="reh-fmt-hint">Ctrl+Enter save \xB7 Esc cancel</kbd>
`,toolbar.querySelectorAll(".reh-fmt-btn").forEach(btn=>{btn.addEventListener("mousedown",e=>{e.preventDefault();const wrap=btn.dataset.b,s=ta.selectionStart,end=ta.selectionEnd,sel=ta.value.slice(s,end)||"text";ta.value=ta.value.slice(0,s)+wrap+sel+wrap+ta.value.slice(end),ta.setSelectionRange(s+wrap.length,s+wrap.length+sel.length),ta.focus()})}),targetEl.style.display="none",targetEl.before(toolbar),toolbar.after(ta),ta.focus(),ta.select();const save=()=>{const newText=ta.value.trim()||orig;line.text=newText,targetEl.innerHTML=renderMarkdownInline(newText),targetEl.style.display="",toolbar.remove(),ta.remove(),rehState.synthCache.delete(idx);const dot=document.getElementById("reh-syd-"+idx);dot&&(dot.style.display="none")};ta.addEventListener("keydown",e=>{e.key==="Escape"&&(ta.value=orig,save()),e.key==="Enter"&&(e.ctrlKey||e.metaKey)&&(e.preventDefault(),save())}),ta.addEventListener("blur",save)}function openScriptEditorModal(){var _a2,_b2,_c2;const currentTitle=((_a2=$("reh-page-title"))==null?void 0:_a2.textContent)||((_b2=$("reh-script-title"))==null?void 0:_b2.value)||"Script",currentScript=linesToScriptText()||((_c2=$("reh-script-text"))==null?void 0:_c2.value)||"",modal=document.createElement("div");modal.className="reh-modal-overlay",modal.innerHTML=`
<div class="reh-modal-box" style="max-width:720px;width:95vw;height:80vh">
<div class="reh-modal-head">
<h3 style="margin:0;font-size:15px"><span class="mdi mdi-script-text-outline"></span> Edit Script</h3>
<button class="btn-secondary btn-sm" id="_reh-mc">\u2715 Close</button>
</div>
<div style="padding:10px 14px;border-bottom:1px solid var(--border);display:flex;gap:10px;align-items:center">
<label style="font-size:12px;font-weight:600;flex-shrink:0">Title</label>
<input type="text" id="_reh-mt" value="${escHtml(currentTitle)}" style="flex:1;padding:5px 8px;border:1px solid var(--border);border-radius:4px;font-size:13px;background:var(--surface);color:var(--text)">
</div>
<textarea id="_reh-ms" style="flex:1;padding:14px;font-family:'Courier New',monospace;font-size:12.5px;border:none;outline:none;resize:none;background:var(--surface);color:var(--text);height:calc(80vh - 140px);width:100%;box-sizing:border-box;" spellcheck="false">${escHtml(currentScript)}</textarea>
<div style="padding:10px 14px;display:flex;gap:8px;justify-content:flex-end;border-top:1px solid var(--border)">
<button class="btn-secondary" id="_reh-mcan">Cancel</button>
<button class="btn-primary" id="_reh-mapp"><span class="mdi mdi-auto-fix"></span> Apply changes</button>
</div>
</div>
`,document.body.appendChild(modal);const close=()=>modal.remove();modal.querySelector("#_reh-mc").onclick=close,modal.querySelector("#_reh-mcan").onclick=close,modal.addEventListener("click",e=>{e.target===modal&&close()}),modal.querySelector("#_reh-mapp").onclick=()=>{const newTitle=modal.querySelector("#_reh-mt").value.trim()||"Script",newScript=modal.querySelector("#_reh-ms").value,newLines=parseScript(newScript);if(!newLines.filter(l=>l.type==="dialog").length){toast("No dialog found in the edited script","error");return}newLines.forEach(nl=>{if(nl.type!=="dialog")return;const old=rehState.lines.find(ol=>ol.type==="dialog"&&ol.speaker===nl.speaker&&ol.text===nl.text);old&&(nl.emotion=old.emotion)});const detected=detectCharacters(newLines);Object.entries(detected).forEach(([sp])=>{rehState.cast[sp]&&(detected[sp]={...detected[sp],...rehState.cast[sp]})}),rehState.lines=newLines,rehState.cast=detected,rehState.lineIndex=Math.min(rehState.lineIndex,newLines.length-1),rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.practiceStart=null,rehState.practiceEnd=null,$("reh-script-title")&&($("reh-script-title").value=newTitle),$("reh-page-title")&&($("reh-page-title").textContent=newTitle),buildScriptPage(),highlightCurrentLine(),close(),toast("Script updated","success")}}function updatePracticeRange(){const start=rehState.practiceStart,end=rehState.practiceEnd;document.querySelectorAll(".reh-block").forEach(el=>{const idx=parseInt(el.dataset.index);el.classList.toggle("reh-in-range",start!==null&&idx>=start&&(end===null||idx<=end)),el.classList.toggle("reh-range-start",start!==null&&idx===start),el.classList.toggle("reh-range-end",end!==null&&idx===end);const gb=el.querySelector(".reh-gutter-btn");gb&&(gb.textContent=idx===start?"\u25B6":idx===end?"\u25A0":"")});const info=$("reh-practice-info");if(info){const show=end!==null;if(info.classList.toggle("visible",show),info.hidden=!show,show){const f=$("reh-practice-from"),t=$("reh-practice-to");f&&(f.textContent=(start!==null?start:0)+1),t&&(t.textContent=end+1)}}}function clearPracticeRange(){rehState.practiceStart=null,rehState.practiceEnd=null,updatePracticeRange()}function setPracticeRangeFromSelection(){const picked=[...rehState.bulkSel].sort((a,b)=>a-b);if(!picked.length){toast("Select the lines you want to rehearse first","error");return}rehState.practiceStart=picked[0],rehState.practiceEnd=picked[picked.length-1],rehState.lineIndex=rehState.practiceStart,setBulkMode(!1),updatePracticeRange(),highlightCurrentLine(),toast(`Practice range set: lines ${rehState.practiceStart+1}\u2013${rehState.practiceEnd+1}`,"success")}(_Ea=$("reh-practice-clear"))==null||_Ea.addEventListener("click",clearPracticeRange),(_Fa=$("reh-bulk-range"))==null||_Fa.addEventListener("click",setPracticeRangeFromSelection);let rehEmoPicker=null;function openEmoPicker(idx,anchorBtn){closeEmoPicker();const line=rehState.lines[idx],allEmos=[...REH_EMOTIONS,...rehCustomEmotions],rect=anchorBtn.getBoundingClientRect(),left=Math.min(rect.left,window.innerWidth-265),topBelow=rect.bottom+4,topAbove=rect.top-4,top=window.innerHeight-topBelow>=240?topBelow:topAbove-260,pop=document.createElement("div");pop.className="reh-emo-popover",pop.style.cssText=`position:fixed;z-index:1000;top:${top}px;left:${left}px;width:258px;`,pop.innerHTML=`<div class="reh-emo-pop-inner">
<input class="reh-emo-search" placeholder="Search or type custom tone\u2026" autocomplete="off">
<div class="reh-emo-list"></div>
<div class="reh-emo-custom-row">
<button class="btn-secondary btn-sm" id="_emo-custom-apply" style="width:100%;font-size:11px">
<span class="mdi mdi-plus"></span> Use as custom tone
</button>
</div>
</div>`,document.body.appendChild(pop),rehEmoPicker=pop;const searchEl=pop.querySelector(".reh-emo-search"),listEl=pop.querySelector(".reh-emo-list"),applyBtn=pop.querySelector("#_emo-custom-apply");function renderList(q=""){const lq=q.toLowerCase(),filtered=allEmos.filter(e=>!q||e.label.toLowerCase().includes(lq)||e.value.toLowerCase().includes(lq));listEl.innerHTML=filtered.map(e=>`<div class="reh-emo-item${line.emotion===e.value?" selected":""}" data-value="${escHtml(e.value)}">
<span class="reh-emo-item-emoji">${e.emoji}</span>
<span class="reh-emo-item-label">${escHtml(e.label)}</span>
${line.emotion===e.value?'<span class="mdi mdi-check" style="margin-left:auto;font-size:13px;color:var(--accent)"></span>':""}
</div>`).join(""),listEl.querySelectorAll(".reh-emo-item").forEach(item=>{item.addEventListener("mousedown",e=>{e.preventDefault(),selectEmotion(idx,item.dataset.value,anchorBtn),closeEmoPicker()})})}searchEl.addEventListener("input",()=>renderList(searchEl.value)),searchEl.addEventListener("keydown",e=>{if(e.key==="Escape"&&closeEmoPicker(),e.key==="Enter"){const val=searchEl.value.trim();val&&(selectEmotion(idx,val,anchorBtn),closeEmoPicker())}}),applyBtn.addEventListener("mousedown",e=>{e.preventDefault();const val=searchEl.value.trim();if(!val)return;if(![...REH_EMOTIONS,...rehCustomEmotions].find(e2=>e2.value===val)){rehCustomEmotions.push({emoji:"\u2728",label:val,value:val,custom:!0});try{localStorage.setItem("reh-custom-emotions",JSON.stringify(rehCustomEmotions))}catch{}}selectEmotion(idx,val,anchorBtn),closeEmoPicker()}),renderList(),searchEl.focus(),setTimeout(()=>document.addEventListener("mousedown",_closePickerOnOutside),50)}function _closePickerOnOutside(e){rehEmoPicker&&!rehEmoPicker.contains(e.target)&&closeEmoPicker()}function closeEmoPicker(){rehEmoPicker&&(rehEmoPicker.remove(),rehEmoPicker=null),document.removeEventListener("mousedown",_closePickerOnOutside)}function _markSynthDot(idx,state){const dot=document.getElementById("reh-syd-"+idx);if(!dot)return;dot.className="reh-synth-dot"+(state==="stale"?" stale":"")+(state==="synthesizing"?" synthesizing":""),dot.style.display=state?"":"none",dot.title=state==="stale"?"Tone changed \u2014 needs re-synthesis":state==="synthesizing"?"Synthesizing\u2026":"Pre-synthesized";const block=dot.closest("[data-index]");block&&block.classList.toggle("reh-line-synthesizing",state==="synthesizing")}function _showReSynthBtn(idx,show){const btn=document.getElementById("reh-rsb-"+idx);btn&&(btn.hidden=!show)}function _rehSyncCastVoiceFromLibrary(rec){if(!window.rehState||!rehState.lines||!rehState.lines.length||!rec||!rec.name||!rec.voice)return;const openBook=typeof _lineAudioBookName=="function"?_lineAudioBookName():"";if(!openBook||String(rec.book||"").trim().toLowerCase()!==openBook.trim().toLowerCase())return;const target=String(rec.name).toUpperCase().trim(),sp=Object.keys(rehState.cast).find(k=>String(k).toUpperCase().trim()===target);if(!sp)return;const c=rehState.cast[sp];!c||c.voice===rec.voice||(c.voice=rec.voice,c.voiceData=getVoiceData(rec.voice),rehState.lines.forEach((line,idx)=>{line.type==="dialog"&&line.speaker===sp&&rehState.synthCache.has(idx)&&(rehState.synthCache.delete(idx),rehState.staleLines.add(idx),typeof _markSynthDot=="function"&&_markSynthDot(idx,"stale"))}),typeof _updateStaleBatchBtn=="function"&&_updateStaleBatchBtn(),typeof renderCastList=="function"&&renderCastList())}window._rehSyncCastVoiceFromLibrary=_rehSyncCastVoiceFromLibrary;async function synthOneLine(idx){const line=rehState.lines[idx];if(!line||line.type!=="dialog")return;const c=rehState.cast[line.speaker];if(!c||!c.voice||c.voice==="me")return;const instruct=_buildInstruct(c.instruct,line.emotion,c.voice);_showReSynthBtn(idx,!1),_markSynthDot(idx,"synthesizing");try{const blob=await fetchTtsPreviewBlob(c.voice,_rehInlineTone(stripMarkdown(line.text),line.emotion),"wav",instruct,_ttsBackendForVoice(c.voice,rehState.backend));rehState.synthCache.set(idx,blob),rehState.staleLines.delete(idx),preDecodeBlob(idx,blob),_markSynthDot(idx,"ok"),toast("Re-synthesized line "+(idx+1),"success")}catch(e){_markSynthDot(idx,null),_showReSynthBtn(idx,!0),toast("Synthesis failed: "+e.message,"error")}}function selectEmotion(idx,value,anchorBtn){rehState.lines[idx].emotion=value,rehState.synthCache.has(idx)&&(rehState.synthCache.delete(idx),rehState.staleLines.add(idx),_markSynthDot(idx,"stale"),_updateStaleBatchBtn()),_showReSynthBtn(idx,!0);const info=getEmotionInfo(value);anchorBtn.className="reh-emo-btn"+(value?" has-emotion":""),anchorBtn.innerHTML=`${info.emoji?info.emoji+" ":""}<span class="reh-emo-label">${escHtml(info.label)}</span> <span class="mdi mdi-chevron-down" style="font-size:9px;opacity:.6"></span>`,value&&_checkToneStyleSupport()}function _rehToneCmpRow(backend,isCurrent){const check=ok=>ok?'<span class="reh-tone-cmp-yes"><span class="mdi mdi-check"></span></span>':'<span class="reh-tone-cmp-no"><span class="mdi mdi-close"></span></span>',action=isCurrent?'<span class="reh-tone-cmp-current-tag">current</span>':`<button type="button" class="btn-secondary btn-sm reh-tone-switch-btn" data-backend-id="${escHtml(backend.id)}">Switch</button>`;return`<tr${isCurrent?' class="reh-tone-cmp-current"':""}>
<td>${escHtml(backend.label)}</td>
<td>${check(backend.style_aware)}</td>
<td>${check(backend.uses_wav)}</td>
<td>${action}</td>
</tr>`}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=`<strong>${escHtml(b.label)}</strong> keeps each character\u2019s voice consistent <em>and</em> applies tone. Per-line tones are sent as inline <code>[tags]</code> (e.g. <code>[whisper]</code>, <code>[excited]</code>, <code>[laughing]</code>). You can also type a custom tone like <code>[professional broadcast tone]</code> \u2014 S2 supports free-form descriptions. <a href="https://huggingface.co/fishaudio/s2-pro" target="_blank" rel="noopener">Fish-Speech S2 \u2197</a>`),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?`<table class="reh-tone-cmp"><thead><tr><th></th><th>Tone control</th><th>Voice stays identical</th><th></th></tr></thead><tbody>${_rehToneCmpRow(b,!0)}${_rehToneCmpRow(styleAware,!1)}</tbody></table>`:`<strong>${escHtml(b.label)}</strong> 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||""))?'<p class="reh-tone-cmp-note">Qwen3TTS tone is sent as the per-line style/instruct text, so this is the right path for directed delivery.</p>':"";txtEl&&(txtEl.innerHTML=wavBackend?`<table class="reh-tone-cmp"><thead><tr><th></th><th>Tone control</th><th>Voice stays identical</th><th></th></tr></thead><tbody>${_rehToneCmpRow(b,!0)}${_rehToneCmpRow(wavBackend,!1)}</tbody></table>${qwenHint}`:`<strong>${escHtml(b.label)}</strong> 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())(k<lo||k>hi)&&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)=>`<span class="reh-word" data-wi="${i}">${escHtml(t.word)}</span>`).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<rehState.lines.length;i++)if(rehState.lines[i].type==="dialog"&&rehState.synthCache.has(i))return i;return-1}async function playAudioBlobFallback(blob){const url=URL.createObjectURL(blob),audio=$("reh-tts-audio");audio&&(audio.src=url,audio.style.display="",await new Promise(r=>{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.lineIndex<rehState.lines.length&&rehState.lines[rehState.lineIndex].type!=="dialog"&&rehState.lines[rehState.lineIndex].type!=="pagebreak"&&!(rehState.practiceEnd!==null&&rehState.lineIndex>rehState.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?'<span class="mdi mdi-pause"></span>':'<span class="mdi mdi-play"></span>',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<rehState.lines.length;i++){const line=rehState.lines[i];if(line.ignored||line.hidden||rehState.synthCache.has(i))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)}idxToKey.set(i,await _lineAudioCacheKey(text,voice,instruct))}if(!idxToKey.size)return;const keyToIdx=new Map([...idxToKey].map(([i,k])=>[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='<span class="mdi mdi-loading mdi-spin"></span> 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='<span class="mdi mdi-broom"></span> 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=`<span class="reh-rec-cue-name" style="color:${c.color}">${escHtml(line.speaker)}</span> \u2014 your line:<div class="reh-rec-cue-text">${escHtml(stripMarkdown(line.text))}</div>`),$("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<active&&(bar.classList.add("on"),db>-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<ADD;i++)ring[RING-ADD+i]=data[Math.floor(i*data.length/ADD)];const ctx=canvas.getContext("2d"),w=canvas.width,h=canvas.height;ctx.clearRect(0,0,w,h),ctx.beginPath(),ctx.strokeStyle=peak>.98?"#f38ba8":db>-12?"#f9e2af":"#a6e3a1",ctx.lineWidth=1.5;const mid=h/2;for(let i=0;i<RING;i++){const x=i/RING*w,y=mid-ring[i]*mid*.85;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y)}ctx.stroke()}rehState.recMeterRaf=requestAnimationFrame(tick)};tick()}async function startRehMic(){if(rehState.recDestStream)return;const AudioCtx=window.AudioContext||window.webkitAudioContext;if(rehState.recStream=await requestMicrophoneStream({raw:!0}),AudioCtx){rehState.recAudioCtx=new AudioCtx,rehState.recSourceNode=rehState.recAudioCtx.createMediaStreamSource(rehState.recStream),rehState.recGainNode=rehState.recAudioCtx.createGain(),rehState.recAnalyser=rehState.recAudioCtx.createAnalyser(),rehState.recAnalyser.fftSize=1024;const dest=rehState.recAudioCtx.createMediaStreamDestination();rehState.recSourceNode.connect(rehState.recGainNode),rehState.recGainNode.connect(rehState.recAnalyser),rehState.recGainNode.connect(dest),rehState.recDestStream=dest.stream,rehStartMeter()}else rehState.recDestStream=rehState.recStream}function stopRehMic(){rehState.recMeterRaf&&cancelAnimationFrame(rehState.recMeterRaf),rehState.recMeterRaf=null,[rehState.recSourceNode,rehState.recGainNode,rehState.recAnalyser].forEach(n=>{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='<p class="note">No clips in this session.</p>';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?`<audio controls src="${URL.createObjectURL(clip.blob)}" style="width:100%;max-width:320px;margin-top:4px"></audio>`:"",dlHtml=clip.blob?`<a class="btn-secondary" style="text-decoration:none;flex-shrink:0" download="line_${idx+1}_${clip.speaker}.webm" href="${URL.createObjectURL(clip.blob)}"><span class="mdi mdi-download"></span></a>`:"";return`<div class="reh-summary-row">
<span style="width:10px;height:10px;border-radius:50%;background:${c.color};flex-shrink:0;margin-top:4px"></span>
<div style="flex:1;min-width:0">
<div style="font-weight:600;font-size:13px;color:${c.color}">${escHtml(clip.speaker||"\u2014")} <span style="font-weight:400;color:var(--subtext)">${typeLabel}</span></div>
<div style="font-size:13px;line-height:1.4;margin-top:2px">${escHtml(stripMarkdown(line.text))}</div>
${audioHtml}
</div>
${dlHtml}
</div>`}).join("")}}(_eb=$("reh-new-session-btn"))==null||_eb.addEventListener("click",()=>{stopPlay(),stopRehMic(),rehState.lines=[],rehState.cast={},rehState.clips=[],rehState.lineIndex=0,rehState.savedId=null,rehState.synthCache.clear(),rehDecodedBuffers.clear(),rehState.narratorVoice="",rehState.practiceStart=null,rehState.practiceEnd=null,$("reh-script-text")&&($("reh-script-text").value=""),$("reh-script-title")&&($("reh-script-title").value=""),renderLibraryList(),showPhase(1)}),(_fb=$("reh-resume-btn"))==null||_fb.addEventListener("click",()=>{showPhase(3),highlightCurrentLine()}),(_gb=$("reh-save-session-btn"))==null||_gb.addEventListener("click",saveToLibrary),(_hb=$("reh-cast-save-btn"))==null||_hb.addEventListener("click",saveToLibrary),(_ib=$("reh-export-session-btn"))==null||_ib.addEventListener("click",exportToFile),(_jb=$("reh-tb-save"))==null||_jb.addEventListener("click",saveToLibrary),(_kb=$("reh-tb-export"))==null||_kb.addEventListener("click",exportToFile),(_lb=$("reh-lib-import-file"))==null||_lb.addEventListener("change",async function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];f&&(await importFromFile(f),this.value="")}),document.querySelectorAll(".reh-impex-file").forEach(inp=>{inp.addEventListener("change",async function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];f&&(await importFromFile(f),this.value="",navRehearserPhase(1))})}),(_mb=$("reh-impex-reh-btn"))==null||_mb.addEventListener("click",exportToFile),(_nb=$("reh-impex-fountain-btn"))==null||_nb.addEventListener("click",()=>{var _a2;(_a2=$("reh-fountain-btn"))==null||_a2.click()}),(_ob=$("reh-impex-fdx-btn"))==null||_ob.addEventListener("click",()=>{var _a2;(_a2=$("reh-fdx-export-btn"))==null||_a2.click()}),(_pb=$("reh-impex-osf-btn"))==null||_pb.addEventListener("click",()=>{var _a2;(_a2=$("reh-osf-export-btn"))==null||_a2.click()});function _downloadText(content,filename){const a=document.createElement("a");a.href=URL.createObjectURL(new Blob([content],{type:"text/plain;charset=utf-8"})),a.download=filename,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),3e3)}function exportAsTxt(){var _a2,_b2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||((_b2=$("reh-page-title"))==null?void 0:_b2.textContent)||"script";_downloadText(linesToScriptText(),title.replace(/[^a-z0-9]/gi,"_")+".txt")}function exportAsMd(){var _a2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Script",lines=rehState.lines.map(l=>{switch(l.type){case"act":return`
# ${l.text}
`;case"scene":return`
## ${l.text}
`;case"transition":return`
*${l.text}*
`;case"direction":return`*(${l.text})*`;case"action":return`
${l.text}
`;case"dialog":return`
**${l.speaker}**
${l.text}
`;default:return""}}).join(`
`);_downloadText(`# ${title}
${lines.trim()}
`,title.replace(/[^a-z0-9]/gi,"_")+".md")}(_qb=$("reh-impex-txt-btn"))==null||_qb.addEventListener("click",exportAsTxt),(_rb=$("reh-impex-md-btn"))==null||_rb.addEventListener("click",exportAsMd),function(){const inp=$("reh-impex-url"),btn=$("reh-impex-url-btn"),status2=$("reh-impex-url-status");if(!inp||!btn)return;async function fetchUrl(){const url=inp.value.trim();if(!url){toast("Paste a URL first","error");return}btn.disabled=!0,status2&&(status2.textContent="Fetching\u2026",status2.style.color="");try{const r=await fetch("/api/fetch-web-script",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json(),ta=$("reh-script-text");ta&&(ta.value=d.text);const titleInp=$("reh-script-title");titleInp&&d.title&&(titleInp.value=d.title.replace(/-/g," ").replace(/\b\w/g,c=>c.toUpperCase())),status2&&(status2.textContent=`\u2713 Fetched ${(d.chars/1e3).toFixed(0)} K chars \u2014 scroll down to Parse & cast`,status2.style.color="var(--green)"),navRehearserPhase(1),setTimeout(()=>ta==null?void 0:ta.scrollIntoView({behavior:"smooth",block:"nearest"}),300),toast(`Fetched "${d.title}" \u2014 click "Parse & cast" to continue`,"success")}catch(e){status2&&(status2.textContent="\u2717 "+e.message,status2.style.color="var(--red)"),toast("Fetch failed: "+e.message,"error")}finally{btn.disabled=!1}}btn.addEventListener("click",fetchUrl),inp.addEventListener("keydown",e=>{e.key==="Enter"&&fetchUrl()})}(),function(){const modal=$("reh-imsdb-modal"),grid=$("reh-imsdb-grid"),search=$("reh-imsdb-search"),countEl=$("reh-imsdb-count"),closeBtn=$("reh-imsdb-close"),coverBtn=$("reh-imsdb-cover-btn"),listBtn=$("reh-imsdb-list-btn"),urlInp=$("reh-imsdb-url"),urlBtn=$("reh-imsdb-url-fetch");if(!modal||!grid)return;const CACHE_KEY="reh-imsdb-cat-v1",CACHE_TTL=6*3600*1e3;let _catalogue=null,_posterObserver=null,_view=localStorage.getItem("reh-imsdb-view")||"list";function _applyView(){grid.classList.toggle("list-view",_view==="list"),coverBtn&&coverBtn.classList.toggle("active",_view==="cover"),listBtn&&listBtn.classList.toggle("active",_view==="list")}function _ensurePosterObserver(){return _posterObserver||(_posterObserver=new IntersectionObserver(entries=>{entries.forEach(async ent=>{if(!ent.isIntersecting)return;const card=ent.target;_posterObserver.unobserve(card);const title=card.dataset.title;try{const d=await fetch("/api/movie-poster?title="+encodeURIComponent(title)).then(r=>r.json());if(d.poster){const img=document.createElement("img");img.className="reh-imsdb-poster",img.loading="lazy",img.src=d.poster,img.alt=title,img.onload=()=>{var _a2;const wrap=card.querySelector(".reh-imsdb-poster-wrap");wrap&&((_a2=wrap.querySelector(".reh-imsdb-fallback"))==null||_a2.remove(),wrap.appendChild(img))}}d.year&&card.querySelectorAll(".reh-imsdb-year").forEach(y=>{y.textContent=d.year})}catch{}})},{root:grid,rootMargin:"300px"}),_posterObserver)}function _bookColor(title){let h=0;for(let i=0;i<title.length;i++)h=h*31+title.charCodeAt(i)>>>0;return`hsl(${h%360}, 45%, 42%)`}function renderGrid(items){const obs=_ensurePosterObserver();if(grid.innerHTML="",!items.length){grid.innerHTML='<div class="reh-imsdb-loading">No matches.</div>';return}const frag=document.createDocumentFragment();items.slice(0,400).forEach(it=>{const card=document.createElement("div");card.className="reh-imsdb-card",card.dataset.title=it.title,card.dataset.url=it.fetch_url;const color1=_bookColor(it.title),color2=_bookColor(it.title+"_");card.innerHTML=`
<div class="reh-imsdb-poster-wrap">
<div class="reh-imsdb-fallback" style="background:linear-gradient(160deg, ${color1}, ${color2})">
<span class="mdi mdi-movie-roll"></span>
<span class="reh-imsdb-fallback-title">${escHtml(it.title)}</span>
</div>
</div>
<div class="reh-imsdb-meta">
<span class="reh-imsdb-title">${escHtml(it.title)}</span>
<span class="reh-imsdb-year"></span>
</div>`,card.addEventListener("click",()=>importImsdb(it)),frag.appendChild(card),obs.observe(card)}),grid.appendChild(frag),countEl&&(countEl.textContent=`${items.length} script${items.length!==1?"s":""}${items.length>400?" (showing 400)":""}`),_applyView()}async function importAnyWebUrl(url,label="script"){if(url=String(url||"").trim(),!url){toast("Paste a script URL first","error");return}if(!/^https?:\/\//i.test(url)){toast("URL must start with http:// or https://","error");return}urlBtn&&(urlBtn.disabled=!0),grid.innerHTML=`<div class="reh-imsdb-loading"><span class="reh-imsdb-spinner"></span> Fetching ${escHtml(label)}\u2026</div>`;try{const r=await fetch("/api/fetch-web-script",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json(),ta=$("reh-script-text");ta&&(ta.value=d.text);const ti=$("reh-script-title");ti&&(ti.value=d.title||label),closeModal(),navRehearserPhase(1),setTimeout(()=>ta==null?void 0:ta.scrollIntoView({behavior:"smooth",block:"center"}),250),toast(`Loaded "${d.title||label}" (${(d.chars/1e3).toFixed(0)} K) \u2014 click "Parse & cast"`,"success")}catch(e){grid.innerHTML=`<div class="reh-imsdb-load-error">Fetch failed: ${escHtml(e.message)}</div>`,_catalogue&&renderGrid(_searchFilter(_catalogue)),toast("Script fetch failed: "+e.message,"error")}finally{urlBtn&&(urlBtn.disabled=!1)}}async function importImsdb(it){await importAnyWebUrl(it.fetch_url,it.title||"IMSDb script")}async function _loadCatalogue(){try{const raw=localStorage.getItem(CACHE_KEY);if(raw){const cached=JSON.parse(raw);if(Date.now()-cached.ts<CACHE_TTL&&Array.isArray(cached.items)&&cached.items.length){_catalogue=cached.items;return}}}catch{}_catalogue=(await fetch("/api/imsdb/list").then(r=>r.json())).items||[];try{localStorage.setItem(CACHE_KEY,JSON.stringify({ts:Date.now(),items:_catalogue}))}catch{}}async function openModal(){if(modal.hidden=!1,urlInp&&(urlInp.value=""),_applyView(),!_catalogue){grid.innerHTML='<div class="reh-imsdb-loading"><span class="reh-imsdb-spinner"></span> Loading catalogue\u2026</div>';try{await _loadCatalogue()}catch(e){grid.innerHTML=`<div class="reh-imsdb-loading">Failed to load: ${escHtml(e.message)}</div>`;return}}renderGrid(_searchFilter(_catalogue)),search==null||search.focus()}function closeModal(){modal.hidden=!0}function _searchFilter(items){const q=(search==null?void 0:search.value.trim().toLowerCase())||"";return q?items.filter(it=>it.title.toLowerCase().includes(q)):items}let _searchTimer=null;search==null||search.addEventListener("input",()=>{clearTimeout(_searchTimer),_searchTimer=setTimeout(()=>{_catalogue&&renderGrid(_searchFilter(_catalogue))},150)}),search==null||search.addEventListener("keydown",e=>{e.key==="Enter"&&/^https?:\/\//i.test(search.value.trim())&&(e.preventDefault(),importAnyWebUrl(search.value.trim(),"pasted URL"))}),urlBtn==null||urlBtn.addEventListener("click",()=>importAnyWebUrl((urlInp==null?void 0:urlInp.value)||"","pasted URL")),urlInp==null||urlInp.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),importAnyWebUrl(urlInp.value,"pasted URL"))}),coverBtn==null||coverBtn.addEventListener("click",()=>{_view="cover",localStorage.setItem("reh-imsdb-view",_view),_applyView()}),listBtn==null||listBtn.addEventListener("click",()=>{_view="list",localStorage.setItem("reh-imsdb-view",_view),_applyView()}),[$("reh-browse-imsdb-btn"),$("reh-browse-imsdb-btn-impex")].forEach(btn=>{btn==null||btn.addEventListener("click",openModal)}),closeBtn==null||closeBtn.addEventListener("click",closeModal),modal.addEventListener("click",e=>{e.target===modal&&closeModal()})}();const trainState={seq:[],turn:0,phase:"idle",cueSource:null,mediaRec:null,recChunks:[],recRaf:null,recStream:null,recCtx:null,recAnalyser:null,recTimer:null,recSecs:0};function _trainNormWords(str){return(str||"").toLowerCase().replace(/[^a-zäöüàáâãèéêëìíîïòóôõùúûüýÿæœßа-яёА-ЯЁ0-9\s]/g,"").trim().split(/\s+/).filter(Boolean)}function _trainLcs(a,b){const R=a.length,C=b.length,dp=Array.from({length:R+1},()=>new Int16Array(C+1));for(let i2=1;i2<=R;i2++)for(let j2=1;j2<=C;j2++)dp[i2][j2]=a[i2-1]===b[j2-1]?dp[i2-1][j2-1]+1:Math.max(dp[i2-1][j2],dp[i2][j2-1]);let i=R,j=C;const seq=[];for(;i>0&&j>0;)a[i-1]===b[j-1]?(seq.unshift([i-1,j-1]),i--,j--):dp[i-1][j]>=dp[i][j-1]?i--:j--;return seq}function _trainCompare(expected,actual){const ew=_trainNormWords(expected),aw=_trainNormWords(actual);if(!ew.length)return{html_exp:"",html_act:"",score:1};const pairs=_trainLcs(ew,aw),matchedE=new Set(pairs.map(p=>p[0])),matchedA=new Set(pairs.map(p=>p[1])),expHtml=ew.map((w,i)=>matchedE.has(i)?`<span class="tw-ok">${escHtml(w)}</span>`:`<span class="tw-miss">${escHtml(w)}</span>`).join(" "),actHtml=aw.map((w,i)=>matchedA.has(i)?`<span class="tw-ok">${escHtml(w)}</span>`:`<span class="tw-extra">${escHtml(w)}</span>`).join(" "),score=ew.length?Math.round(matchedE.size/ew.length*100):100;return{html_exp:expHtml,html_act:actHtml,score}}function buildTrainSeq(){const seq=[],lines=rehState.lines;for(let i=0;i<lines.length;i++){const line=lines[i];if(line.type!=="dialog")continue;const cast=rehState.cast[line.speaker];if(!cast||cast.voice!=="me")continue;const cueLines=[];for(let j=i-1;j>=0&&cueLines.length<3;j--){const prev=lines[j];if(prev.type==="scene"||prev.type==="act")break;if(prev.type!=="dialog")continue;const pc=rehState.cast[prev.speaker];if(!pc||pc.voice==="me")break;cueLines.unshift({index:j,speaker:prev.speaker,text:prev.text,emotion:prev.emotion||"",voice:pc.voice,color:pc.color,instruct:_buildInstruct(pc.instruct,prev.emotion,pc.voice),voiceData:pc.voiceData})}seq.push({cueLines,myLine:{index:i,speaker:line.speaker,text:line.text,color:cast.color}})}return seq}function _trainSetState(phase){trainState.phase=phase;const stateEl=$("reh-train-rec-state"),playBtn=$("reh-train-play"),recBtn=$("reh-train-record"),stopBtn=$("reh-train-stop-rec"),cueState=$("reh-train-cue-state"),map={idle:{state:"Read the cue above \xB7 press \u25B6 to hear it",play:!0,rec:!1,stop:!1},playing_cue:{state:"Playing cue\u2026",play:!1,rec:!1,stop:!1},ready:{state:"Press \u{1F3A4} to record your line",play:!1,rec:!0,stop:!1},recording:{state:"Recording\u2026",play:!1,rec:!1,stop:!0},transcribing:{state:"Transcribing\u2026",play:!1,rec:!1,stop:!1},done:{state:"Done \u2014 press \u25B6 to replay or \u23ED for next",play:!0,rec:!0,stop:!1}},m=map[phase]||map.idle;stateEl&&(stateEl.textContent=m.state),cueState&&phase==="playing_cue"?cueState.innerHTML='<span class="mdi mdi-volume-high" style="color:var(--accent)"></span>':cueState&&(cueState.innerHTML=""),playBtn&&(playBtn.disabled=!m.play),recBtn&&(recBtn.disabled=!m.rec,recBtn.hidden=!!m.stop),stopBtn&&(stopBtn.disabled=!m.stop,stopBtn.hidden=!m.stop);const timerEl=$("reh-train-rec-timer"),metersEl=$("reh-train-meters");phase==="recording"?(timerEl&&(timerEl.hidden=!1),metersEl&&(metersEl.hidden=!1)):phase!=="done"&&(timerEl&&(timerEl.hidden=!0),metersEl&&(metersEl.hidden=!0))}async function _trainPlayCueLines(cueLines){_trainSetState("playing_cue");for(const cue of cueLines){if(trainState.phase!=="playing_cue")break;let blob=rehState.synthCache.get(cue.index);if(!blob)try{blob=await fetchTtsPreviewBlob(cue.voice,_rehInlineTone(stripMarkdown(cue.text),cue.emotion),"wav",cue.instruct,_ttsBackendForVoice(cue.voice,rehState.backend))}catch(e){toast("Cue TTS failed: "+e.message,"error");break}if(trainState.phase!=="playing_cue")break;await new Promise(resolve=>{const url=URL.createObjectURL(blob),audio=new Audio(url);audio.onended=()=>{URL.revokeObjectURL(url),resolve()},audio.onerror=()=>{URL.revokeObjectURL(url),resolve()},trainState.cueAudio=audio,audio.play().catch(resolve)})}trainState.phase==="playing_cue"&&_trainSetState("ready")}async function _trainTranscribe(blob){_trainSetState("transcribing");try{const fd=new FormData;fd.append("file",blob,"train_rec.webm"),fd.append("backend",(_appSettings==null?void 0:_appSettings.stt_preferred_backend)||"configured");const r=await fetch("/api/transcribe-bytes",{method:"POST",body:fd});if(!r.ok)throw new Error(r.statusText);return((await r.json()).text||"").trim()}catch(e){return toast("Transcription failed: "+e.message,"error"),""}}async function trainGoTo(idx){trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),_trainStopMic(),trainState.turn=Math.max(0,Math.min(idx,trainState.seq.length-1));const turn=trainState.seq[trainState.turn];if(!turn)return;const progEl=$("reh-train-progress");progEl&&(progEl.textContent=`Turn ${trainState.turn+1} / ${trainState.seq.length}`);const cueCard=$("reh-train-cue"),cueAvEl=$("reh-train-cue-avatar"),cueSpeaker=$("reh-train-cue-speaker"),cueText=$("reh-train-cue-text");if(turn.cueLines.length){const lastCue=turn.cueLines[turn.cueLines.length-1];cueAvEl&&(cueAvEl.innerHTML=voiceAvatarHtml(lastCue.voice,lastCue.color,32)),cueSpeaker&&(cueSpeaker.textContent=lastCue.speaker),cueText&&(cueText.innerHTML=turn.cueLines.map(c=>`<div class="reh-train-cue-block">
<span class="reh-train-cue-who" style="color:${c.color}">${escHtml(c.speaker)}</span>
<span class="reh-train-cue-line">${escHtml(c.text)}</span>
</div>`).join("")),cueCard==null||cueCard.classList.remove("no-cue")}else cueAvEl&&(cueAvEl.innerHTML='<span class="mdi mdi-information-outline" style="font-size:24px;color:var(--subtext)"></span>'),cueSpeaker&&(cueSpeaker.textContent="No preceding cue"),cueText&&(cueText.textContent="This is the first line \u2014 speak when ready."),cueCard==null||cueCard.classList.add("no-cue");const mySpeaker=$("reh-train-my-speaker"),myText=$("reh-train-my-text");mySpeaker&&(mySpeaker.textContent=turn.myLine.speaker),myText&&(myText.textContent=turn.myLine.text);const resultEl=$("reh-train-result");resultEl&&(resultEl.hidden=!0);const prevAudio=$("reh-train-audio-preview");prevAudio&&(prevAudio.src="",prevAudio.style.display="none"),_trainSetState(turn.cueLines.length?"idle":"ready")}function _trainStartMeter(){const meterEl=$("reh-train-meter"),dbEl=$("reh-train-db"),waveEl=$("reh-train-wave");if(!trainState.recAnalyser||!meterEl)return;const analyser=trainState.recAnalyser,fft=new Uint8Array(analyser.frequencyBinCount),wCtx=waveEl==null?void 0:waveEl.getContext("2d"),W=(waveEl==null?void 0:waveEl.width)||300,H=(waveEl==null?void 0:waveEl.height)||36,BARS=18;function tick(){analyser.getByteFrequencyData(fft);const rms=fft.reduce((s,v)=>s+v*v,0)/fft.length,db=rms>0?20*Math.log10(Math.sqrt(rms)/128):-1/0;dbEl&&(dbEl.textContent=isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB");const slots=Array.from({length:BARS},(_,i)=>{const s=Math.floor(i/BARS*fft.length),e=Math.floor((i+1)/BARS*fft.length);return fft.slice(s,e).reduce((a,b)=>a+b,0)/(e-s)/255});meterEl.innerHTML=slots.map(v=>{const h=Math.max(2,Math.round(v*28)),col=v>.85?"var(--red)":v>.6?"#f59e0b":"var(--green)";return`<span style="height:${h}px;background:${col};width:4px;border-radius:2px;display:inline-block;vertical-align:bottom;margin:0 1px"></span>`}).join(""),wCtx&&(analyser.getByteTimeDomainData(fft),wCtx.clearRect(0,0,W,H),wCtx.beginPath(),wCtx.strokeStyle="var(--accent)",wCtx.lineWidth=1.5,fft.forEach((v,i)=>{const x=i/fft.length*W,y=v/255*H;i?wCtx.lineTo(x,y):wCtx.moveTo(x,y)}),wCtx.stroke()),trainState.recRaf=requestAnimationFrame(tick)}trainState.recRaf=requestAnimationFrame(tick)}function _trainStopMic(){if(trainState.recRaf&&(cancelAnimationFrame(trainState.recRaf),trainState.recRaf=null),trainState.recTimer&&(clearInterval(trainState.recTimer),trainState.recTimer=null),trainState.mediaRec&&trainState.mediaRec.state!=="inactive")try{trainState.mediaRec.stop()}catch{}if(trainState.mediaRec=null,trainState.recCtx){try{trainState.recCtx.close()}catch{}trainState.recCtx=null}trainState.recStream&&(trainState.recStream.getTracks().forEach(t=>t.stop()),trainState.recStream=null),trainState.recAnalyser=null,trainState.recChunks=[];const timerEl=$("reh-train-rec-timer");timerEl&&(timerEl.hidden=!0,timerEl.textContent="0:00");const metersEl=$("reh-train-meters");metersEl&&(metersEl.hidden=!0)}async function _trainStartRecording(){if(trainState.phase==="ready")try{const stream=await requestMicrophoneStream({raw:!0});trainState.recStream=stream;const ctx=new AudioContext;trainState.recCtx=ctx;const src=ctx.createMediaStreamSource(stream),analyser=ctx.createAnalyser();analyser.fftSize=256,trainState.recAnalyser=analyser;const dst=ctx.createMediaStreamDestination();src.connect(analyser),analyser.connect(dst),_trainSetState("recording"),_trainStartMeter();const timerEl=$("reh-train-rec-timer");timerEl&&(timerEl.hidden=!1),trainState.recSecs=0,trainState.recTimer=setInterval(()=>{trainState.recSecs++;const m=Math.floor(trainState.recSecs/60),s=trainState.recSecs%60;timerEl&&(timerEl.textContent=`${m}:${String(s).padStart(2,"0")}`)},1e3),trainState.recChunks=[];const mr=new MediaRecorder(dst.stream,{audioBitsPerSecond:128e3});trainState.mediaRec=mr,mr.ondataavailable=e=>{e.data.size>0&&trainState.recChunks.push(e.data)},mr.onstop=async()=>{var _a2;_trainStopMic();const blob=new Blob(trainState.recChunks,{type:"audio/webm"});trainState.recChunks=[];const url=URL.createObjectURL(blob),prevAudio=$("reh-train-audio-preview");prevAudio&&(prevAudio.src=url,prevAudio.style.display="");const transcript=await _trainTranscribe(blob),turn=trainState.seq[trainState.turn],{html_exp,html_act,score}=_trainCompare(((_a2=turn==null?void 0:turn.myLine)==null?void 0:_a2.text)||"",transcript),resultEl=$("reh-train-result"),expEl=$("reh-train-expected-text"),actEl=$("reh-train-actual-text"),scoreEl=$("reh-train-score");if(expEl&&(expEl.innerHTML=html_exp),actEl&&(actEl.innerHTML=html_act||'<em style="color:var(--subtext)">Nothing detected</em>'),scoreEl){const col=score>=90?"var(--green)":score>=60?"#f59e0b":"var(--red)";scoreEl.innerHTML=`<span style="color:${col};font-weight:700">${score}%</span>`}resultEl&&(resultEl.hidden=!1),_trainSetState("done")},mr.start()}catch(e){toast("Microphone error: "+e.message,"error"),_trainSetState("ready")}}function _trainStopRecording(){trainState.mediaRec&&trainState.mediaRec.state==="recording"&&trainState.mediaRec.stop()}function _trainHideStage(hide){[".reh-stage-area","#reh-tts-status-bar","#reh-rec-overlay","#reh-synth-bar"].forEach(sel=>{const el=document.querySelector(sel)||document.getElementById(sel.replace("#",""));el&&(el.style.display=hide?"none":"")})}function enterTrainMode(){const seq=buildTrainSeq();if(!seq.length){toast('No "I play this" lines found \u2014 check "I play this" on at least one character in the Cast tab.',"error");return}trainState.seq=seq,trainState.turn=0,trainState.phase="idle",_trainHideStage(!0);const panel=$("reh-train-panel");panel&&(panel.hidden=!1),trainGoTo(0)}function exitTrainMode(){trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),_trainStopMic(),trainState.phase="idle",trainState.seq=[],_trainHideStage(!1);const panel=$("reh-train-panel");panel&&(panel.hidden=!0)}if((_sb=$("reh-bulk-toggle"))==null||_sb.addEventListener("click",()=>setBulkMode(!rehState.bulkMode)),(_tb=$("reh-bulk-done"))==null||_tb.addEventListener("click",()=>setBulkMode(!1)),(_ub=$("reh-bulk-all"))==null||_ub.addEventListener("click",()=>{document.querySelectorAll("#reh-script-lines .reh-bulk-check").forEach(cb=>rehState.bulkSel.add(parseInt(cb.dataset.bulk))),rehState.bulkAnchor=rehState.bulkSel.size?Math.min(...rehState.bulkSel):null,document.querySelectorAll("#reh-script-lines .reh-bulk-check").forEach(cb=>_refreshBulkLine(parseInt(cb.dataset.bulk))),_updateBulkCount()}),(_vb=$("reh-bulk-none"))==null||_vb.addEventListener("click",()=>{const had=[...rehState.bulkSel];rehState.bulkSel.clear(),rehState.bulkAnchor=null,had.forEach(_refreshBulkLine),_updateBulkCount()}),(_wb=$("reh-bulk-ignore"))==null||_wb.addEventListener("click",()=>_bulkApply(l=>{l.ignored=!0})),(_xb=$("reh-bulk-unignore"))==null||_xb.addEventListener("click",()=>_bulkApply(l=>{l.ignored=!1})),(_yb=$("reh-bulk-hide"))==null||_yb.addEventListener("click",()=>_bulkApply(l=>{l.hidden=!0})),(_zb=$("reh-bulk-delete"))==null||_zb.addEventListener("click",_bulkDelete),(_Ab=$("reh-bulk-show-hidden"))==null||_Ab.addEventListener("change",e=>{rehState.showHidden=e.target.checked,buildScriptPage()}),(_Bb=$("reh-page-mode-btn"))==null||_Bb.addEventListener("click",cyclePageMode),(_Cb=$("reh-train-open-btn"))==null||_Cb.addEventListener("click",enterTrainMode),(_Db=$("reh-train-exit-btn"))==null||_Db.addEventListener("click",exitTrainMode),(_Eb=$("reh-train-play"))==null||_Eb.addEventListener("click",()=>{const turn=trainState.seq[trainState.turn];turn&&trainState.phase!=="playing_cue"&&(trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),turn.cueLines.length?_trainPlayCueLines(turn.cueLines):_trainSetState("ready"))}),(_Fb=$("reh-train-record"))==null||_Fb.addEventListener("click",()=>{(trainState.phase==="ready"||trainState.phase==="done")&&_trainStartRecording()}),(_Gb=$("reh-train-stop-rec"))==null||_Gb.addEventListener("click",_trainStopRecording),(_Hb=$("reh-train-prev"))==null||_Hb.addEventListener("click",()=>{trainState.turn>0&&trainGoTo(trainState.turn-1)}),(_Ib=$("reh-train-next"))==null||_Ib.addEventListener("click",()=>{trainState.turn<trainState.seq.length-1&&trainGoTo(trainState.turn+1)}),(_Jb=$("reh-train-repeat"))==null||_Jb.addEventListener("click",()=>{trainState.cueAudio&&(trainState.cueAudio.pause(),trainState.cueAudio=null),_trainStopMic();const turn=trainState.seq[trainState.turn];if(!turn)return;const resultEl=$("reh-train-result");resultEl&&(resultEl.hidden=!0),turn.cueLines.length?_trainPlayCueLines(turn.cueLines):_trainSetState("ready")}),rehRenderMeter(),_syncPageModeBtn(),renderLibraryList().catch(()=>{}),$("reh-skip-desc-toggle")&&($("reh-skip-desc-toggle").checked=rehState.skipDescriptions),window._rehearserStartImpEx)delete window._rehearserStartImpEx,showRehImpEx();else{const _initPhase=window._rehearserStartPhase||1;delete window._rehearserStartPhase,showPhase(_initPhase)}const readerState={mode:null,title:"",pdfDoc:null,pages:[],sentences:[],idx:0,readingIdx:-1,playing:!1,speed:1,scale:1,zoomMode:"fit-width",audioCtx:null,currentSource:null,raf:null,blobCache:new Map,bufCache:new Map,gainCache:new Map,unitsByPage:new Map,baseSentences:[],chunkMode:"sentence",normalize:!0,io:null,_seq:0,synthRunning:!1,synthCancel:!1,selecting:!1,selStart:null,selEnd:null,selAnchored:!1,fileBlob:null,docText:"",savedId:null,savedAudioIdx:new Set,sourceUploaded:!1,ocrWorker:null,synthStarted:!1};window.readerState=readerState;const READER_RESUME_KEY="reader-resume",READER_FMT="mp3",READER_BUF_WINDOW=3,READER_CANVAS_MARGIN=2200,READER_IMPORT_YIELD_EVERY=350,READER_PDF_INITIAL_PAGES=16,READER_PDF_INITIAL_MAX_WITHOUT_TEXT=64,READER_OCR_LANGS="deu+eng",READER_OCR_MIN_GAP_RATIO=.08,READER_OCR_MIN_GAP_PX=40,READER_OCR_RENDER_SCALE=2.5,READER_OCR_MIN_CONFIDENCE=40;function readerCtx(){return readerState.audioCtx||(readerState.audioCtx=new(window.AudioContext||window.webkitAudioContext)),readerState.audioCtx.state==="suspended"&&readerState.audioCtx.resume().catch(()=>{}),readerState.audioCtx}function readerYield(){return new Promise(resolve=>setTimeout(resolve,0))}window.readerOnShow=async function(){const sel=$("reader-backend-select");if(sel&&(!sel.value||sel.options.length<=1)){if(typeof availableTtsBackends=="function"&&!availableTtsBackends().length&&typeof refreshTtsBackendAvailability=="function")try{await refreshTtsBackendAvailability()}catch{}typeof availableTtsBackends=="function"&&availableTtsBackends().length&&(sel.innerHTML=ttsBackendOptions(sel.value),sel.disabled=!1)}readerUpdateBackendHint(),window.VoicePicker&&VoicePicker.upgrade("reader-voice-select"),readerRenderLibrary()},window.readerStop=function(){readerSaveResume(),readerPersistProgress(),readerStopPlayback()};async function readerFetchVoices(){var _a2;const btn=$("reader-fetch-voices-btn"),backend=(_a2=$("reader-backend-select"))==null?void 0:_a2.value;if(!backend){toast("No available TTS backend","error");return}btn&&(btn.disabled=!0);try{const raw=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json()),ids=(Array.isArray(raw)?raw:[]).map(v=>typeof v=="string"?v:v.id||v.name||v.voice_id).filter(Boolean),sel=$("reader-voice-select");if(!sel)return;const prev=sel.value;window.VoicePicker?(VoicePicker.upgrade("reader-voice-select"),VoicePicker.populate("reader-voice-select",ids),prev&&ids.includes(prev)?VoicePicker.setValue("reader-voice-select",prev):ids.length&&VoicePicker.setValue("reader-voice-select",ids[0])):(sel.innerHTML='<option value="">\u2014 select a voice \u2014</option>',ids.forEach(id=>{const o=document.createElement("option");o.value=o.textContent=id,sel.appendChild(o)}),prev&&ids.includes(prev)&&(sel.value=prev)),toast("Fetched "+ids.length+" voices","success")}catch(e){toast("Fetch failed: "+(e.message||e),"error")}finally{btn&&(btn.disabled=!1)}}function readerUpdateBackendHint(){var _a2;const el=$("reader-backend-hint");if(!el)return;const id=((_a2=$("reader-backend-select"))==null?void 0:_a2.value)||"",b=typeof backendById=="function"?backendById(id):null,steady=id&&(b&&b.style_aware||/design|custom|kokoro|magpie/i.test(id));el.innerHTML=steady?'<span class="mdi mdi-check-circle-outline"></span> Good for long-form narration. For maximum steadiness, increase \u201CVoice consistency\u201D.':'<span class="mdi mdi-information-outline"></span> Cloned / zero-shot voices re-sample each chunk, so the voice can drift between sections. To steady it: raise \u201CVoice consistency\u201D to paragraph/page, set a fixed <b>Seed</b> with a low <b>Temperature</b> (e.g. 0.3), keep \u201CNormalise loudness\u201D on, or pick a style-aware backend.',el.classList.toggle("reader-hint-warn",!steady)}function readerTitleFromText(text){return(String(text||"").split(/\r?\n/).map(x=>x.trim()).find(Boolean)||"Pasted text").replace(/\s+/g," ").slice(0,80)||"Pasted text"}function readerUpdateToolbarVisibility(loaded=!!readerState.sentences.length){const toolbar=$("reader-toolbar");toolbar&&(toolbar.hidden=!loaded);const pdfOnly=readerState.mode==="pdf",zoom=$("reader-zoom");zoom&&(zoom.hidden=!pdfOnly);const legend=document.querySelector(".reader-legend");legend&&(legend.hidden=!pdfOnly)}function readerFinishLoadedDocument({resume=!0,successPrefix="Loaded"}={}){if(!readerState.sentences.length)return toast("No readable text found in this document","error"),!1;readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),$("reader-transport").hidden=!1,$("reader-dropzone").hidden=!0;const importArea=$("reader-import-area");importArea&&(importArea.hidden=!0),readerUpdateToolbarVisibility(!0),$("reader-synthbar").hidden=!1;const pr=$("reader-page-range");if(pr&&(pr.hidden=readerState.mode!=="pdf",readerState.mode==="pdf")){const n=readerState.pages.length,pf=$("reader-page-from"),pt=$("reader-page-to");pf&&(pf.max=n,pf.value=1),pt&&(pt.max=n,pt.value=n)}readerUpdateScopeLabel();const resumed=resume&&readerLoadResume();return readerUpdateProgress(),readerHighlightSentence(readerState.sentences[readerState.idx]),toast(resumed?"Resumed at sentence "+(readerState.idx+1)+" / "+readerState.sentences.length:successPrefix+" \xB7 "+readerState.sentences.length+" sentences \u2014 press play","success"),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("source"),!0}async function readerImportFile(file){if(!file)return;const name=(file.name||"").toLowerCase();readerResetDoc(),readerState.title=file.name.replace(/\.[^.]+$/,""),readerState.fileBlob=file;const titleEl=$("reader-doc-title");titleEl&&(titleEl.textContent=readerState.title);try{if(name.endsWith(".pdf")){toast("Loading PDF\u2026","success");const loaded=await readerLoadPdfSkeleton(file);if(!loaded)return;readerShowExtractPrompt(loaded)}else{const text=await file.text();readerState.docText=text,await readerLoadText(text),readerFinishLoadedDocument({successPrefix:"Loaded"})}}catch(e){toast("Import failed: "+(e.message||e),"error")}}function readerShowExtractPrompt(loaded){const dz=$("reader-dropzone");dz&&(dz.hidden=!0);const importArea=$("reader-import-area");importArea&&(importArea.hidden=!0),readerUpdateToolbarVisibility(!0);const banner=$("reader-extract-banner");if(banner){banner.hidden=!1;const pc=$("reader-extract-pagecount");pc&&(pc.textContent=String(loaded.nPages))}const btn=$("reader-extract-btn");btn&&(btn.onclick=()=>readerRunExtraction(loaded)),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("source"),toast("PDF loaded \xB7 "+loaded.nPages+' pages \u2014 click "Extract Text" when ready',"success")}async function readerRunExtraction(loaded){const btn=$("reader-extract-btn"),original=btn?btn.innerHTML:"";btn&&(btn.disabled=!0,btn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span> Extracting\u2026');try{await readerExtractPdfText(loaded);const banner=$("reader-extract-banner");banner&&(banner.hidden=!0),readerFinishLoadedDocument({resume:!1,successPrefix:"Extracted"})}catch(e){toast("Text extraction failed: "+(e.message||e),"error")}finally{btn&&(btn.disabled=!1,btn.innerHTML=original)}}async function readerImportPastedText(){const ta=$("reader-paste-text"),text=((ta==null?void 0:ta.value)||"").trim();if(!text){toast("Paste some text first","error"),ta==null||ta.focus();return}const btn=$("reader-paste-load");btn&&(btn.disabled=!0),readerResetDoc(),readerState.title=readerTitleFromText(text),readerState.docText=text,readerState.fileBlob=null,readerState.sourceUploaded=!1;const titleEl=$("reader-doc-title");titleEl&&(titleEl.textContent=readerState.title);try{await readerLoadText(text),readerFinishLoadedDocument({resume:!1,successPrefix:"Loaded pasted text"})}catch(e){toast("Import failed: "+(e.message||e),"error")}finally{btn&&(btn.disabled=!1)}}function readerResetDoc(){readerStopPlayback(),readerState._seq++,readerState.mode=null,readerState.pdfDoc=null,readerState.pages=[],readerState.sentences=[],readerState.idx=0,readerState.readingIdx=-1,readerState.scale=1,readerState.zoomMode="fit-width",readerState.blobCache.clear(),readerState.bufCache.clear(),readerState.gainCache.clear(),readerState.unitsByPage=new Map,readerState.baseSentences=[],readerState.fileBlob=null,readerState.docText="",readerState.savedId=null,readerState.savedAudioIdx=new Set,readerState.sourceUploaded=!1;const extractBanner=$("reader-extract-banner");extractBanner&&(extractBanner.hidden=!0),readerState.io&&(readerState.io.disconnect(),readerState.io=null),readerState.synthRunning=!1,readerState.synthCancel=!1,readerState.synthStarted=!1,readerSetSelecting(!1),readerState.selStart=readerState.selEnd=null,readerState.selAnchored=!1;const sb=$("reader-synthbar");sb&&(sb.hidden=!0);const tr=$("reader-transport");tr&&(tr.hidden=!0),readerUpdateToolbarVisibility(!1);const sp=$("reader-synth-prog");sp&&(sp.hidden=!0);const si=$("reader-sel-info");si&&(si.hidden=!0);const doc=$("reader-doc");doc&&(doc.innerHTML="",doc.classList.remove("two-page","reader-selecting","reader-synth-started")),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("source");const importArea=$("reader-import-area");importArea&&(importArea.hidden=!1),readerClearSearch({clearInput:!0})}const READER_NO_LEADING_SPACE_RE=/^[«"'’”)\]]+$/;function readerBuildSentences(words){const sentences=[];let cur=null;const endRe=/[.!?]["'”’)\]]?$/,abbrev=/^(mr|mrs|ms|dr|prof|sr|jr|vs|etc|e\.g|i\.e|no|vol|st|fig)\.?$/i;for(const w of words)w.para&&cur&&cur.words.length&&(sentences.push(cur),cur=null),cur||(cur={text:"",words:[],status:"pending",_stat:null,paraStart:!!w.para}),cur.words.push(w),cur.text+=(cur.text&&!READER_NO_LEADING_SPACE_RE.test(w.text)?" ":"")+w.text,(endRe.test(w.text)&&!abbrev.test(w.text.replace(/[^a-z.]/gi,""))&&cur.words.length>=2||cur.words.length>=45)&&(sentences.push(cur),cur=null);return cur&&cur.words.length&&sentences.push(cur),sentences}function readerGroupUnits(base,mode){if(mode==="sentence"||!base.length)return base.map(s=>({text:s.text,words:s.words,status:"pending",_stat:null,paraStart:!!s.paraStart}));const maxChars=mode==="page"?2e3:500,isPdf=readerState.mode==="pdf",keyOf=s=>{var _a2,_b2,_c2,_d2;return isPdf?(_b2=(_a2=s.words[0])==null?void 0:_a2.page)!=null?_b2:0:(_d2=(_c2=s.words[0])==null?void 0:_c2.para)!=null?_d2:0},units=[];let cur=null;for(const s of base){const k=keyOf(s),boundary=cur&&(mode==="paragraph"&&k!==cur._key||mode==="page"&&isPdf&&k!==cur._key),tooLong=cur&&cur.words.length&&cur.text.length+s.text.length+1>maxChars;(boundary||tooLong)&&(units.push(cur),cur=null),cur||(cur={text:"",words:[],status:"pending",_stat:null,_key:k,paraStart:!!s.paraStart}),cur.text+=cur.text?(s.paraStart?`
`:" ")+s.text:s.text,cur.words.push(...s.words)}return cur&&units.push(cur),units}function readerMarkParagraphBreaks(pageWords){if(!pageWords.length)return;const lines=[];let curLine=null,curTop=null;for(const w of pageWords)curLine&&Math.abs(w.top-curTop)<w.h*.4?curLine.push(w):(curLine=[w],curTop=w.top,lines.push(curLine));if(lines[0][0].para=!0,lines.length<3)return;const gaps=[];for(let i=1;i<lines.length;i++)gaps.push(lines[i][0].top-lines[i-1][0].top);const sorted=[...gaps].sort((a,b)=>a-b),median=sorted[Math.floor(sorted.length/2)]||0;if(!(median<=0))for(let i=1;i<lines.length;i++)lines[i][0].top-lines[i-1][0].top>median*1.35&&(lines[i][0].para=!0)}async function loadTesseract(){window.Tesseract||await new Promise((resolve,reject)=>{const s=document.createElement("script");s.src="/static/js/tesseract/tesseract.min.js",s.onload=resolve,s.onerror=reject,document.head.appendChild(s)})}async function readerGetOcrWorker(){return readerState.ocrWorker||(await loadTesseract(),readerState.ocrWorker=await Tesseract.createWorker(READER_OCR_LANGS,1,{workerPath:"/static/js/tesseract/worker.min.js",corePath:"/static/js/tesseract/tesseract-core-simd-lstm.wasm.js",langPath:"/static/js/tesseract/lang",gzip:!0})),readerState.ocrWorker}function _readerTimeout(promise,ms,label){return new Promise(function(resolve,reject){const t=setTimeout(function(){reject(new Error((label||"operation")+" timed out after "+ms+"ms"))},ms);promise.then(function(v){clearTimeout(t),resolve(v)},function(e){clearTimeout(t),reject(e)})})}async function _readerOcrAttempt(worker,canvas,psm){var _a2;await worker.setParameters({tessedit_pageseg_mode:psm});const{data}=await _readerTimeout(worker.recognize(canvas),2e4,"Heading OCR"),text=(data.text||"").replace(/\s+/g," ").trim();return!text||((_a2=data.confidence)!=null?_a2:0)<READER_OCR_MIN_CONFIDENCE?null:text}async function readerOcrPageHeading(page,base,pageIdx,gapPx){let crop,tightCrop;try{const worker=await readerGetOcrWorker(),viewport=page.getViewport({scale:READER_OCR_RENDER_SCALE}),cropH=Math.max(Math.round(gapPx*READER_OCR_RENDER_SCALE),1);crop=document.createElement("canvas"),crop.width=viewport.width,crop.height=cropH,await _readerTimeout(page.render({canvasContext:crop.getContext("2d"),viewport}).promise,15e3,"Page render");let text=null;if(cropH>40){const y0=Math.round(cropH*.45),inset=Math.round(crop.width*.03);tightCrop=document.createElement("canvas"),tightCrop.width=Math.max(crop.width-inset*2,1),tightCrop.height=cropH-y0,tightCrop.getContext("2d").putImageData(crop.getContext("2d").getImageData(inset,y0,tightCrop.width,tightCrop.height),0,0),text=await _readerOcrAttempt(worker,tightCrop,"7")}if(text||(text=await _readerOcrAttempt(worker,crop,"3")),!text)return[];const tokens=text.split(" ").filter(t=>t&&/[a-zäöüßàâçéèêëîïôûùüÿñæœ0-9]/i.test(t));if(!tokens.length)return[];const bandH=Math.max(gapPx-4,10),avgCharW=Math.max((base.width-16)/text.length,4),words=[];let x=8;for(const tok of tokens){const w=Math.max(tok.length*avgCharW,10);words.push({page:pageIdx,x,top:4,w,h:bandH,text:tok,ocr:!0}),x+=w+avgCharW}return words}catch(e){return console.warn("Heading OCR failed",e),[]}finally{crop&&(crop.width=0,crop.height=0),tightCrop&&(tightCrop.width=0,tightCrop.height=0)}}async function readerLoadPdfSkeleton(file){await loadPdfJs();const seq=readerState._seq,ab=await file.arrayBuffer(),pdf=await pdfjsLib.getDocument({data:ab}).promise;if(seq!==readerState._seq)return null;readerState.mode="pdf",readerState.pdfDoc=pdf;const doc=$("reader-doc"),nPages=pdf.numPages,page1=await pdf.getPage(1);if(seq!==readerState._seq)return null;const base1=page1.getViewport({scale:1});for(let p=1;p<=nPages;p++){const pageDiv=document.createElement("div");pageDiv.className="reader-page",pageDiv.style.width=base1.width+"px",pageDiv.style.height=base1.height+"px";const overlay=document.createElement("div");overlay.className="reader-overlay",pageDiv.appendChild(overlay),doc.appendChild(pageDiv),readerState.pages.push({pageDiv,overlay,page:null,base:base1,rendered:!1,renderTask:null}),p%50===0&&await readerYield()}return readerApplyZoom("fit-width"),readerSetupLazyRaster(),{pdf,page1,nPages}}async function readerExtractPdfText(loaded){var _a2;const seq=readerState._seq,pdf=loaded.pdf,page1=loaded.page1,nPages=loaded.nPages,progress=readerShowParseProgress(nPages),ocrHeadingsEnabled=((_a2=$("reader-ocr-headings"))==null?void 0:_a2.checked)!==!1;for(let p=1;p<=nPages;p++){const page=p===1?page1:await pdf.getPage(p);if(seq!==readerState._seq){progress.done();return}const base=page.getViewport({scale:1}),pageIdx=p-1,pgState=readerState.pages[pageIdx];pgState.page=page,pgState.base=base,pgState.pageDiv.style.width=base.width*readerState.scale+"px",pgState.pageDiv.style.height=base.height*readerState.scale+"px",readerIsPageNearView(pgState)&&readerRenderPage(pageIdx);let content;try{content=await _readerTimeout(page.getTextContent(),2e4,"Page text extraction")}catch(e){console.warn("getTextContent failed/timed out on page",p,e),content={items:[]}}if(seq!==readerState._seq)return;await readerYield();const pageWords=[];for(let itemIdx=0;itemIdx<content.items.length;itemIdx++){const item=content.items[itemIdx],str=item.str||"";if(!str.trim())continue;const m=pdfjsLib.Util.transform(base.transform,item.transform),h=Math.hypot(m[2],m[3])||Math.hypot(m[0],m[1])||12,x=m[4],top=m[5]-h,w=item.width||0,tokens=str.split(/(\s+)/),totalLen=str.length||1;let offX=x;for(const tok of tokens){const tw=tok.length/totalLen*w;tok.trim()&&pageWords.push({page:pageIdx,x:offX,top,w:Math.max(tw,2),h,text:tok}),offX+=tw}itemIdx&&itemIdx%READER_IMPORT_YIELD_EVERY===0&&await readerYield()}if(ocrHeadingsEnabled){let gapPx=base.height;for(const w of pageWords)w.top<gapPx&&(gapPx=w.top);if(gapPx>Math.max(base.height*READER_OCR_MIN_GAP_RATIO,READER_OCR_MIN_GAP_PX)){const ocrWords=await readerOcrPageHeading(page,base,pageIdx,gapPx);if(seq!==readerState._seq)return;ocrWords.length&&pageWords.unshift(...ocrWords)}}readerMarkParagraphBreaks(pageWords);const pageBase=readerBuildSentences(pageWords),pageUnits=readerGroupUnits(pageBase,readerState.chunkMode);pageUnits.length||pageUnits.push({text:"",words:[{page:pageIdx,x:0,top:0,w:0,h:0,text:""}],status:"pending",_stat:null,paraStart:!1});const startUnit=readerState.sentences.length;readerState.baseSentences.push(...pageBase),readerState.sentences.push(...pageUnits);const idxs=[];for(let u=startUnit;u<readerState.sentences.length;u++)idxs.push(u);readerState.unitsByPage.set(pageIdx,idxs),progress.update(p),await new Promise(r=>setTimeout(r,0))}if(progress.done(),readerState.ocrWorker){try{readerState.ocrWorker.terminate()}catch{}readerState.ocrWorker=null}}async function readerLoadPdf(file){const loaded=await readerLoadPdfSkeleton(file);loaded&&await readerExtractPdfText(loaded)}function readerShowParseProgress(total){const doc=$("reader-doc");if(!doc)return{update(){},done(){}};const el=document.createElement("div");el.className="reader-parsing",el.innerHTML='<span class="mdi mdi-loading mdi-spin" style="font-size:26px;"></span> <span>Reading PDF\u2026 page 0 / '+total+"</span>",el.style.cssText="position:fixed; top:80px; left:50%; transform:translateX(-50%); z-index:100; background:var(--accent); color:#fff; padding:14px 28px; border-radius:32px; font-size:18px; display:inline-flex; align-items:center; gap:12px; box-shadow:0 6px 24px rgba(0,0,0,.35); font-weight:700;",doc.insertBefore(el,doc.firstChild);const label=el.querySelector("span:last-child");return{update(p){label&&(label.textContent="Reading PDF\u2026 page "+p+" / "+total)},done(){el.remove()}}}function readerComputeScale(mode){const doc=$("reader-doc");if(!doc||!readerState.pages.length)return 1;let maxW=0,maxH=0;for(const p of readerState.pages)p.base&&(p.base.width>maxW&&(maxW=p.base.width),p.base.height>maxH&&(maxH=p.base.height));if(!maxW)return 1;const padW=36,gap=16,availW=(doc.clientWidth||900)-padW,availH=(doc.clientHeight||600)-36;return mode==="fit-width"?Math.max(.2,availW/maxW):mode==="fit-height"?Math.max(.2,availH/maxH):mode==="two"?Math.max(.2,(availW-gap)/2/maxW):readerState.scale}function readerApplyZoom(mode){if(!readerState.pages.length)return;mode&&(readerState.zoomMode=mode),mode&&mode!=="custom"&&(readerState.scale=readerComputeScale(mode));const scale=readerState.scale;$("reader-doc").classList.toggle("two-page",readerState.zoomMode==="two"),readerState.pages.forEach(pg=>{pg.pageDiv.style.width=pg.base.width*scale+"px",pg.pageDiv.style.height=pg.base.height*scale+"px";const old=pg.pageDiv.querySelector("canvas.reader-canvas");if(old&&old.remove(),pg.renderTask){try{pg.renderTask.cancel()}catch{}pg.renderTask=null}pg.rendered=!1}),readerState.pages.forEach(pg=>pg.overlay.querySelectorAll(".reader-stat").forEach(el=>{const i=parseInt(el.dataset.si);isNaN(i)||readerPaintStatus(i)})),readerPaintSearchHighlights(),readerState.idx<readerState.sentences.length&&readerHighlightSentence(readerState.sentences[readerState.idx]);const pct=$("reader-zoom-pct");pct&&(pct.textContent=Math.round(scale*100)+"%"),readerRenderVisible()}function readerSetupLazyRaster(){readerState.io&&readerState.io.disconnect(),readerState.io=new IntersectionObserver(entries=>{entries.forEach(en=>{if(!en.isIntersecting)return;const idx=readerState.pages.findIndex(p=>p.pageDiv===en.target);idx>=0&&readerRenderPage(idx)})},{root:$("reader-doc"),rootMargin:"600px 0px"}),readerState.pages.forEach(p=>readerState.io.observe(p.pageDiv))}function readerRenderVisible(){$("reader-doc")&&(readerState.pages.forEach((pg,i)=>{readerIsPageNearView(pg,800)&&readerRenderPage(i)}),readerEvictCanvases())}function readerIsPageNearView(pg,margin=800){const doc=$("reader-doc");if(!doc||!(pg!=null&&pg.pageDiv))return!1;const dr=doc.getBoundingClientRect(),r=pg.pageDiv.getBoundingClientRect();return r.bottom>dr.top-margin&&r.top<dr.bottom+margin}function readerEvictCanvases(dr){const doc=$("reader-doc");doc&&(dr=dr||doc.getBoundingClientRect(),readerState.pages.forEach(pg=>{if(!pg.rendered)return;const r=pg.pageDiv.getBoundingClientRect();if(r.bottom<dr.top-READER_CANVAS_MARGIN||r.top>dr.bottom+READER_CANVAS_MARGIN){if(pg.renderTask){try{pg.renderTask.cancel()}catch{}pg.renderTask=null}const c=pg.pageDiv.querySelector("canvas.reader-canvas");c&&c.remove(),pg.rendered=!1}}))}const READER_MAX_CANVAS_PIXELS=2e6,READER_MAX_CANVAS_SIDE=1800;async function readerRenderPage(idx){var _a2;const pg=readerState.pages[idx];if(!pg||pg.rendered||!pg.page)return;pg.rendered=!0;const scale=readerState.scale,viewport=pg.page.getViewport({scale});let renderViewport=viewport;const overArea=viewport.width*viewport.height>READER_MAX_CANVAS_PIXELS,overSide=viewport.width>READER_MAX_CANVAS_SIDE||viewport.height>READER_MAX_CANVAS_SIDE;if(overArea||overSide){let fit=overArea?Math.sqrt(READER_MAX_CANVAS_PIXELS/(viewport.width*viewport.height)):1;fit=Math.min(fit,READER_MAX_CANVAS_SIDE/Math.max(viewport.width,viewport.height)),renderViewport=pg.page.getViewport({scale:scale*fit})}const canvas=document.createElement("canvas");canvas.className="reader-canvas",canvas.width=Math.floor(renderViewport.width),canvas.height=Math.floor(renderViewport.height),canvas.style.width=Math.floor(viewport.width)+"px",canvas.style.height=Math.floor(viewport.height)+"px",pg.pageDiv.insertBefore(canvas,pg.overlay),readerCreatePageStatus(idx);try{pg.renderTask=pg.page.render({canvasContext:canvas.getContext("2d"),viewport:renderViewport}),await _readerTimeout(pg.renderTask.promise,15e3,"Page render")}catch{pg.rendered=!1,canvas.remove();try{(_a2=pg.renderTask)==null||_a2.cancel()}catch{}}finally{pg.renderTask=null}}async function readerLoadText(text){readerState.mode="text",readerState.docText=text;const doc=$("reader-doc"),pane=document.createElement("div");pane.className="reader-text",doc.appendChild(pane);const words=[],paras=text.replace(/\r\n/g,`
`).split(/\n{2,}/),frag=document.createDocumentFragment();let renderedWords=0;for(let pi=0;pi<paras.length;pi++){const para=paras[pi],pEl=document.createElement("p");pEl.className="reader-text-para";const lines=para.split(`
`);for(let li=0;li<lines.length;li++){const toks=lines[li].trim().split(/\s+/);for(const tok of toks){if(!tok)continue;const span=document.createElement("span");span.className="reader-word",span.textContent=tok,pEl.appendChild(span),pEl.appendChild(document.createTextNode(" ")),words.push({el:span,text:tok,page:0,para:pi}),renderedWords++,renderedWords%READER_IMPORT_YIELD_EVERY===0&&await readerYield()}li<lines.length-1&&pEl.appendChild(document.createElement("br"))}frag.appendChild(pEl),pi&&pi%20===0&&await readerYield()}pane.appendChild(frag),readerState.baseSentences=readerBuildSentences(words),await readerYield(),readerState.sentences=readerGroupUnits(readerState.baseSentences,readerState.chunkMode),pane.addEventListener("click",e=>{const span=e.target.closest(".reader-word");if(!span)return;const si=readerState.sentences.findIndex(s=>s.words.some(w=>w.el===span));si<0||(readerState.selecting?readerPickSentence(si):readerJumpTo(si))})}function readerSentBBox(sentence){var _a2,_b2;const pageIdx=(_b2=(_a2=sentence.words[0])==null?void 0:_a2.page)!=null?_b2:0,on=sentence.words.filter(w=>w.page===pageIdx);return{page:pageIdx,x:Math.min(...on.map(w=>w.x)),top:Math.min(...on.map(w=>w.top)),w:Math.max(...on.map(w=>w.x+w.w))-Math.min(...on.map(w=>w.x)),h:Math.max(...on.map(w=>w.top+w.h))-Math.min(...on.map(w=>w.top))}}function readerBuildUnitIndex(){readerState.unitsByPage=new Map,readerState.mode==="pdf"&&readerState.sentences.forEach((s,i)=>{var _a2,_b2;const pg=(_b2=(_a2=s.words[0])==null?void 0:_a2.page)!=null?_b2:0;readerState.unitsByPage.has(pg)||readerState.unitsByPage.set(pg,[]),readerState.unitsByPage.get(pg).push(i)})}function readerCreatePageStatus(pageIdx){var _a2;if(readerState.mode!=="pdf")return;const pg=readerState.pages[pageIdx];if(!pg)return;const idxs=((_a2=readerState.unitsByPage)==null?void 0:_a2.get(pageIdx))||[];for(const i of idxs){const s=readerState.sentences[i];if(!s||s._stat)continue;const el=document.createElement("div");el.className="reader-stat",el.dataset.si=i,pg.overlay.appendChild(el),s._stat=el,readerPaintStatus(i)}}function readerEffectiveStatus(s){return s.status}function readerSetStatus(idx,status2){var _a2;const s=readerState.sentences[idx];s&&(s.status=status2,status2!=="pending"&&!readerState.synthStarted&&(readerState.synthStarted=!0,(_a2=$("reader-doc"))==null||_a2.classList.add("reader-synth-started")),readerPaintStatus(idx))}function readerPaintStatus(idx){const s=readerState.sentences[idx];if(!s)return;const eff=readerEffectiveStatus(s);if(readerState.mode==="pdf"){if(!s._stat)return;const b=readerSentBBox(s),scale=readerState.scale;Object.assign(s._stat.style,{left:b.x*scale+"px",top:b.top*scale+"px",width:b.w*scale+"px",height:b.h*scale+"px"}),s._stat.className="reader-stat reader-stat-"+eff}else s.words.forEach(w=>{w.el&&(w.el.classList.remove("stat-pending","stat-synth","stat-ready","stat-reading"),w.el.classList.add("stat-"+eff))})}let _readerWordBox=null;function readerEnsureBox(){_readerWordBox||(_readerWordBox=document.createElement("div"),_readerWordBox.className="reader-hl-word")}function readerClearHighlights(){readerState.mode==="text"?document.querySelectorAll(".reader-word.is-word").forEach(el=>el.classList.remove("is-word")):_readerWordBox&&(_readerWordBox.hidden=!0)}function readerEnsureVisible(el){const doc=$("reader-doc");if(!doc||!el)return;const dr=doc.getBoundingClientRect(),er=el.getBoundingClientRect();if(er.top<dr.top+70||er.bottom>dr.bottom-50){const delta=er.top-dr.top-doc.clientHeight*.38;doc.scrollTo({top:doc.scrollTop+delta,behavior:"smooth"})}}function readerHighlightSentence(sentence){var _a2;sentence&&(readerState.mode==="text"?readerEnsureVisible((_a2=sentence.words[0])==null?void 0:_a2.el):sentence._stat&&readerEnsureVisible(sentence._stat))}function readerHighlightWord(sentence,wi){const w=sentence.words[wi];if(!w)return;if(readerState.mode==="text"){document.querySelectorAll(".reader-word.is-word").forEach(el=>el.classList.remove("is-word")),w.el&&(w.el.classList.add("is-word"),readerEnsureVisible(w.el));return}readerEnsureBox();const pg=readerState.pages[w.page];if(!pg)return;const scale=readerState.scale;_readerWordBox.parentNode!==pg.overlay&&pg.overlay.appendChild(_readerWordBox),Object.assign(_readerWordBox.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:w.w*scale+"px",height:w.h*scale+"px"}),_readerWordBox.hidden=!1,readerEnsureVisible(_readerWordBox)}function readerSetSelecting(on){readerState.selecting=!!on,readerState.selAnchored=!1;const doc=$("reader-doc");doc&&doc.classList.toggle("reader-selecting",readerState.selecting);const btn=$("reader-select-toggle");btn&&btn.classList.toggle("active",readerState.selecting);const lbl=$("reader-select-label");lbl&&(lbl.textContent=readerState.selecting?"Done":"Select range");const ic=$("reader-select-icon");ic&&(ic.className="mdi "+(readerState.selecting?"mdi-check":"mdi-cursor-default-click-outline")),readerSelHint(readerState.selecting?"Click the <b>start</b> sentence\u2026":""),readerState.selecting||readerPaintSelection()}function readerSelHint(html){const h=$("reader-sel-hint");h&&(h.innerHTML=html,h.hidden=!html)}function readerPickSentence(si){if(!readerState.selAnchored)readerState.selStart=si,readerState.selEnd=si,readerState.selAnchored=!0,readerSelHint("Start at sentence <b>"+(si+1)+"</b> \u2014 now click the <b>end</b> sentence");else{readerState.selEnd=si,readerState.selAnchored=!1;const r=readerSelRange();readerSelHint("Selected <b>"+(r[1]-r[0]+1)+"</b> sentences \u2014 click a new start, or <b>Done</b>")}readerPaintSelection(),readerUpdateScopeLabel()}function readerClearSelection(){readerState.selStart=readerState.selEnd=null,readerState.selAnchored=!1,readerPaintSelection(),readerUpdateScopeLabel(),readerState.selecting&&readerSelHint("Click the <b>start</b> sentence\u2026")}function readerSelRange(){return readerState.selStart===null||readerState.selEnd===null?null:[Math.min(readerState.selStart,readerState.selEnd),Math.max(readerState.selStart,readerState.selEnd)]}function readerPaintSelection(){const range=readerSelRange(),inSel=i=>range&&i>=range[0]&&i<=range[1],anchorI=readerState.selAnchored&&range&&range[0]===range[1]?range[0]:-1;readerState.sentences.forEach((s,i)=>{const sel=inSel(i)&&i!==anchorI,anchor=i===anchorI;readerState.mode==="pdf"?s._stat&&(s._stat.classList.toggle("sel",sel),s._stat.classList.toggle("sel-anchor",anchor)):s.words.forEach(w=>{w.el&&(w.el.classList.toggle("in-sel",sel),w.el.classList.toggle("sel-anchor",anchor))})})}function readerScopeIndices(){var _a2,_b2;const all=readerState.sentences.map((_,i)=>i),range=readerSelRange();if(range)return all.filter(i=>i>=range[0]&&i<=range[1]);if(readerState.mode==="pdf"){const n=readerState.pages.length;let from=parseInt((_a2=$("reader-page-from"))==null?void 0:_a2.value)||1,to=parseInt((_b2=$("reader-page-to"))==null?void 0:_b2.value)||n;return from=Math.max(1,Math.min(from,n)),to=Math.max(from,Math.min(to,n)),all.filter(i=>{var _a3,_b3;const p=((_b3=(_a3=readerState.sentences[i].words[0])==null?void 0:_a3.page)!=null?_b3:0)+1;return p>=from&&p<=to})}return all}function readerUpdateScopeLabel(){const scopeEl=$("reader-synth-scope"),range=readerSelRange();scopeEl&&(range?scopeEl.textContent="selection":readerState.mode==="pdf"?scopeEl.textContent="pages":scopeEl.textContent="all");const info=$("reader-sel-info"),txt=$("reader-sel-text");info&&(info.hidden=!range),txt&&range&&(txt.textContent="sentences "+(range[0]+1)+"\u2013"+(range[1]+1)+" ("+(range[1]-range[0]+1)+")")}function _readerTextForSynth(text){var _a2,_b2;const backend=((_a2=$("reader-backend-select"))==null?void 0:_a2.value)||"",emotion=((_b2=$("reader-emotion-select"))==null?void 0:_b2.value)||"";if(!/fish/i.test(backend)||!emotion||/^\s*\[/.test(text))return text;const tag=typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(emotion):"";return tag?`[${tag}] ${text}`:text}setTimeout(function(){const sel=$("reader-emotion-select");!sel||typeof window.REH_EMOTIONS=="undefined"||(window.REH_EMOTIONS.forEach(e=>{if(!e.value)return;const o=document.createElement("option");o.value=e.value,o.textContent=`${e.emoji} ${e.label}`,sel.appendChild(o)}),sel.addEventListener("change",()=>{var _a2;const backend=((_a2=$("reader-backend-select"))==null?void 0:_a2.value)||"";if(!/fish/i.test(backend)){const instructInput=$("reader-instruct");instructInput&&sel.value&&(instructInput.value=sel.value)}}))},0);async function readerSynthIndices(targets){var _a2,_b2,_c2;if(targets=targets.filter(i=>!readerState.blobCache.has(i)),!targets.length||readerState.synthRunning)return{done:0,failed:[]};const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice)return toast("Pick a voice first","error"),{done:0,failed:[]};if(!backend)return toast("No TTS backend selected","error"),{done:0,failed:[]};const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.synthRunning=!0,readerState.synthCancel=!1;const prog=$("reader-synth-prog");prog&&(prog.hidden=!1);const total=targets.length;let done=0;const failed=[],update=()=>{const f=$("reader-synth-fill");f&&(f.style.width=done/total*100+"%");const l=$("reader-synth-label");l&&(l.textContent=done+" / "+total)};update();try{const queue=targets.slice(),worker=async()=>{var _a3;for(;queue.length&&!readerState.synthCancel;){const i=queue.shift();if(readerState.blobCache.has(i)){done++,update();continue}readerState.sentences[i].status==="pending"&&readerSetStatus(i,"synth");try{const blob=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[i].text),READER_FMT,instruct,backend,!1,readerGenParams());readerState.blobCache.set(i,blob),readerState.sentences[i].status==="synth"&&readerSetStatus(i,"ready")}catch(e){failed.push(i),((_a3=readerState.sentences[i])==null?void 0:_a3.status)==="synth"&&readerSetStatus(i,"pending"),console.error("[reader] synth failed for sentence",i,e)}done++,update()}},N=Math.min(2,targets.length);await Promise.all(Array.from({length:N},worker))}finally{readerState.synthRunning=!1,prog&&(prog.hidden=!0)}return{done,failed}}async function readerSynthAll(){const targets=readerScopeIndices().filter(i=>!readerState.blobCache.has(i));if(!targets.length){toast("Selected range is already synthesised","success");return}const{done,failed}=await readerSynthIndices(targets);if(readerState.synthCancel){toast("Synthesis cancelled ("+done+" done)","error");return}if(failed.length){toast(done-failed.length+" / "+done+" sentences synthesised \u2014 "+failed.length+" failed, see red markers","error");return}toast("Synthesised "+done+" sentences","success")}function readerSafeName(s){return(s||"audio").replace(/[\/\\:*?"<>|]+/g,"_").replace(/\s+/g," ").trim().slice(0,60)||"audio"}function readerPad(n){return String(n).padStart(2,"0")}function readerPageOf(i){var _a2,_b2;return readerState.mode==="pdf"?((_b2=(_a2=readerState.sentences[i].words[0])==null?void 0:_a2.page)!=null?_b2:0)+1:1}function readerDownload(blob,filename){const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),setTimeout(()=>{URL.revokeObjectURL(a.href),a.remove()},1500)}const _readerDelay=ms=>new Promise(r=>setTimeout(r,ms));async function readerExport(mode){if(!readerState.sentences.length){toast("Import a document first","error");return}const indices=readerScopeIndices(),missing=indices.filter(i=>!readerState.blobCache.has(i));let failedCount=0;if(missing.length){toast("Synthesising "+missing.length+" missing sentence(s) before export\u2026","success");const{failed}=await readerSynthIndices(missing);if(readerState.synthCancel){toast("Export cancelled","error");return}failedCount=failed.length}const ready=indices.filter(i=>readerState.blobCache.has(i));if(!ready.length){toast("Nothing to export","error");return}if(failedCount){toast(failedCount+" sentence(s) failed to synthesise \u2014 fix them (see red markers) before exporting, or missing audio will silently drop from the file","error");return}const title=readerSafeName(readerState.title);if(mode==="sentence"){const perPage={};for(const i of ready){const pg=readerPageOf(i);perPage[pg]=(perPage[pg]||0)+1;const name=readerState.mode==="pdf"?`${title} - p${readerPad(pg)} - ${readerPad(perPage[pg])}.mp3`:`${title} - ${readerPad(perPage[pg])}.mp3`;readerDownload(readerState.blobCache.get(i),name),await _readerDelay(350)}toast("Exported "+ready.length+" MP3 files","success");return}const byPage=new Map;ready.forEach(i=>{const pg=readerPageOf(i);byPage.has(pg)||byPage.set(pg,[]),byPage.get(pg).push(i)});let files=0;for(const[pg,idxs]of[...byPage.entries()].sort((a,b)=>a[0]-b[0])){const blob=new Blob(idxs.map(i=>readerState.blobCache.get(i)),{type:"audio/mpeg"}),name=readerState.mode==="pdf"?`${title} - p${readerPad(pg)}.mp3`:`${title}.mp3`;readerDownload(blob,name),files++,await _readerDelay(400)}toast("Exported "+files+(readerState.mode==="pdf"?" page MP3 file(s)":" MP3"),"success")}function readerRechunk(mode){readerState.chunkMode=mode,readerState.baseSentences.length&&(readerStopPlayback(),readerState.blobCache.clear(),readerState.bufCache.clear(),readerState.gainCache.clear(),readerState.savedAudioIdx=new Set,readerState.pages.forEach(p=>p.overlay.querySelectorAll(".reader-stat").forEach(e=>e.remove())),readerClearSelection(),readerState.sentences=readerGroupUnits(readerState.baseSentences,mode),readerBuildUnitIndex(),readerState.pages.forEach((p,i)=>{p.rendered&&readerCreatePageStatus(i)}),readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),readerState.idx=0,readerUpdateScopeLabel(),readerUpdateProgress(),readerHighlightSentence(readerState.sentences[0]),toast("Voice consistency: "+mode+" \u2014 audio cleared, re-synthesise","success"))}function readerGenParams(){var _a2,_b2,_c2;const out={},seed=(_a2=$("reader-seed"))==null?void 0:_a2.value.trim(),temp=(_b2=$("reader-temp"))==null?void 0:_b2.value.trim(),nspd=(_c2=$("reader-tts-speed"))==null?void 0:_c2.value.trim();return seed!==""&&seed!=null&&!isNaN(+seed)&&(out.seed=parseInt(seed,10)),temp!==""&&temp!=null&&!isNaN(+temp)&&(out.temperature=parseFloat(temp)),nspd!==""&&nspd!=null&&!isNaN(+nspd)&&parseFloat(nspd)!==1&&(out.speed=parseFloat(nspd)),Object.keys(out).length?out:null}function readerComputeGain(idx,buf){if(readerState.gainCache.has(idx))return readerState.gainCache.get(idx);const ch=buf.getChannelData(0),step=Math.max(1,Math.floor(ch.length/8e3));let sum=0,n=0,peak=1e-6;for(let i=0;i<ch.length;i+=step){const v=ch[i];sum+=v*v,n++,Math.abs(v)>peak&&(peak=Math.abs(v))}let gain=.1/(Math.sqrt(sum/Math.max(1,n))||1e-4);return gain=Math.max(.5,Math.min(gain,4)),gain=Math.min(gain,.99/peak),readerState.gainCache.set(idx,gain),gain}async function readerFetchServerAudio(idx){if(!readerState.savedId||!readerState.savedAudioIdx.has(idx))return null;try{const r=await fetch(`${READER_API}/${readerState.savedId}/audio/${idx}`);if(r.ok){const b=await r.blob();return readerState.blobCache.set(idx,b),b}}catch{}return null}async function readerGetBuffer(idx){var _a2,_b2,_c2;if(readerState.bufCache.has(idx))return readerState.bufCache.get(idx);let blob=readerState.blobCache.get(idx)||await readerFetchServerAudio(idx);if(!blob){const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice)throw new Error("Pick a voice first");if(!backend)throw new Error("No TTS backend selected");const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.sentences[idx].status!=="reading"&&readerSetStatus(idx,"synth"),blob=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[idx].text),READER_FMT,instruct,backend,!1,readerGenParams()),readerState.blobCache.set(idx,blob),readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"ready")}const buf=await readerCtx().decodeAudioData(await blob.arrayBuffer());return readerState.bufCache.set(idx,buf),buf}function readerEvictBuffers(center){if(!(readerState.bufCache.size<=READER_BUF_WINDOW*2+1))for(const i of readerState.bufCache.keys())Math.abs(i-center)>READER_BUF_WINDOW&&readerState.bufCache.delete(i)}async function readerPrefetch(idx){var _a2,_b2,_c2;if(!(idx<0||idx>=readerState.sentences.length)){if(!readerState.blobCache.has(idx)&&!await readerFetchServerAudio(idx)){const voice=(_a2=$("reader-voice-select"))==null?void 0:_a2.value,backend=(_b2=$("reader-backend-select"))==null?void 0:_b2.value;if(!voice||!backend)return;const instruct=((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"";readerState.sentences[idx].status==="pending"&&readerSetStatus(idx,"synth");try{const b=await fetchTtsPreviewBlob(voice,_readerTextForSynth(readerState.sentences[idx].text),READER_FMT,instruct,backend,!1,readerGenParams());readerState.blobCache.set(idx,b),readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"ready")}catch{readerState.sentences[idx].status==="synth"&&readerSetStatus(idx,"pending");return}}if(!readerState.bufCache.has(idx)&&Math.abs(idx-readerState.idx)<=READER_BUF_WINDOW)try{readerState.bufCache.set(idx,await readerCtx().decodeAudioData(await readerState.blobCache.get(idx).arrayBuffer()))}catch{}}}function readerClearReading(){const i=readerState.readingIdx;i>=0&&i<readerState.sentences.length&&readerState.sentences[i].status==="reading"&&readerSetStatus(i,readerState.blobCache.has(i)?"ready":"pending"),readerState.readingIdx=-1}async function readerPlayCurrent(){if(!readerState.playing)return;if(readerState.idx>=readerState.sentences.length){readerStopPlayback(),readerState.idx=0,readerUpdateProgress();return}const idx=readerState.idx,sentence=readerState.sentences[idx];readerUpdateProgress(),readerSaveResume(),readerState.readingIdx=idx,readerSetStatus(idx,"reading"),readerHighlightSentence(sentence);let buf;try{buf=await readerGetBuffer(idx)}catch(e){toast(e.message||String(e),"error"),readerSetStatus(idx,"pending"),readerStopPlayback();return}if(!readerState.playing||readerState.idx!==idx)return;readerEvictBuffers(idx);const timings=computeWordTimings(sentence.text,buf.duration),ctx=readerCtx();readerStopSource();const src=ctx.createBufferSource();if(src.buffer=buf,src.playbackRate.value=readerState.speed,readerState.normalize){const g=ctx.createGain();g.gain.value=readerComputeGain(idx,buf),src.connect(g),g.connect(ctx.destination)}else src.connect(ctx.destination);readerState.currentSource=src;const t0=ctx.currentTime;readerPrefetch(idx+1),src.onended=()=>{readerState.currentSource===src&&(readerState.currentSource=null,readerState.raf&&(cancelAnimationFrame(readerState.raf),readerState.raf=null),readerSetStatus(idx,"ready"),readerState.readingIdx===idx&&(readerState.readingIdx=-1),readerState.playing&&(readerState.idx++,readerPlayCurrent()))},src.start(0);const tick=()=>{if(readerState.currentSource!==src)return;const elapsed=(ctx.currentTime-t0)*readerState.speed;let active=0;for(let i=timings.length-1;i>=0;i--)if(elapsed>=timings[i].start){active=i;break}readerHighlightWord(sentence,Math.min(active,sentence.words.length-1)),readerState.raf=requestAnimationFrame(tick)};readerState.raf=requestAnimationFrame(tick)}function readerStopSource(){if(readerState.currentSource){try{readerState.currentSource.onended=null,readerState.currentSource.stop(0)}catch{}readerState.currentSource=null}readerState.raf&&(cancelAnimationFrame(readerState.raf),readerState.raf=null)}function readerStopPlayback(){readerState.playing=!1,readerStopSource(),readerClearHighlights(),readerClearReading(),readerUpdatePlayBtn(),typeof window.setNavBusy=="function"&&window.setNavBusy("s-reader",!1)}function readerPlay(){var _a2;if(!readerState.sentences.length){toast("Import a document first","error");return}if(!((_a2=$("reader-voice-select"))!=null&&_a2.value)){toast("Pick a voice first","error");return}readerCtx(),readerState.playing=!0,readerUpdatePlayBtn(),typeof window.setNavBusy=="function"&&window.setNavBusy("s-reader",!0),readerPlayCurrent()}function readerPause(){readerState.playing=!1,readerStopSource(),readerSaveResume(),readerPersistProgress(),readerClearReading(),readerUpdatePlayBtn()}function readerJumpTo(idx){readerState.idx=Math.max(0,Math.min(idx,readerState.sentences.length-1)),readerStopSource(),readerClearHighlights(),readerClearReading(),readerUpdateProgress(),readerSaveResume(),readerState.playing?readerPlayCurrent():readerHighlightSentence(readerState.sentences[readerState.idx])}function readerUpdatePlayBtn(){const btn=$("reader-play");if(!btn)return;const ic=btn.querySelector(".mdi");ic&&(ic.className="mdi "+(readerState.playing?"mdi-pause":"mdi-play"))}function readerUpdateProgress(){const total=readerState.sentences.length,cur=total?readerState.idx+1:0,lbl=$("reader-progress-label");lbl&&(lbl.textContent=cur+" / "+total);const fill=$("reader-progress-fill");fill&&(fill.style.width=(total?readerState.idx/total*100:0)+"%")}function readerSaveResume(){if(!(!readerState.title||!readerState.sentences.length))try{localStorage.setItem(READER_RESUME_KEY,JSON.stringify({title:readerState.title,idx:readerState.idx,total:readerState.sentences.length}))}catch{}}function readerLoadResume(){try{const r=JSON.parse(localStorage.getItem(READER_RESUME_KEY)||"null");if(r&&r.title===readerState.title&&r.idx>0&&r.idx<readerState.sentences.length)return readerState.idx=r.idx,!0}catch{}return!1}const READER_API="/api/reader/docs";function readerSyntheticCount(){let n=0;for(let i=0;i<readerState.sentences.length;i++)readerState.blobCache.has(i)&&n++;return n}async function readerSaveLibrary(){var _a2,_b2,_c2,_d2,_e2,_f2;if(!readerState.sentences.length){toast("Import a document first","error");return}if(readerState.mode==="pdf"&&!readerState.fileBlob){toast("Original PDF unavailable to save","error");return}const meta={title:readerState.title||"Untitled",kind:readerState.mode,idx:readerState.idx,voice:((_a2=$("reader-voice-select"))==null?void 0:_a2.value)||"",backend:((_b2=$("reader-backend-select"))==null?void 0:_b2.value)||"",speed:readerState.speed,instruct:((_c2=$("reader-instruct"))==null?void 0:_c2.value.trim())||"",chunkMode:readerState.chunkMode,seed:((_d2=$("reader-seed"))==null?void 0:_d2.value.trim())||"",temperature:((_e2=$("reader-temp"))==null?void 0:_e2.value.trim())||"",tts_speed:parseFloat((_f2=$("reader-tts-speed"))==null?void 0:_f2.value)||1,normalize:readerState.normalize,sentenceCount:readerState.sentences.length,pageCount:readerState.pages.length,synthCount:readerSyntheticCount(),updated:new Date().toISOString()},btn=$("reader-save-lib");btn&&(btn.disabled=!0);try{readerState.savedId&&(meta.id=readerState.savedId);const r=await fetch(READER_API,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(meta)});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const fresh=!readerState.savedId;if(readerState.savedId=(await r.json()).id,fresh||!readerState.sourceUploaded){const ext=readerState.mode==="pdf"?"pdf":"txt",body=readerState.mode==="pdf"?readerState.fileBlob:new Blob([readerState.docText||""],{type:"text/plain"});(await fetch(`${READER_API}/${readerState.savedId}/source?ext=${ext}`,{method:"PUT",body})).ok&&(readerState.sourceUploaded=!0)}let uploaded=0;for(let i=0;i<readerState.sentences.length;i++)readerState.blobCache.has(i)&&!readerState.savedAudioIdx.has(i)&&(await fetch(`${READER_API}/${readerState.savedId}/audio/${i}`,{method:"PUT",body:readerState.blobCache.get(i)})).ok&&(readerState.savedAudioIdx.add(i),uploaded++);toast("Saved ("+meta.synthCount+" / "+meta.sentenceCount+" synthesised"+(uploaded?", +"+uploaded+" new":"")+")","success"),readerRenderLibrary()}catch(e){toast("Save failed: "+(e.message||e),"error")}finally{btn&&(btn.disabled=!1)}}async function readerPersistProgress(){if(readerState.savedId)try{await fetch(`${READER_API}/${readerState.savedId}/progress`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({idx:readerState.idx,updated:new Date().toISOString()})})}catch{}}async function readerDeleteLibraryDoc(id){try{await fetch(`${READER_API}/${id}`,{method:"DELETE"}),readerState.savedId===id&&(readerState.savedId=null,readerState.savedAudioIdx=new Set,readerState.sourceUploaded=!1),readerRenderLibrary()}catch(e){toast("Delete failed: "+(e.message||e),"error")}}async function readerOpenLibraryDoc(id){let rec;try{const r=await fetch(`${READER_API}/${id}`);if(!r.ok)throw new Error(r.statusText);rec=await r.json()}catch(e){toast("Open failed: "+(e.message||e),"error");return}readerResetDoc(),readerState.title=rec.title;const titleEl=$("reader-doc-title");if(titleEl&&(titleEl.textContent=rec.title),readerState.chunkMode=rec.chunkMode||"sentence",$("reader-chunk-mode")&&($("reader-chunk-mode").value=readerState.chunkMode),$("reader-seed")&&($("reader-seed").value=rec.seed||""),$("reader-temp")&&($("reader-temp").value=rec.temperature||""),$("reader-tts-speed")&&typeof rec.tts_speed=="number"&&($("reader-tts-speed").value=rec.tts_speed),readerState.normalize=rec.normalize!==!1,$("reader-normalize")&&($("reader-normalize").checked=readerState.normalize),rec.backend&&$("reader-backend-select")&&readerSetSelectValue("reader-backend-select",rec.backend),rec.voice&&$("reader-voice-select")&&readerSetSelectValue("reader-voice-select",rec.voice),typeof rec.speed=="number"){readerState.speed=rec.speed,$("reader-speed")&&($("reader-speed").value=rec.speed);const sl=$("reader-speed-label");sl&&(sl.textContent=rec.speed.toFixed(2).replace(/0$/,"")+"\xD7")}$("reader-instruct")&&($("reader-instruct").value=rec.instruct||"");let sourceBlob=null;try{const sr=await fetch(`${READER_API}/${id}/source`);sr.ok&&(sourceBlob=await sr.blob())}catch{}try{if(rec.kind==="pdf"){if(!sourceBlob){const inp=document.createElement("input");inp.type="file",inp.accept=".pdf",inp.onchange=async()=>{const file=inp.files[0];if(!file)return;if(!(await fetch(`${READER_API}/${id}/source?ext=pdf`,{method:"PUT",body:file})).ok){toast("Re-upload failed","error");return}toast("PDF restored \u2014 opening\u2026","success"),readerState.savedId=id,readerState.sourceUploaded=!0,readerState.fileBlob=file,await readerLoadPdf(file)},inp.click(),toast("PDF source missing \u2014 please re-select the original file","error");return}readerState.fileBlob=sourceBlob,await readerLoadPdf(sourceBlob)}else{const text=sourceBlob?await sourceBlob.text():rec.text||"";readerState.docText=text,await readerLoadText(text)}}catch(e){toast("Open failed: "+(e.message||e),"error");return}if(!readerState.sentences.length){toast("Document had no readable text","error");return}readerState.mode==="text"&&readerState.sentences.forEach((s,i)=>readerSetStatus(i,"pending")),readerState.savedId=id,readerState.sourceUploaded=!0,readerState.savedAudioIdx=new Set,rec.sentenceCount===readerState.sentences.length?(rec.audioIdx||[]).forEach(i=>{i<readerState.sentences.length&&(readerState.savedAudioIdx.add(i),readerSetStatus(i,"ready"))}):(rec.audioIdx||[]).length&&toast("Document changed \u2014 re-synthesis needed","error"),readerState.idx=Math.max(0,Math.min(rec.idx||0,readerState.sentences.length-1)),$("reader-transport").hidden=!1,$("reader-dropzone").hidden=!0;const importArea=$("reader-import-area");importArea&&(importArea.hidden=!0),readerUpdateToolbarVisibility(!0),$("reader-synthbar").hidden=!1;const pr=$("reader-page-range");if(pr&&(pr.hidden=readerState.mode!=="pdf",readerState.mode==="pdf")){const n=readerState.pages.length;$("reader-page-from")&&($("reader-page-from").max=n,$("reader-page-from").value=1),$("reader-page-to")&&($("reader-page-to").max=n,$("reader-page-to").value=n)}readerUpdateScopeLabel(),readerUpdateProgress(),readerHighlightSentence(readerState.sentences[readerState.idx]),toast('Opened "'+rec.title+'" \xB7 resuming at sentence '+(readerState.idx+1),"success"),typeof window.navReaderView=="function"?window.navReaderView("main"):showReaderView("main")}function readerSetSelectValue(id,value){const sel=$(id);if(sel){if(![...sel.options].some(o=>o.value===value)){const o=document.createElement("option");o.value=o.textContent=value,sel.appendChild(o)}sel.value=value,id==="reader-voice-select"&&window.VoicePicker&&VoicePicker.setValue(id,value)}}async function readerRenderLibrary(){const card=$("reader-library-card"),list=$("reader-lib-list");if(!card||!list)return;let all=[];try{const r=await fetch(READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='<div style="padding: 20px; text-align: center; color: var(--subtext);">No saved books yet.</div>';return}list.innerHTML=all.map(rec=>{const total=rec.sentenceCount||0,synth=rec.synthCount||0,readPct=total?Math.round((rec.idx||0)/total*100):0,synthPct=total?Math.round(synth/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"";let h=0;const titleStr=rec.title||"Untitled";for(let i=0;i<titleStr.length;i++)h=(h*31+titleStr.charCodeAt(i))%360;const h2=(h+40)%360,c1=`hsl(${h}, 55%, 42%)`,c2=`hsl(${h2}, 58%, 30%)`,coverUrl=`${READER_API}/${rec.id}/cover?t=${new Date(rec.updated||Date.now()).getTime()}`,bgStyle=rec.hasCover?`style="background-image: linear-gradient(to bottom, rgba(0,0,0,0.3) 0%, rgba(0,0,0,0.8) 100%), url('${coverUrl}'); background-size: cover; background-position: center; color: #fff;"`:`style="--bk1:${c1};--bk2:${c2}"`;return`<div class="reh-book reader-book" data-id="${rec.id}" title="${escHtml(rec.title)}" ${bgStyle}>
<div class="reh-book-actions">
<button class="reh-book-act reader-book-del" data-del="${rec.id}" title="Delete"><span class="mdi mdi-delete-outline"></span></button>
</div>
<div class="reh-book-title">${escHtml(rec.title)}</div>
<div class="reh-book-meta">
${total} sentences${rec.pageCount?" \xB7 "+rec.pageCount+" pg":""}
<div class="reh-book-progress" title="${synthPct}% audio"><div style="width:${synthPct}%"></div></div>
<div style="margin-top:4px;opacity:.8"><span class="mdi ${rec.kind==="pdf"?"mdi-file-pdf-box":"mdi-text-box-outline"}"></span> ${readPct}% read \xB7 ${date}</div>
</div>
</div>`}).join(""),list.querySelectorAll(".reader-book").forEach(el=>{el.addEventListener("click",e=>{e.target.closest(".reader-book-del")||readerOpenLibraryDoc(el.dataset.id)});const delBtn=el.querySelector(".reader-book-del");delBtn&&delBtn.addEventListener("click",e=>{if(e.stopPropagation(),el.querySelector(".reader-book-del-confirm"))return;const confirmOverlay=document.createElement("div");confirmOverlay.className="reader-book-del-confirm",confirmOverlay.style.cssText="position:absolute; inset:0; background:rgba(0,0,0,0.85); color:#fff; display:flex; flex-direction:column; justify-content:center; align-items:center; border-radius:inherit; z-index:10; padding:12px; text-align:center; box-sizing:border-box;",confirmOverlay.innerHTML=`
<div style="font-weight:600; margin-bottom:8px; font-size:14px;">Delete Book?</div>
<div style="font-size:11px; opacity:0.8; margin-bottom:12px; line-height:1.4;">Audio files will be removed.</div>
<div style="display:flex; gap:8px;">
<button class="btn-secondary btn-sm" id="btn-cancel-del" style="background:rgba(255,255,255,0.15); border:none; color:#fff; padding:6px 12px;">Cancel</button>
<button class="btn-primary btn-sm" id="btn-confirm-del" style="background:var(--red,#ef4444); border:none; color:#fff; padding:6px 12px;">Delete</button>
</div>
`,confirmOverlay.addEventListener("click",ce=>ce.stopPropagation()),confirmOverlay.querySelector("#btn-cancel-del").addEventListener("click",ce=>{ce.stopPropagation(),confirmOverlay.remove()}),confirmOverlay.querySelector("#btn-confirm-del").addEventListener("click",async ce=>{ce.stopPropagation(),confirmOverlay.innerHTML='<span class="mdi mdi-loading mdi-spin" style="font-size:24px;"></span>';try{if(!(await fetch(`${READER_API}/${el.dataset.id}`,{method:"DELETE"})).ok)throw new Error("Failed to delete book");toast("Book deleted","success"),readerRenderLibrary()}catch(err){toast(err.message,"error"),confirmOverlay.remove()}}),el.appendChild(confirmOverlay)}),el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.style.borderColor="var(--accent)")}),el.addEventListener("dragleave",()=>{el.style.borderColor=""}),el.addEventListener("drop",async e=>{var _a2;if(!(e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")))return;e.preventDefault(),e.stopPropagation(),el.style.borderColor="";const f=(_a2=e.dataTransfer.files)==null?void 0:_a2[0];if(!f||!f.type.startsWith("image/")){toast("Please drop an image file","error");return}try{const id=el.dataset.id,r=await fetch(`${READER_API}/${id}/cover`,{method:"PUT",body:f});if(!r.ok)throw new Error(r.statusText);await fetch(`${READER_API}/${id}/progress`,{method:"PATCH",headers:{"Content-Type":"application/json"},body:JSON.stringify({updated:new Date().toISOString()})}),toast("Cover image updated","success"),readerRenderLibrary()}catch(err){toast("Failed to upload cover: "+(err.message||err),"error")}})}),list.querySelectorAll(".reader-book-del").forEach(btn=>btn.addEventListener("click",e=>{e.stopPropagation(),confirm("Delete this saved document and its audio?")&&readerDeleteLibraryDoc(btn.dataset.del)}))}(_Kb=$("reader-file-input"))==null||_Kb.addEventListener("change",function(){var _a2;const f=(_a2=this.files)==null?void 0:_a2[0];this.value="",readerImportFile(f)}),(_Lb=$("reader-paste-load"))==null||_Lb.addEventListener("click",readerImportPastedText),(_Mb=$("reader-paste-text"))==null||_Mb.addEventListener("keydown",e=>{(e.ctrlKey||e.metaKey)&&e.key==="Enter"&&(e.preventDefault(),readerImportPastedText())}),(_Nb=$("reader-fetch-voices-btn"))==null||_Nb.addEventListener("click",readerFetchVoices),(_Ob=$("reader-backend-select"))==null||_Ob.addEventListener("change",()=>{const sel=$("reader-voice-select");sel&&(sel.innerHTML='<option value="">\u2014 fetch voices \u2014</option>'),window.VoicePicker&&(VoicePicker.upgrade("reader-voice-select"),VoicePicker.populate("reader-voice-select",[])),readerUpdateBackendHint()}),(_Pb=$("reader-chunk-mode"))==null||_Pb.addEventListener("change",function(){readerRechunk(this.value)}),(_Qb=$("reader-normalize"))==null||_Qb.addEventListener("change",function(){readerState.normalize=this.checked}),(_Rb=$("reader-play"))==null||_Rb.addEventListener("click",()=>{readerState.playing?readerPause():readerPlay()}),(_Sb=$("reader-stop"))==null||_Sb.addEventListener("click",()=>{readerStopPlayback(),readerState.idx=0,readerSaveResume(),readerUpdateProgress()}),(_Tb=$("reader-prev"))==null||_Tb.addEventListener("click",()=>readerJumpTo(readerState.idx-1)),(_Ub=$("reader-next"))==null||_Ub.addEventListener("click",()=>readerJumpTo(readerState.idx+1)),(_Vb=$("reader-speed"))==null||_Vb.addEventListener("input",function(){readerState.speed=parseFloat(this.value)||1;const lbl=$("reader-speed-label");lbl&&(lbl.textContent=readerState.speed.toFixed(2).replace(/0$/,"")+"\xD7"),readerState.currentSource&&(readerState.currentSource.playbackRate.value=readerState.speed)}),(_Wb=$("reader-zoom-fitw"))==null||_Wb.addEventListener("click",()=>readerApplyZoom("fit-width")),(_Xb=$("reader-zoom-fith"))==null||_Xb.addEventListener("click",()=>readerApplyZoom("fit-height")),(_Yb=$("reader-zoom-two"))==null||_Yb.addEventListener("click",()=>readerApplyZoom("two")),(_Zb=$("reader-zoom-in"))==null||_Zb.addEventListener("click",()=>{readerState.scale=Math.min(4,readerState.scale*1.2),readerApplyZoom("custom")}),(__b=$("reader-zoom-out"))==null||__b.addEventListener("click",()=>{readerState.scale=Math.max(.2,readerState.scale/1.2),readerApplyZoom("custom")});let _readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,_readerSearchTimer=null;const READER_SEARCH_MAX_HITS=2500;function readerSearchTextMap(words){var _a2;let text="";const spans=[];for(let i=0;i<words.length;i++){const raw=String(((_a2=words[i])==null?void 0:_a2.text)||"").trim();if(!raw)continue;text&&(text+=" ");const start=text.length;text+=raw.toLowerCase(),spans.push({wordIndex:i,start,end:text.length})}return{text,spans}}function readerFindSearchGroups(sentence,term){const words=(sentence==null?void 0:sentence.words)||[],needle=String(term||"").trim().replace(/\s+/g," ").toLowerCase();if(!needle)return[];const mapped=readerSearchTextMap(words),groups=[];let pos=0;for(;(pos=mapped.text.indexOf(needle,pos))!==-1;){const end=pos+needle.length,idxs=mapped.spans.filter(sp=>sp.end>pos&&sp.start<end).map(sp=>sp.wordIndex);idxs.length&&groups.push(idxs),pos=Math.max(pos+1,end)}return groups}function readerRemoveSearchHighlights(){document.querySelectorAll(".reader-search-hit").forEach(el=>el.remove()),document.querySelectorAll(".reader-word.search-hit, .reader-word.search-current").forEach(el=>{el.classList.remove("search-hit","search-current")})}function readerSetSearchStatus(text){const st=$("reader-search-status");st&&(st.textContent=text||"")}function readerClearSearch({clearInput=!1}={}){if(_readerSearchTerm="",_readerSearchHits=[],_readerSearchCurrent=-1,readerRemoveSearchHighlights(),readerSetSearchStatus(""),clearInput){const inp=$("reader-search");inp&&(inp.value="")}}function readerCollectSearchHits(term){var _a2,_b2,_c2;const hits=[],sents=readerState.sentences||[];for(let si=0;si<sents.length;si++){const s=sents[si],groups=readerFindSearchGroups(s,term);for(const wordIndexes of groups){const page=(_c2=(_b2=(_a2=s.words)==null?void 0:_a2[wordIndexes[0]])==null?void 0:_b2.page)!=null?_c2:0;if(hits.push({sentenceIndex:si,page,wordIndexes,nodes:[]}),hits.length>=READER_SEARCH_MAX_HITS)return hits}}return hits}function readerPaintSearchHighlights(){if(readerRemoveSearchHighlights(),!_readerSearchTerm||!_readerSearchHits.length)return;const scale=readerState.scale||1;_readerSearchHits.forEach((hit,hi)=>{var _a2;const s=readerState.sentences[hit.sentenceIndex];if(s){hit.nodes=[];for(const wi of hit.wordIndexes){const w=(_a2=s.words)==null?void 0:_a2[wi];if(w)if(readerState.mode==="pdf"){const pg=readerState.pages[w.page];if(!(pg!=null&&pg.overlay))continue;const el=document.createElement("div");el.className="reader-search-hit"+(hi===_readerSearchCurrent?" search-current":""),Object.assign(el.style,{left:w.x*scale+"px",top:w.top*scale+"px",width:Math.max(w.w,2)*scale+"px",height:Math.max(w.h,2)*scale+"px"}),pg.overlay.appendChild(el),hit.nodes.push(el)}else w.el&&(w.el.classList.add("search-hit"),hi===_readerSearchCurrent&&w.el.classList.add("search-current"),hit.nodes.push(w.el))}}})}function readerUpdateSearchStatus(){var _a2,_b2;if(!_readerSearchTerm){readerSetSearchStatus("");return}if(!((_a2=readerState.sentences)!=null&&_a2.length)){readerSetSearchStatus("No text loaded");return}if(!_readerSearchHits.length){readerSetSearchStatus("0/0");return}const current=_readerSearchCurrent>=0?_readerSearchCurrent+1:1,hit=_readerSearchHits[Math.max(0,_readerSearchCurrent)],capped=_readerSearchHits.length>=READER_SEARCH_MAX_HITS?"+":"",page=readerState.mode==="pdf"?" \xB7 p."+(((_b2=hit==null?void 0:hit.page)!=null?_b2:0)+1):"";readerSetSearchStatus(current+"/"+_readerSearchHits.length+capped+page)}function readerJumpToSearchHit(idx){var _a2;if(!_readerSearchHits.length)return;_readerSearchCurrent=(idx+_readerSearchHits.length)%_readerSearchHits.length,readerPaintSearchHighlights(),readerUpdateSearchStatus();const hit=_readerSearchHits[_readerSearchCurrent],node=(_a2=hit==null?void 0:hit.nodes)==null?void 0:_a2[0];if(node)readerEnsureVisible(node);else if(readerState.mode==="pdf"){const pg=readerState.pages[hit.page];pg!=null&&pg.pageDiv&&(readerRenderPage(hit.page),pg.pageDiv.scrollIntoView({behavior:"smooth",block:"center"}))}}function readerSearchApply(term,{jump=!0}={}){if(term=String(term||"").trim(),!term){readerClearSearch();return}_readerSearchTerm=term,_readerSearchHits=readerCollectSearchHits(term),_readerSearchCurrent=_readerSearchHits.length?0:-1,readerPaintSearchHighlights(),readerUpdateSearchStatus(),jump&&_readerSearchHits.length&&readerJumpToSearchHit(0)}function readerSearchStep(direction){var _a2;const term=((_a2=$("reader-search"))==null?void 0:_a2.value.trim())||_readerSearchTerm;if(!_readerSearchHits.length||term!==_readerSearchTerm){readerSearchApply(term,{jump:!0});return}readerJumpToSearchHit(_readerSearchCurrent+(direction===-1?-1:1))}(_$b=$("reader-search"))==null||_$b.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),readerSearchStep(e.shiftKey?-1:1)),e.key==="Escape"&&readerClearSearch({clearInput:!0})}),(_ac=$("reader-search"))==null||_ac.addEventListener("input",e=>{clearTimeout(_readerSearchTimer);const term=e.target.value.trim();if(!term){readerClearSearch();return}_readerSearchTimer=setTimeout(()=>readerSearchApply(term,{jump:!0}),160)}),(_bc=$("reader-search-prev"))==null||_bc.addEventListener("click",()=>readerSearchStep(-1)),(_cc=$("reader-search-next"))==null||_cc.addEventListener("click",()=>readerSearchStep(1)),(_dc=$("reader-search-clear"))==null||_dc.addEventListener("click",()=>readerClearSearch({clearInput:!0})),(_ec=$("reader-save-lib"))==null||_ec.addEventListener("click",readerSaveLibrary),(_fc=$("reader-export-btn"))==null||_fc.addEventListener("click",()=>{var _a2;return readerExport(((_a2=$("reader-export-mode"))==null?void 0:_a2.value)||"page")}),(_gc=$("reader-synth-all"))==null||_gc.addEventListener("click",readerSynthAll),(_hc=$("reader-synth-cancel"))==null||_hc.addEventListener("click",()=>{readerState.synthCancel=!0}),(_ic=$("reader-select-toggle"))==null||_ic.addEventListener("click",()=>{readerState.selecting?readerSetSelecting(!1):(readerClearSelection(),readerSetSelecting(!0))}),(_jc=$("reader-sel-clear"))==null||_jc.addEventListener("click",readerClearSelection),document.addEventListener("keydown",e=>{e.key==="Escape"&&readerState.selecting&&readerSetSelecting(!1)}),(_kc=$("reader-page-from"))==null||_kc.addEventListener("change",readerUpdateScopeLabel),(_lc=$("reader-page-to"))==null||_lc.addEventListener("change",readerUpdateScopeLabel),(_mc=$("reader-doc"))==null||_mc.addEventListener("click",e=>{if(!readerState.selecting||readerState.mode!=="pdf")return;const stat=e.target.closest(".reader-stat");if(!stat)return;const si=parseInt(stat.dataset.si);isNaN(si)||readerPickSentence(si)}),(_nc=$("reader-doc"))==null||_nc.addEventListener("scroll",function(){let t=null;return()=>{t||(t=setTimeout(()=>{t=null,readerRenderVisible()},120))}}()),window.addEventListener("resize",function(){let t=null;return()=>{readerState.mode!=="pdf"||readerState.zoomMode==="custom"||(clearTimeout(t),t=setTimeout(()=>readerApplyZoom(readerState.zoomMode),200))}}()),function(){["reader-dropzone","reader-doc"].map(id=>$(id)).filter(Boolean).forEach(el=>{el.addEventListener("dragover",e=>{e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.add("dragover"))}),el.addEventListener("dragleave",()=>el.classList.remove("dragover")),el.addEventListener("drop",e=>{var _a2;e.dataTransfer&&[...e.dataTransfer.types||[]].includes("Files")&&(e.preventDefault(),el.classList.remove("dragover"),readerImportFile((_a2=e.dataTransfer.files)==null?void 0:_a2[0]))})})}(),window.showReaderView=function(view){const main=$("reader-main-view"),cast=$("reader-audiobook-panel"),chars=$("reader-charsheets-panel"),lib=$("reader-library-card");main&&(main.hidden=view!=="main"),cast&&(cast.hidden=view!=="cast",view!=="cast"&&(cast.style.display="",cast.style.flexDirection="",cast.style.minHeight=""),view==="cast"&&!cast.innerHTML.trim()&&(cast.innerHTML=`<div style="padding:60px 20px; text-align:center; color:var(--subtext);"><span class="mdi mdi-drama-masks" style="font-size:48px; opacity:0.5; margin-bottom:16px; display:block;"></span><h3 style="margin:0 0 8px 0; color:var(--text);">No active casting session</h3><p style="margin:0;">Open a document in the Reader tab and click "Cast as audiobook" to start extracting characters.</p><br><button class="btn-primary" onclick="navReaderView('main')">Go to Reader</button></div>`,cast.className="ab-castpanel-inline card")),chars&&(chars.hidden=view!=="chars",view==="chars"&&!chars.innerHTML.trim()&&(chars.innerHTML=`<div class="card" style="padding:24px; min-height:260px; display:flex; flex-direction:column; gap:12px; justify-content:center;"><div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Character sheets</div><p class="card-subtitle" style="margin:0;">This stage will show the live passage-by-passage extraction view, prompt editor, and generated sheets once you start a cast.</p><div class="btn-row" style="margin-top:4px;"><button class="btn-primary btn-sm" onclick="if (typeof csForReader === 'function') csForReader();"><span class="mdi mdi-account-details-outline"></span> Cast from Reader</button><button class="btn-secondary btn-sm" onclick="if (typeof csForRehearser === 'function') csForRehearser();"><span class="mdi mdi-theater"></span> Cast from Rehearser</button><button class="btn-secondary btn-sm" onclick="if (typeof navLibraryView === 'function') navLibraryView('characters');"><span class="mdi mdi-folder-account-outline"></span> Go to Cast</button></div></div>`,chars.className="")),lib&&(lib.hidden=view!=="library",view==="library"&&(lib.classList.remove("card"),lib.style.boxShadow="none",lib.style.border="none",lib.style.background="transparent",readerRenderLibrary())),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs(view==="cast"?"cast":view==="chars"?"chars":"source")},window.navReaderView=function(view){window.showReaderView(view)},window.readerJumpToPage=function(pageNum){const pg=parseInt(pageNum,10);if(!pg)return Promise.resolve(!1);typeof navTo=="function"&&navTo("s-reader"),window.showReaderView("main");let tries=0;const waitFrame=()=>new Promise(resolve=>requestAnimationFrame(resolve)),jump=async()=>{var _a2;await waitFrame();const pageState=(_a2=readerState.pages)==null?void 0:_a2[pg-1],pageDiv=pageState==null?void 0:pageState.pageDiv;return pageDiv&&!pageDiv.hidden?(await readerRenderPage(pg-1),pageDiv.scrollIntoView({behavior:"smooth",block:"start",inline:"nearest"}),readerRenderVisible(),!0):tries++<20?(await new Promise(resolve=>setTimeout(resolve,100)),jump()):(typeof toast=="function"&&toast("Source page is not loaded in Reader yet","info"),!1)};return jump()},window.readerOnShow=async function(){const sel=$("reader-backend-select");if(sel&&(!sel.value||sel.options.length<=1)){if(typeof availableTtsBackends=="function"&&!availableTtsBackends().length&&typeof refreshTtsBackendAvailability=="function")try{await refreshTtsBackendAvailability()}catch{}typeof availableTtsBackends=="function"&&availableTtsBackends().length&&(sel.innerHTML=ttsBackendOptions(sel.value),sel.disabled=!1)}readerUpdateBackendHint(),window.VoicePicker&&VoicePicker.upgrade("reader-voice-select"),readerRenderLibrary(),window._readerStartView&&(window.showReaderView(window._readerStartView),window._readerStartView=null),_readerEnsureImportVisible()};function _readerEnsureImportVisible(){if(readerState.sentences.length)return;const card=document.getElementById("reader-config-card"),body=card?card.querySelector(".card-col-body"):null;body&&body.hidden&&(body.hidden=!1,card.classList.remove("card-col-closed"))}const AUDIOBOOK_CHUNK_CHARS=3e3,AUDIOBOOK_WARMUP_TIMEOUT_MS=24e4,AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS=6e5,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS=3e5,AUDIOBOOK_RECAST_TIMEOUT_MS=24e4,AUDIOBOOK_RECAST_CONTEXT_CHARS=4200,AUDIOBOOK_RECAST_TARGETS_PER_CALL=6,AUDIOBOOK_DRAFT_AUTOSAVE_MS=5*60*1e3,_audiobook={running:!1,cancel:!1};window._audiobook=_audiobook;let _abDraftAutosaveTimer=null,_abLastServerDraftErrorAt=0;async function audiobookFetchWithTimeout(url,options={},timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const parentSignal=options.signal,ac=new AbortController;let timedOut=!1;const timer=setTimeout(()=>{timedOut=!0,ac.abort()},timeoutMs),onParentAbort=()=>ac.abort(parentSignal==null?void 0:parentSignal.reason);parentSignal&&(parentSignal.aborted?onParentAbort():parentSignal.addEventListener("abort",onParentAbort,{once:!0}));try{return await fetch(url,{...options,signal:ac.signal})}catch(err){if(timedOut){const timeoutErr=new Error(`Timed out after ${Math.ceil(timeoutMs/1e3)}s`);throw timeoutErr.name="TimeoutError",timeoutErr}throw err}finally{clearTimeout(timer),parentSignal&&parentSignal.removeEventListener("abort",onParentAbort)}}function audiobookTimeoutSeconds(timeoutMs){return Math.max(5,Math.round(timeoutMs/1e3))}async function audiobookAttributeStream(body,view,outerSignal,idleTimeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/attribute-dialogue/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify({...body,want_reasoning:!0})});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(`
`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&(view!=null&&view.thinking)&&view.thinking(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}catch(err){throw(err==null?void 0:err.name)==="AbortError"&&!(outerSignal&&outerSignal.aborted)?new Error("stream idle timeout"):err}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}function audiobookDialogueKey(text){return String(text||"").normalize("NFKC").replace(/[»«„“”"'`]+/g,"").replace(/\s+/g," ").trim().toLowerCase()}function audiobookSegmentText(seg){return seg?seg.type==="narration"||!seg.speaker||/^Unknown|Unbekannt/i.test(seg.speaker)?seg.text||"":`"${seg.text||""}"`:""}function _abWordRangeAtPoint(x,y){let node=null,offset=0;if(document.caretRangeFromPoint){const r=document.caretRangeFromPoint(x,y);if(!r)return null;node=r.startContainer,offset=r.startOffset}else if(document.caretPositionFromPoint){const p=document.caretPositionFromPoint(x,y);if(!p)return null;node=p.offsetNode,offset=p.offset}else return null;if(!node||node.nodeType!==3)return null;const text=node.nodeValue||"",isW=ch=>ch!=null&&/[\p{L}\p{N}'-]/u.test(ch);if(offset>=text.length&&(offset=text.length-1),!isW(text[offset]))if(offset>0&&isW(text[offset-1]))offset--;else return null;let a=offset,b=offset;for(;a>0&&isW(text[a-1]);)a--;for(;b+1<text.length&&isW(text[b+1]);)b++;const word=text.slice(a,b+1).trim();if(!word||word.length<2)return null;const range=document.createRange();return range.setStart(node,a),range.setEnd(node,b+1),{word,range}}function audiobookJoinSegmentText(a,b){const left=String(a||""),right=String(b||"");return left?right?/\s$/.test(left)||/^\s/.test(right)||/^[,.;:!?»«”"')\]]/.test(right)||/[([{„“"']$/.test(left)?left+right:left+" "+right:left:right}const _AB_INQUIT_VERBS="(?:sagte|rief|fragte|antwortete|erwiderte|murmelte|fl[\xFCu]sterte|br[\xFCu]llte|entgegnete|meinte|knurrte|zischte|stammelte|schrie|dr[\xE4a]ngte|herrschte|seufzte|wisperte|grunzte|befahl|erkl[\xE4a]rte|fuhr fort|setzte hinzu|f[\xFCu]gte hinzu)";function _abSuspectNarration(segs,minScore){var _a2;minScore=minScore||3;const norm=i=>{var _a3;return String(((_a3=segs[i])==null?void 0:_a3.text)||"").replace(/\s+/g," ").trim()},out=new Set;for(let i=0;i<segs.length;i++){if(((_a2=segs[i])==null?void 0:_a2.type)!=="narration")continue;const t=norm(i),nxt=i+1<segs.length?norm(i+1):"";let score=0;new RegExp(`^[\\s,\xBB\xAB"'\\-\u2013]{0,4}(?:\\w+\\s+){0,2}`+_AB_INQUIT_VERBS+"\\b","i").test(nxt)&&(score+=5),(t.includes("\xBB")||t.includes("\xAB"))&&(score+=4);const p12=/\b(ich|du|wir|ihr|mein|dein|dich|euch)\b/i.test(t),excl=/[!?]\s*$/.test(t),imper=/^(Bringt|Los|Mach|Kommt|Geht|Nehmt|Haltet|Schafft|T[öo]tet|Sieh|Hört|Wartet)\b/.test(t);p12&&(score+=1),excl&&(score+=1),imper&&(score+=3),p12&&excl&&(score+=1),t.length<160&&(p12||excl||imper)&&(score+=1),score>=minScore&&out.add(i)}return out}window._abSuspectNarration=_abSuspectNarration;function audiobookRecastGroups(unknownIdxs,segs){const groups=[];let cur=[];for(const idx of unknownIdxs){const prev=cur.length?cur[cur.length-1]:-1/0,shortGap=idx<=prev+2&&segs.slice(prev+1,idx).every(s=>!s||s.type==="narration"||/^Unknown|Unbekannt/i.test(s.speaker||""));!cur.length||shortGap&&cur.length<AUDIOBOOK_RECAST_TARGETS_PER_CALL?cur.push(idx):(groups.push(cur),cur=[idx])}return cur.length&&groups.push(cur),groups}function audiobookRecastContext(segs,group){const targetStart=group[0],targetEnd=group[group.length-1]+1;let start=Math.max(0,targetStart-5),end=Math.min(segs.length,targetEnd+4);const render=()=>segs.slice(start,end).map(audiobookSegmentText).join(" ").trim();let text=render();for(;text.length>AUDIOBOOK_RECAST_CONTEXT_CHARS&&(start<targetStart||end>targetEnd)&&(end>targetEnd&&end--,text=render(),!(text.length<=AUDIOBOOK_RECAST_CONTEXT_CHARS));)start<targetStart&&start++,text=render();const recent=audiobookRecastRecent(segs,start,end);return{text,start,end,recent}}function audiobookRecastRecent(segs,start,end){const lines=[];for(let i=Math.max(0,start-10);i<Math.min(segs.length,end+8);i++){const s=segs[i];!s||s.type!=="dialogue"||!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)||lines.push(`${i<start?"before":i>=end?"after":"near"}: ${s.speaker}: ${(s.text||"").slice(0,100)}`)}return lines.slice(-12).join(`
`)}function audiobookFindReturnedSegment(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;let loose=null;for(let i=0;i<returned.length;i++){if(used.has(i))continue;const cand=returned[i];if(!cand)continue;const candKey=audiobookDialogueKey(cand.text);if(!candKey)continue;if(candKey===targetKey)return{idx:i,seg:cand};Math.min(candKey.length,targetKey.length)>=18&&(candKey.includes(targetKey)||targetKey.includes(candKey))&&(loose=loose||{idx:i,seg:cand})}return loose}function audiobookFindReturnedSegmentSequence(targetSeg,returned,used){const targetKey=audiobookDialogueKey(targetSeg==null?void 0:targetSeg.text);if(!targetKey)return null;for(let i=0;i<returned.length;i++){if(used.has(i)||!returned[i])continue;let joined="";const idxs=[];for(let j=i;j<returned.length&&!(used.has(j)||!returned[j]);j++){joined=audiobookJoinSegmentText(joined,returned[j].text||""),idxs.push(j);const key=audiobookDialogueKey(joined);if(key===targetKey)return{idxs,segs:idxs.map(k=>returned[k])};if(key.length>targetKey.length*1.35+40)break}}return null}function _abStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):String(v)}const _AB_DRAFT_KEY="ttsvc_ab_draft";function _abBookId(){return window.readerState&&readerState.savedId||_audiobook.bookId||null}function _abDraftKey(bookId){return bookId?_AB_DRAFT_KEY+"_"+bookId:_AB_DRAFT_KEY}function _abTextId(text){const s=(text.slice(0,300)+text.slice(-300)).replace(/\s+/g,"");let h=5381;for(let i=0;i<s.length;i++)h=((h<<5)+h^s.charCodeAt(i))>>>0;return h.toString(36)+"_"+text.length}function _abSafeFilename(name,fallback="cast"){return(String(name||fallback||"cast").replace(/[\\/:*?"<>|]+/g,"_").replace(/\s+/g,"_").replace(/^_+|_+$/g,"")||fallback||"cast").slice(0,120)}function _abCastMarkdown(data){const payload=data||{},segments=Array.isArray(payload.segments)?payload.segments:[],speakers=[...new Set(segments.filter(s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker).map(s=>String(s.speaker)))].sort(),lines=[`# ${payload.title||"Cast Script"}`,""],metaBits=[];payload.savedAt&&metaBits.push("exported "+new Date(payload.savedAt).toISOString().slice(0,16).replace("T"," ")),metaBits.push(`${speakers.length} character${speakers.length!==1?"s":""}`),metaBits.push(`${segments.length} segment${segments.length!==1?"s":""}`),lines.push(`*${metaBits.join(" \xB7 ")}*`,""),speakers.length&&lines.push(`**Characters:** ${speakers.join(", ")}`,"");let lastPage=null;for(const seg of segments){const text=String((seg==null?void 0:seg.text)||"").trim();if(text){if(seg.page!=null&&seg.page!==lastPage&&(lines.push("---","",`## Page ${seg.page}`,""),lastPage=seg.page),seg.type==="dialogue"){const speaker=String(seg.speaker||"Narrator"),tag=seg.emotion?` *(${seg.emotion})*`:"";lines.push(`**${speaker.toUpperCase()}**${tag}: "${text}"`)}else lines.push(text);lines.push("")}}return lines.join(`
`)}function _abStampSegmentPages(segs,pageMarks,text){if(!Array.isArray(segs)||!segs.length||segs.some(s=>s&&s.page!=null))return;const marks=(pageMarks||[]).slice().sort((a,b)=>(a.offset||0)-(b.offset||0)),src=text||"";if(!marks.length||!src)return;let markIdx=0,searchPos=0,cur=null;marks[0].offset<=2&&(cur=marks[0].page+1,markIdx=1);for(const s of segs){const probe=String(s.text||"").trim().slice(0,24),at=probe?src.indexOf(probe,searchPos):-1,pos=at>=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx<marks.length&&pos>=marks[markIdx].offset;)cur=marks[markIdx].page+1,markIdx++;cur!=null&&(s.page=cur)}}let _abLastDraftSaveAt=0;const AB_DRAFT_SAVE_THROTTLE_MS=4e3;function _abSaveDraft(segs,roster,text,done,total){var _a2;const isFinal=total>0&&done>=total,now=Date.now();if(!isFinal&&now-_abLastDraftSaveAt<AB_DRAFT_SAVE_THROTTLE_MS)return;_abLastDraftSaveAt=now;const bookId=_abBookId(),payload={bookId,title:((_a2=window.readerState)==null?void 0:_a2.title)||"",textId:_abTextId(text),segments:segs,roster,pageMarks:_audiobook.pageMarks||[],rehId:_audiobook.rehId||null,done,total,savedAt:Date.now()};try{localStorage.setItem(_abDraftKey(bookId),JSON.stringify(payload))}catch{}bookId&&fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)}).then(r=>{if(!r.ok)throw new Error(r.statusText||`HTTP ${r.status}`)}).catch(err=>{console.warn("[audiobook] server draft autosave failed:",err);const now2=Date.now();now2-_abLastServerDraftErrorAt>6e4&&(_abLastServerDraftErrorAt=now2,typeof toast=="function"&&toast("Server autosave failed. Browser draft is still saved.","error"))})}let _abExportBusy=!1;async function audiobookExportCastMd(){var _a2;if(_abExportBusy)return;_abExportBusy=!0,setTimeout(()=>{_abExportBusy=!1},2e3);const segs=_audiobook.segments||[];if(!segs.length){toast("No cast to export","error");return}const bookId=_abBookId();_audiobook.lastText&&_abSaveDraft(segs,_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0);const title=((_a2=window.readerState)==null?void 0:_a2.title)||_audiobook.title||"audiobook";if(bookId)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast/export`);if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const blob2=await r.blob(),match=(r.headers.get("Content-Disposition")||"").match(/filename="([^"]+)"/i),name=(match==null?void 0:match[1])||`${_abSafeFilename(title)}_cast.zip`;if(typeof readerDownload=="function")readerDownload(blob2,name);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob2),a.download=name,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}toast("Exported cast + character sheets (.zip)","success");return}catch{toast("Server export failed, downloading local cast copy","error")}const payload={bookId,title,textId:_audiobook.lastText?_abTextId(_audiobook.lastText):"",segments:segs,roster:_audiobook.roster||[],pageMarks:_audiobook.pageMarks||[],rehId:_audiobook.rehId||null,done:_audiobook.completedChunks||0,total:_audiobook.completedTotal||0,savedAt:Date.now()},blob=new Blob([_abCastMarkdown(payload)],{type:"text/markdown;charset=utf-8"});if(typeof readerDownload=="function")readerDownload(blob,`${_abSafeFilename(title)}_cast.md`);else{const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=`${_abSafeFilename(title)}_cast.md`,a.click(),setTimeout(()=>URL.revokeObjectURL(a.href),500)}}function _abStartDraftAutosave(saveNow){if(_abStopDraftAutosave(),typeof saveNow!="function")return;_abDraftAutosaveTimer=setInterval(saveNow,AUDIOBOOK_DRAFT_AUTOSAVE_MS);const saveOnLeave=()=>saveNow();_audiobook._draftSaveOnLeave=saveOnLeave,window.addEventListener("pagehide",saveOnLeave),window.addEventListener("beforeunload",saveOnLeave)}function _abStopDraftAutosave(){_abDraftAutosaveTimer&&clearInterval(_abDraftAutosaveTimer),_abDraftAutosaveTimer=null,_audiobook._draftSaveOnLeave&&(window.removeEventListener("pagehide",_audiobook._draftSaveOnLeave),window.removeEventListener("beforeunload",_audiobook._draftSaveOnLeave),_audiobook._draftSaveOnLeave=null)}async function _abEnsureLibraryBook(){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2;const rs=window.readerState;if(!rs||rs.savedId||!((_a2=rs.sentences)!=null&&_a2.length))return!!(rs!=null&&rs.savedId);if(rs.mode==="pdf"&&!rs.fileBlob)return!1;const meta={title:rs.title||"Untitled",kind:rs.mode,idx:rs.idx,voice:((_b2=$("reader-voice-select"))==null?void 0:_b2.value)||"",backend:((_c2=$("reader-backend-select"))==null?void 0:_c2.value)||"",speed:rs.speed,instruct:((_d2=$("reader-instruct"))==null?void 0:_d2.value.trim())||"",chunkMode:rs.chunkMode,seed:((_e2=$("reader-seed"))==null?void 0:_e2.value.trim())||"",temperature:((_f2=$("reader-temp"))==null?void 0:_f2.value.trim())||"",tts_speed:parseFloat((_g2=$("reader-tts-speed"))==null?void 0:_g2.value)||1,normalize:rs.normalize,sentenceCount:rs.sentences.length,pageCount:((_h2=rs.pages)==null?void 0:_h2.length)||0,synthCount:0,updated:new Date().toISOString()};try{const r=await fetch("/api/reader/docs",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(meta)});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);rs.savedId=(await r.json()).id,_audiobook.bookId=rs.savedId;const ext=rs.mode==="pdf"?"pdf":"txt",body=rs.mode==="pdf"?rs.fileBlob:new Blob([rs.docText||audiobookScopeText()||""],{type:"text/plain"});return(await fetch(`/api/reader/docs/${encodeURIComponent(rs.savedId)}/source?ext=${ext}`,{method:"PUT",body})).ok&&(rs.sourceUploaded=!0),typeof readerRenderLibrary=="function"&&readerRenderLibrary(),!0}catch(err){return console.warn("[audiobook] could not create reader-library autosave target:",err),!1}}async function _abLoadDraftServer(bookId){if(!bookId)return null;for(let attempt=0;attempt<2;attempt++)try{const r=await fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`);if(!r.ok){if(attempt===0&&(r.status===404||r.status>=500)){await new Promise(resolve=>setTimeout(resolve,650));continue}return null}const d=await r.json();return d&&Array.isArray(d.segments)&&d.segments.length?d:null}catch{attempt===0&&await new Promise(resolve=>setTimeout(resolve,650))}return null}function _abLoadDraft(text){var _a2;const bookId=_abBookId(),textId=_abTextId(text),title=String(((_a2=window.readerState)==null?void 0:_a2.title)||"").trim().toLowerCase();try{if(bookId){const raw2=localStorage.getItem(_abDraftKey(bookId));if(raw2){const d=JSON.parse(raw2);if(d&&Array.isArray(d.segments)&&d.segments.length)return d}}const raw=localStorage.getItem(_AB_DRAFT_KEY);if(raw){const d=JSON.parse(raw);if(d&&Array.isArray(d.segments)&&d.segments.length&&d.textId===textId)return d}let best=null;for(let i=0;i<localStorage.length;i++){const key=localStorage.key(i);if(!(!key||!key.startsWith(_AB_DRAFT_KEY+"_"))&&!(bookId&&key===_abDraftKey(bookId)))try{const cand=JSON.parse(localStorage.getItem(key)||"null");if(!cand||!Array.isArray(cand.segments)||!cand.segments.length)continue;const candTitle=String(cand.title||"").trim().toLowerCase(),exactText=cand.textId&&cand.textId===textId,sameTitle=title&&candTitle&&candTitle===title;if(!exactText&&!sameTitle)continue;const score=(exactText?1e6:0)+(sameTitle?1e5:0)+cand.segments.length;(!best||score>best.score)&&(best={score,draft:cand})}catch{}}if(best!=null&&best.draft)return best.draft}catch{return null}return null}function _abClearDraft(){const bookId=_abBookId();try{localStorage.removeItem(_abDraftKey(bookId))}catch{}bookId&&fetch(`/api/reader/docs/${encodeURIComponent(bookId)}/scripts/cast`,{method:"DELETE"}).catch(()=>{})}const AB_DIALOGUE_RE=/[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/;function audiobookHasDialogue(t){return AB_DIALOGUE_RE.test(t||"")}function audiobookDehyphenate(t){return(t||"").replace(/([a-zäöüß])-\s+(?=[a-zäöüßA-ZÄÖÜ])/g,"$1")}const AB_SPEECH_VERBS="(?:sagte|fragte|rief|antwortete|erwiderte|entgegnete|meinte|fl\xFCsterte|wisperte|raunte|murmelte|brummte|knurrte|br\xFCllte|schrie|stammelte|fauchte|zischte|seufzte|lachte|kicherte|befahl|wiederholte|fuhr\\s+fort|grunzte|versetzte|unterbrach|dr\xE4ngte|herrschte|keuchte|hauchte|\xE4chzte|st\xF6hnte|jammerte|spottete|h\xF6hnte|warf\\s+ein|setzte\\s+hinzu|erkl\xE4rte|verk\xFCndete|widersprach|beharrte|gestand|best\xE4tigte|erinnerte|dachte|\xFCberlegte|sinnierte|said|asked|replied|answered|whispered|murmured|muttered|shouted|cried|called|exclaimed|added|continued)",AB_NOTNAME=new Set(["Der","Die","Das","Den","Dem","Ein","Eine","Einen","Er","Sie","Es","Ich","Du","Wir","Ihr","Man","Und","Aber","Da","Dann","Doch","So","Nun","Jetzt","The","He","She","It","They","A","An","And","But","Then","Now","Sofort","Pl\xF6tzlich","Endlich","Schlie\xDFlich","Stille","Schweigen","Stimme","Stimmen","Frage","Antwort","Gel\xE4chter","Wieder","Gleich","Sogleich","Langsam","Leise","Laut","Kaum","Vielleicht","Nat\xFCrlich","Wirklich","Ja","Nein","Komm","Warte","Halt","Geh","Hier","Dort","Oben","Unten","Schon","Noch","Auch","Nur","Immer","Nie"]),_AB_NAME="([A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+)",AB_PERSON_NOUNS=new Set(["Mann","Frau","Junge","M\xE4dchen","Alte","Alter","Fremde","Fremder","Krieger","Kriegerin","W\xE4chter","Wache","Soldat","Hauptmann","Ork","Ritter","Magier","Magierin","Zwerg","Elf","Elfe","H\xE4ndler","Wirt","Wirtin","Bauer","Priester","Priesterin","Nachbar","Nachbarin","Sklave","Sklavin","Verweser","Inquisitor","General","K\xF6nig","K\xF6nigin","Prinz","Prinzessin","F\xFCrst","F\xFCrstin","Baron","Baronin","Bote","Diener","Dienerin","Knabe","Kind","Reiter","Reiterin","Bogensch\xFCtze","Schmied","Schmiedin","Heiler","Heilerin","Gelehrte","Gelehrter","Kapit\xE4n","Anf\xFChrer","Anf\xFChrerin"]);function audiobookResolveUnknowns(segs,prevTail,roster){var _a2,_b2;const isUnknown=s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)),isNamed=s=>(s==null?void 0:s.type)==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt|Narrator$/i.test(s.speaker),names=(roster||[]).filter(n=>n&&!/^(Narrator|Unknown|Unbekannt)/i.test(n)).sort((a,b)=>b.length-a.length),lastNameIn=text=>{let best=null,bestAt=-1;for(const n of names){const at=text.lastIndexOf(n);at>bestAt&&(bestAt=at,best=n)}if(best)return best;const clause=text.match(new RegExp(_AB_NAME+"[^.!?:]{0,80}\\b(?:und\\s+)?(?:"+AB_SPEECH_VERBS+")[^:]{0,60}:\\s*$"));if(clause&&!AB_NOTNAME.has(clause[1])&&!AB_PERSON_NOUNS.has(clause[1]))return clause[1];const m=[...text.matchAll(/\b(?:[Dd]er|[Dd]ie|[Dd]en|[Dd]em|[Ee]in|[Ee]ine)\s+([A-ZÄÖÜ][a-zäöüß]{2,})\b/g)].map(x=>x[1]).filter(n=>AB_PERSON_NOUNS.has(n));return m.length?m[m.length-1]:null},all=[...prevTail||[],...segs],offset=(prevTail||[]).length,resolved=[];for(let i=offset;i<all.length;i++){const s=all[i];if(!isUnknown(s))continue;const prev=all[i-1],next=all[i+1];let who=null;if(!who){const dt=String(s.text||"");for(const n of names){const esc=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(new RegExp("\\b(?:man\\s+nennt\\s+mich|nennt\\s+mich|ich\\s+(?:bin|hei(?:\xDF|ss)e)|mein\\s+name\\s+ist|my\\s+name\\s+is|i\\s+am|call\\s+me|they\\s+call\\s+me)\\s+"+esc+"\\b","i").test(dt)){who=n;break}}}if(!who&&(prev==null?void 0:prev.type)==="narration"&&/:\s*$/.test(String(prev.text||"").trim())&&(who=lastNameIn(String(prev.text||""))),!who&&(prev==null?void 0:prev.type)==="narration"){const pt=String(prev.text||"").trim();for(const n of names){const esc=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(new RegExp("\\b"+esc+"\\b[^.!?:]{0,80}\\b(?:und\\s+)?(?:"+AB_SPEECH_VERBS+")\\b[^.!?]{0,60}[.!?]\\s*$").test(pt)){who=n;break}}}if(!who&&(prev==null?void 0:prev.type)==="narration"){const pt=String(prev.text||"").trim();for(const n of names){const esc=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(new RegExp("\\b"+esc+"\\b[^.!?]{0,60}\\b(?:fand|found)\\b[^.!?]{0,20}\\b(?:seine|ihre|his|her)\\s+(?:Stimme|voice)\\b[^.!?]{0,40}[.!?]\\s*$","i").test(pt)){who=n;break}if(new RegExp("\\b"+esc+"\\b[^.!?]{0,60}\\bbrach\\b[^.!?]{0,20}\\b(?:das\\s+)?Schweigen\\b[^.!?]{0,40}[.!?]\\s*$","i").test(pt)){who=n;break}}if(!who&&/\b(?:ihre|seine|his|her)\s+ersten\s+Worte\s+waren\b/i.test(pt)&&(who=lastNameIn(pt)),!who){const m2=pt.match(new RegExp(_AB_NAME+"(?:'s)?\\s+first\\s+words\\s+were\\b","i"));m2&&!AB_NOTNAME.has(m2[1])&&(who=m2[1])}}const _isFusedTurns=/\n\s*\n/.test(String(s.text||"").trim()),_nextIsInquitText=!_isFusedTurns&&next&&new RegExp("^\\W{0,3}(?:"+AB_SPEECH_VERBS+")\\b").test(String(next.text||"").trim());if(!who&&!_isFusedTurns&&((next==null?void 0:next.type)==="narration"||_nextIsInquitText)){const nt=String(next.text||"").trim();if(new RegExp("^\\W{0,3}(?:"+AB_SPEECH_VERBS+")\\s+(?:er|sie|es)\\b","i").test(nt)&&(prev==null?void 0:prev.type)==="narration"){const byPronoun=lastNameIn(String(prev.text||""));byPronoun&&(who=byPronoun)}if(!who)for(const n of names){const esc=n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");if(new RegExp("^\\W{0,3}"+esc+"(?:s|'s)?\\s+Stimme\\b","i").test(nt)){who=n;break}}let m=who?null:nt.match(new RegExp("^\\W{0,3}(?:"+AB_SPEECH_VERBS+")\\s+(?:(?:der|die|das|den|dem)\\s+)?(?:[a-z\xE4\xF6\xFC\xDF]+e[nrms]?\\s+){0,2}"+_AB_NAME));m||(m=nt.match(new RegExp("^(?:Der|Die)\\s+"+_AB_NAME+",\\s+(?:der|die)\\s+gesprochen hatte"))),m||(m=nt.match(new RegExp("^\\W{0,3}(?:ert\xF6nte|erklang|t\xF6nte|drang|kam)\\b[^.!?]{0,60}\\bes\\b[^.!?]{0,60}[.!?]\\s*"+_AB_NAME))),m&&!AB_NOTNAME.has(m[1])&&(who=m[1])}const prevEndsColon=(prev==null?void 0:prev.type)==="narration"&&/:\s*$/.test(String(prev.text||"").trim());if(!who&&!prevEndsColon){const prevDialogues=[];for(let j=i-1;j>=0&&j>=i-6&&prevDialogues.length<2&&!(((_a2=all[j])==null?void 0:_a2.page)!=null&&s.page!=null&&all[j].page!==s.page);j--)if(((_b2=all[j])==null?void 0:_b2.type)==="dialogue"){if(!isNamed(all[j])){prevDialogues.length=0;break}prevDialogues.push(all[j].speaker)}prevDialogues.length===2&&prevDialogues[0]!==prevDialogues[1]&&(who=prevDialogues[1])}who&&_nextIsInquitText&&next&&next.type!=="narration"&&(next.type="narration",next.speaker="Narrator",next.emotion=""),who&&!/^(Narrator|Unknown|Unbekannt)$/i.test(who)&&(s.speaker=who,resolved.push(who))}return resolved}const _AB_TAG_NAME="[A-Z\xC4\xD6\xDC][A-Za-z\xE4\xF6\xFC\xDF'\\-]+",AB_SPEECH_TAG_ONLY_RE=new RegExp("^\\s*[,.;:!?\u2013-]*\\s*(?:(?:"+AB_SPEECH_VERBS+")\\b\\s+(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|"+_AB_TAG_NAME+")\\s+(?:"+AB_SPEECH_VERBS+")\\b)(?:\\s+(?:mit|in|leise|laut|kalt|heiser|erstickt|ruhig|zornig|w\xFCtend|\xE4ngstlich|sp\xF6ttisch|ver\xE4chtlich|fragend|fl\xFCsternd|schrill|dumpf|slowly|coldly|quietly|softly|angrily|hoarsely)\\b[\\s\\S]*)?[.!?\u2026]*\\s*$","i");function audiobookIsSpeechTagOnly(text){const t=String(text||"").trim();return!t||t.length>180||/[»«„“”"‟‚‘’『「]/.test(t)?!1:AB_SPEECH_TAG_ONLY_RE.test(t)}function audiobookGuessSpeaker(after,before){let m;const ok=n=>n&&!AB_NOTNAME.has(n)?n:null;if(m=new RegExp("^[\\s,;\u2013-]*"+AB_SPEECH_VERBS+"\\s+(?:der|die|das|ein|eine)?\\s*"+_AB_NAME).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp("^[\\s,;\u2013-]*"+_AB_NAME+"\\s+"+AB_SPEECH_VERBS).exec(after||"")){const r=ok(m[1]);if(r)return r}if(m=new RegExp(_AB_NAME+"\\s+"+AB_SPEECH_VERBS+"[\\s:,\u2013-]*$").exec(before||"")){const r=ok(m[1]);if(r)return r}return null}const AB_QUOTE_PAIRS={"\xBB":"\xAB","\xAB":"\xBB","\u201E":"\u201C","\u201C":"\u201D",'"':'"',"\u300C":"\u300D","\u300E":"\u300F","\u2018":"\u2019","\u201A":"\u2018"},_AB_UNCLOSED_TAG_RE=new RegExp("^\\s*(?:[,;:\u2013-]\\s*)?(?:"+AB_SPEECH_VERBS+"|(?:er|sie|es|ich|du|wir|ihr|he|she|it|they|I|we|you)\\s+"+AB_SPEECH_VERBS+")\\b","i");function _abUnclosedQuoteEnd(raw,closeIdx){const limit=closeIdx>=0?closeIdx:raw.length;for(let i=0;i<limit;i++){if(!/[.!?]/.test(raw[i])||raw.slice(0,i+1).trim().length<2)continue;const after=raw.slice(i+1,Math.min(limit,i+140));if(_AB_UNCLOSED_TAG_RE.test(after))return i+1}if(closeIdx<0){const m=raw.match(/^([\s\S]{2,180}?[.!?])(?:\s|$)/);if(m&&raw.slice(m[1].length).trim().length>40)return m[1].length}return closeIdx>=0?closeIdx:raw.length}function _abOrphanClosingQuoteSpan(text,closeIdx,minStart){var _a2;if(!"\xAB\u201D\u2019\u2039\u300D\u300F".includes(text[closeIdx]))return null;const before=text.slice(minStart,closeIdx),prev=((_a2=before.match(/\S(?=\s*$)/))==null?void 0:_a2[0])||"";if(!/[.!?]/.test(prev))return null;const trimmedLen=before.trimEnd().length;let localStart=0;const boundaryRe=/[\n\r]|[.!?:]\s+/g;let m;for(;m=boundaryRe.exec(before);){const next=m.index+m[0].length;next<trimmedLen-1&&(localStart=next)}const quote=before.slice(localStart).trim();return/^[\s\S]{2,220}[.!?]$/.test(quote)?{start:minStart+localStart,end:closeIdx+1,quote}:null}const AB_QUOTE_OPENERS="\xBB\u201E\u201A\u203A",AB_QUOTE_CLOSERS=`\xAB"'\u2039`;function _audiobookFixOrphanedQuoteMarks(segments){const isQuoteChar=ch=>ch&&(AB_QUOTE_OPENERS+AB_QUOTE_CLOSERS).includes(ch);for(let i=0;i<segments.length;i++){const s=segments[i];if(s.type!=="narration")continue;const trimmed=s.text.trim();if(!trimmed)continue;const lead=trimmed[0];if(AB_QUOTE_CLOSERS.includes(lead)){const prev=segments[i-1];prev&&prev.type==="dialogue"&&!isQuoteChar(prev.text.trim().slice(-1))&&(prev.text=prev.text.trimEnd()+lead,s.text=trimmed.slice(1).trimStart());continue}const tail=trimmed[trimmed.length-1];if(AB_QUOTE_OPENERS.includes(tail)){const next=segments[i+1];next&&next.type==="dialogue"&&!isQuoteChar(next.text.trim()[0])&&(next.text=tail+next.text.trimStart(),s.text=trimmed.slice(0,-1).trimEnd())}}return segments.filter(s=>s.text.trim())}function _audiobookMergeAdjacentSameSpeaker(segments){const out=[];for(const s of segments){const last=out[out.length-1];if(last&&last.type===s.type&&last.page===s.page){const lastSpeaker=(last.speaker||"Narrator").trim().toLowerCase(),curSpeaker=(s.speaker||"Narrator").trim().toLowerCase(),sameEmotion=s.type!=="dialogue"||(last.emotion||"").toLowerCase()===(s.emotion||"").toLowerCase(),unknownSpeaker=!lastSpeaker||lastSpeaker==="unknown"||lastSpeaker==="unbekannt";if(lastSpeaker===curSpeaker&&sameEmotion&&!(s.type==="dialogue"&&unknownSpeaker)){const lastText=last.text.trimEnd(),curText=s.text.trimStart();last.text=/[.!?…»«”"']$/.test(lastText)?lastText+`
`+curText:lastText+" "+curText;continue}}out.push(Object.assign({},s))}return out}function _audiobookMergeSplitSentences(segments){const out=[];let merged=0;for(const s of segments){const prev=out[out.length-1],cur=String((s==null?void 0:s.text)||"").trim();if(prev&&prev.type==="narration"&&(s==null?void 0:s.type)==="narration"&&cur){const prevText=String(prev.text||"").trimEnd(),endsOpen=prevText&&!/[.!?:;…»"'\)\]]$/.test(prevText),startsLower=/^[a-zäöüß]/.test(cur),prevIsInquit=new RegExp(_AB_INQUIT_VERBS+"[^.!?]{0,40},\\s*$","i").test(prevText);if(endsOpen&&startsLower&&!prevIsInquit){prev.text=prevText+" "+cur,merged++;continue}}out.push(s)}return{segments:out,merged}}function _audiobookDedupNearbyDuplicates(segments){const QUOTE_CHARS='\xBB\xAB\u201E"\u2018\u2019\u203A\u2039',stripRe=new RegExp(`(^[${QUOTE_CHARS}\\s]+)|([${QUOTE_CHARS}\\s]+$)`,"g"),norm=t=>String(t||"").replace(stripRe,"").toLowerCase().replace(/\s+/g," ").trim(),WINDOW=30,MIN_LEN=20,out=[];let removed=0;for(const s of segments){const nt=norm(s==null?void 0:s.text);if(nt.length>=MIN_LEN){let isDup=!1;for(let k=out.length-1;k>=0&&out.length-k<=WINDOW;k--)if(out[k].type===s.type&&norm(out[k].text)===nt){isDup=!0;break}if(isDup){removed++;continue}}out.push(s)}return{segments:out,removed}}function audiobookSplitByQuotes(text){const spans=[];for(let i=0;i<text.length;i++){const open=text[i],close=AB_QUOTE_PAIRS[open];if(!close)continue;const minStart=spans.length?spans[spans.length-1].end:0,orphan=_abOrphanClosingQuoteSpan(text,i,minStart);if(orphan){spans.push(orphan),i=orphan.end-1;continue}if((open==="\xAB"||open==="\xBB")&&i>0&&!/[\s([{—–-]/.test(text[i-1]))continue;const raw=text.slice(i+1),closeIdx=raw.indexOf(close),endInRaw=_abUnclosedQuoteEnd(raw,closeIdx),quote=raw.slice(0,endInRaw).trim();if(!quote)continue;const consumedClose=closeIdx>=0&&endInRaw===closeIdx;spans.push({start:i,end:i+1+endInRaw+(consumedClose?1:0),quote}),i=spans[spans.length-1].end-1}if(!spans.length)return[{speaker:"Narrator",type:"narration",text,emotion:""}];const out=[];let last=0;for(let k=0;k<spans.length;k++){const sp=spans[k],pre=text.slice(last,sp.start);if(pre.trim()&&out.push({speaker:"Narrator",type:"narration",text:pre.trim(),emotion:""}),sp.quote){const after=text.slice(sp.end,k+1<spans.length?spans[k+1].start:text.length),speaker=audiobookGuessSpeaker(after,pre)||"Unknown";out.push({speaker,type:"dialogue",text:sp.quote,emotion:""})}last=sp.end}const tail=text.slice(last).trimEnd();return tail.trim()&&out.push({speaker:"Narrator",type:"narration",text:tail.trim(),emotion:""}),audiobookTurnTaking(out)}function audiobookTurnTaking(segs){let a=null,b=null;for(const s of segs)if(s.type==="dialogue"){if(s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker))s.speaker!==b&&(a=b,b=s.speaker);else if(a&&b&&a!==b){s.speaker=a;const t=a;a=b,b=t}}return segs}function audiobookIsRouterModel(model){return/^auto[-_ ]?router/i.test(String(model||"").trim())}function audiobookSafeLlmModel(model){const m=String(model||"").trim(),saved=String(typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_model||"").trim();return audiobookIsRouterModel(m)&&saved&&!audiobookIsRouterModel(saved)?saved:m||saved}function audiobookLlmUrl(){var _a2;return((_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim())||typeof _appSettings!="undefined"&&(_appSettings==null?void 0:_appSettings.llm_url)||(typeof rehDefaultLlmUrl=="function"?rehDefaultLlmUrl():"")}function audiobookLlmModel(){var _a2;return audiobookSafeLlmModel(((_a2=$("reh-llm-model"))==null?void 0:_a2.value)||"")}function audiobookLang(text){var _a2;const manual=((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||"";if(manual)return manual;const sample=String(text||_audiobook.lastText||"").slice(0,4e3);return sample&&typeof detectLang=="function"&&detectLang(sample)||""}function audiobookAttributeBody(fields,promptOverride=null){const prompt=typeof promptOverride=="string"?promptOverride.trim():"";return prompt?{...fields,audiobook_prompt:prompt}:fields}function audiobookCurrentCastLlm(panel){var _a2,_b2;const url=((_a2=panel==null?void 0:panel.querySelector("#ab-cv-llm-url"))==null?void 0:_a2.value.trim())||audiobookLlmUrl(),rawModel=((_b2=panel==null?void 0:panel.querySelector("#ab-cv-llm-select"))==null?void 0:_b2.value)||audiobookLlmModel();return{url,model:audiobookSafeLlmModel(rawModel)}}function audiobookSaveLlmChoice(url,model){const cleanUrl=String(url||"").trim(),cleanModel=audiobookSafeLlmModel(model);typeof _appSettings!="undefined"&&_appSettings&&(cleanUrl&&(_appSettings.llm_url=cleanUrl),cleanModel&&(_appSettings.llm_model=cleanModel));const patch={};cleanUrl&&(patch.llm_url=cleanUrl),cleanModel&&(patch.llm_model=cleanModel),Object.keys(patch).length&&fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(patch)}).catch(()=>{});const rehModel=$("reh-llm-model");return rehModel&&cleanModel&&[...rehModel.options].some(o=>o.value===cleanModel)&&(rehModel.value=cleanModel),{url:cleanUrl,model:cleanModel}}function audiobookScopeText(){var _a2,_b2,_c2;if(_audiobook.pageMarks=[],typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";const idxs=readerScopeIndices(),anchors=[];let lastPage=null;for(const i of idxs){const u=readerState.sentences[i],pg=(_c2=(_b2=u==null?void 0:u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;pg!=null&&pg!==lastPage&&(anchors.push({page:pg,anchor:(u.text||"").trim().slice(0,40)}),lastPage=pg)}let raw="";for(const i of idxs){const u=readerState.sentences[i];u!=null&&u.text&&(raw+=raw?(u.paraStart?`
`:" ")+u.text:u.text)}raw=raw.replace(/[ \t]+/g," ").replace(/\n{3,}/g,`
`).trim();const text=audiobookDehyphenate(raw);let from=0;for(const a of anchors){const probe=audiobookDehyphenate(a.anchor).slice(0,24);if(!probe)continue;const pos=text.indexOf(probe,from);pos>=0&&(_audiobook.pageMarks.push({offset:pos,page:a.page}),from=pos)}return text}function audiobookProgress(total){let ov=document.getElementById("audiobook-overlay");ov||(ov=document.createElement("div"),ov.id="audiobook-overlay",ov.className="audiobook-overlay",ov.innerHTML=`<div class="audiobook-box">
<div class="audiobook-title"><span class="mdi mdi-drama-masks"></span> Casting audiobook</div>
<div class="audiobook-msg" id="audiobook-msg">Analysing\u2026</div>
<div class="reader-synth-track"><div class="reader-synth-fill" id="audiobook-fill"></div></div>
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="audiobook-cancel">Cancel</button></div>
</div>`,document.body.appendChild(ov),ov.querySelector("#audiobook-cancel").addEventListener("click",()=>{_audiobook.cancel=!0})),ov.hidden=!1;const fill=ov.querySelector("#audiobook-fill"),msg=ov.querySelector("#audiobook-msg");return{update(done,label){fill&&(fill.style.width=done/total*100+"%"),msg&&label&&(msg.textContent=label)},done(){ov.hidden=!0}}}function _abNormalizeColor(color,fallbackName){return clNormalizeColor(color,fallbackName||"Narrator")}function _abDefaultCharacterColor(name){return/^Unknown|Unbekannt/i.test(name||"")?"#a83232":_abNormalizeColor(null,name)}function _abRecordColor(rec,name){var _a2;return _abNormalizeColor((rec==null?void 0:rec.color)||((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.color),name||(rec==null?void 0:rec.name))}function highlightText(text,names){if(!text)return"";let html=escHtml(text);return(names||_audiobook&&_audiobook.roster||[]).filter(n=>n&&n.toLowerCase()!=="narrator"&&!/^Unknown|Unbekannt/i.test(n)).sort((a,b)=>b.length-a.length).forEach(name=>{if(name.length<2)return;const safe=name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),color=_abDefaultCharacterColor(name);html=html.replace(new RegExp(`\\b(${safe})\\b`,"gi"),m=>`<span style="border-bottom: 2px solid ${color}; font-weight: 600;">${m}</span>`)}),html}function _abShowBusyOverlay(message,withProgress){const ov=document.createElement("div");return ov.className="audiobook-overlay ab-busy-overlay",ov.innerHTML=`<div class="audiobook-box" style="text-align:center;">
<span class="mdi mdi-loading mdi-spin" style="font-size:28px; color:var(--accent);"></span>
<div class="audiobook-msg ab-busy-msg" style="margin:12px 0 0;">${escHtml(message)}</div>
${withProgress?'<div class="reader-synth-track" style="margin-top:10px"><div class="reader-synth-fill ab-busy-fill" style="width:0%"></div></div>':""}
</div>`,document.body.appendChild(ov),ov.setProgress=(done,total)=>{const fill=ov.querySelector(".ab-busy-fill");fill&&total&&(fill.style.width=Math.round(done/total*100)+"%");const msgEl=ov.querySelector(".ab-busy-msg");msgEl&&(msgEl.textContent=`${message} (${done} / ${total})`)},ov}let _abFootMenuEl=null;function _abCloseFootMenu(){var _a2;_abFootMenuEl&&((_a2=document.getElementById(_abFootMenuEl.dataset.forId))==null||_a2.classList.remove("is-menu-open"),_abFootMenuEl.remove(),_abFootMenuEl=null,document.removeEventListener("click",_abFootMenuOutside,!0),document.removeEventListener("keydown",_abFootMenuKey,!0))}function _abFootMenuOutside(e){_abFootMenuEl&&!_abFootMenuEl.contains(e.target)&&!e.target.closest(".ab-foot-trigger")&&_abCloseFootMenu()}function _abFootMenuKey(e){e.key==="Escape"&&_abCloseFootMenu()}function _abToggleFootMenu(triggerBtn,items){const reopening=_abFootMenuEl&&_abFootMenuEl.dataset.forId===triggerBtn.id;if(_abCloseFootMenu(),reopening)return;const el=document.createElement("div");el.className="ab-foot-menu",el.dataset.forId=triggerBtn.id,el.innerHTML=items.map((it,i)=>it.divider?'<div class="ab-foot-menu-sep"></div>':`<button type="button" class="ab-foot-menu-item${it.danger?" is-danger":""}" data-idx="${i}" ${it.disabled?"disabled":""} title="${escHtml(it.title||"")}"><span class="mdi ${it.icon}"></span><span class="ab-foot-menu-item-label">${escHtml(it.label)}</span></button>`).join(""),document.body.appendChild(el);const rect=triggerBtn.getBoundingClientRect();el.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-el.offsetWidth-8))+"px";const menuH=el.offsetHeight,spaceBelow=window.innerHeight-rect.bottom,spaceAbove=rect.top;spaceBelow>=menuH+12||spaceBelow>=spaceAbove?(el.style.top=Math.min(rect.bottom+6,Math.max(8,window.innerHeight-menuH-8))+"px",el.style.bottom="auto"):(el.style.bottom=window.innerHeight-rect.top+6+"px",el.style.top="auto"),el.querySelectorAll(".ab-foot-menu-item").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const it=items[Number(btn.dataset.idx)];_abCloseFootMenu(),(_a2=it==null?void 0:it.onClick)==null||_a2.call(it)})}),_abFootMenuEl=el,triggerBtn.classList.add("is-menu-open"),setTimeout(()=>{document.addEventListener("click",_abFootMenuOutside,!0),document.addEventListener("keydown",_abFootMenuKey,!0)},0)}function _abOpenRecastSelectPopup(existingChars){const ov=document.createElement("div");ov.className="audiobook-overlay";const selected=new Set;let sort="lines",filter="";try{sort=localStorage.getItem("ttsvc_ab_recast_sort")||"lines"}catch{}const rowHtml=c=>{var _a2;const lineCount=((_a2=c==null?void 0:c.sheet)==null?void 0:_a2.line_count)||0,color=_abRecordColor(c,c.name),avatar=c!=null&&c.image?`<span class="ab-char-dot ab-char-dot-img" style="border-color:${color}"><img src="${escHtml(c.image)}" alt=""></span>`:`<span class="ab-char-dot" style="background:${color}">${escHtml(((c==null?void 0:c.name)||"?")[0].toUpperCase())}</span>`;return`<label class="ab-char-item" data-name="${escHtml(c.name)}">
<input type="checkbox" class="ab-recast-cb" value="${escHtml(c.name)}"${selected.has(c.name)?" checked":""}>
${avatar}
<span class="ab-char-name">${escHtml(c.name)}</span>
<b class="ab-char-count">${lineCount}</b>
</label>`},render=()=>{const q=filter.trim().toLowerCase(),items=existingChars.filter(c=>!q||(c.name||"").toLowerCase().includes(q)).sort(sort==="alpha"?(a,b)=>(a.name||"").localeCompare(b.name||""):(a,b)=>{var _a2,_b2;return(((_a2=b.sheet)==null?void 0:_a2.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)}),list=ov.querySelector(".ab-recast-select-list");list&&(list.innerHTML=items.length?items.map(rowHtml).join(""):'<div class="ab-cv-empty">No matches</div>',list.querySelectorAll(".ab-recast-cb").forEach(cb=>{cb.addEventListener("change",()=>{cb.checked?selected.add(cb.value):selected.delete(cb.value)})}))};ov.innerHTML=`<div class="audiobook-box"><div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Cast selected character roles</div><div class="audiobook-msg">Only re-reads the passages that mention the characters you pick below, including known aliases, plus a little surrounding context. Everyone else's sheet stays as-is.</div><div class="ab-cv-side-tools"><input type="text" class="ab-cv-side-search" id="ab-recast-search" placeholder="Search characters..." autocomplete="off"><select class="ab-cv-side-sort" id="ab-recast-sort" title="Sort"><option value="lines">Lines</option><option value="alpha">A\u2013Z</option></select></div><div class="ab-recast-select-list"></div><div class="audiobook-actions"><button class="btn-secondary btn-sm" id="ab-recast-select-all">Select all</button><span style="flex:1"></span><button class="btn-secondary btn-sm" id="ab-recast-cancel">Cancel</button><button class="btn-primary btn-sm" id="ab-recast-confirm">Generate selected sheets</button></div></div>`,document.body.appendChild(ov),render();const sortSel=ov.querySelector("#ab-recast-sort");sortSel.value=sort,sortSel.addEventListener("change",()=>{sort=sortSel.value;try{localStorage.setItem("ttsvc_ab_recast_sort",sort)}catch{}render()});let _searchT=null;ov.querySelector("#ab-recast-search").addEventListener("input",e=>{clearTimeout(_searchT);const v=e.target.value;_searchT=setTimeout(()=>{filter=v,render()},120)}),ov.querySelector("#ab-recast-cancel").addEventListener("click",()=>ov.remove()),ov.querySelector("#ab-recast-select-all").addEventListener("click",e=>{const allSelected=existingChars.length>0&&existingChars.every(c=>selected.has(c.name));allSelected?selected.clear():existingChars.forEach(c=>selected.add(c.name)),e.currentTarget.textContent=allSelected?"Select all":"Select none",render()}),ov.querySelector("#ab-recast-confirm").addEventListener("click",async()=>{const names=[...selected];if(!names.length){toast("Select at least one character","error");return}ov.remove(),typeof window.audiobookRecastSelectedCharacters=="function"?await window.audiobookRecastSelectedCharacters(names):typeof window.csForReaderSelective=="function"&&await window.csForReaderSelective(names)})}async function audiobookRecastSelectedCharacters(selectedNames){return typeof window.csForReaderSelective=="function"?window.csForReaderSelective(selectedNames):(toast("Character-sheet recast is unavailable right now","error"),null)}window.audiobookRecastSelectedCharacters=audiobookRecastSelectedCharacters;function _abScrollIntoView(el,opts){el&&(el.scrollIntoView(Object.assign({behavior:"auto"},opts)),requestAnimationFrame(()=>requestAnimationFrame(()=>{el.scrollIntoView(Object.assign({behavior:"smooth"},opts))})))}function audiobookCastView(total,llmUrl,defaultModel,isIdle=!1){var _a2,_b2,_c2,_d2,_e2,_f2;const panel=document.getElementById("reader-audiobook-panel");if(!panel)return;if(typeof window.navReaderView=="function")window.navReaderView("cast");else if(typeof window.showReaderView=="function")window.showReaderView("cast");else{const mainView=document.getElementById("reader-main-view");mainView&&(mainView.hidden=!0),panel.hidden=!1}panel.className="ab-castpanel-inline card",panel.style.display="",panel.style.flexDirection="",panel.style.minHeight="",panel.innerHTML=`
<div class="ab-castpanel-head" id="ab-cv-head">
<span class="mdi mdi-drama-masks"></span>
<span class="ab-castpanel-title">Casting audiobook</span>
<span class="ab-castpanel-count" id="ab-cv-count">passage 0 / ${total}</span>
<button class="ab-castpanel-btn" id="ab-cv-settings-toggle" title="Show / hide casting settings" style="width:auto; padding:2px 6px; margin-left:6px;"><span class="mdi mdi-chevron-up" id="ab-cv-settings-chevron"></span></button>
<span style="flex:1"></span>
<span id="ab-cv-settings" style="display:flex; align-items:center; gap:4px;">
<label for="ab-cv-llm-url" style="font-size:12px; font-weight:600; color:var(--subtext); margin-right:4px;">LLM Engine</label>
<select id="ab-cv-llm-url" style="width:260px; margin-right:4px; padding:2px 6px; font-size:12px; border:1px solid var(--border); border-radius:4px; background:var(--surface); color:var(--text); outline:none; appearance:auto; cursor:pointer;" aria-label="LLM API URL">
<option value="${escHtml(llmUrl)}">${escHtml(llmUrl||"\u2014 Select an endpoint \u2014")}</option>
</select>
<button class="ab-castpanel-btn" id="ab-cv-llm-refresh" style="margin-right:4px;" title="Fetch models"><span class="mdi mdi-refresh"></span></button>
<select id="ab-cv-llm-select" style="width:220px; padding:2px 6px; font-size:12px; border:1px solid var(--border); border-radius:4px; background:var(--surface); color:var(--text); outline:none;" aria-label="LLM Model" title="Change LLM model for character extraction">
<option value="${escHtml(defaultModel)}">${escHtml(defaultModel||"\u2014 fetch models \u2014")}</option>
</select>
<span style="margin-right:8px;"></span>
<button class="ab-castpanel-btn" id="ab-cv-prompt-btn" title="Edit Casting Prompt" style="display:flex; align-items:center; gap:4px; padding:4px 8px; font-weight:600; font-size:12px; width:auto; height:auto; min-height:28px;"><span class="mdi mdi-text-box-edit-outline"></span> Prompt <span class="mdi mdi-chevron-down" id="ab-cv-prompt-chevron"></span></button>
</span>
</div>
<div class="ab-castpanel-prompt" id="ab-cv-prompt-panel" hidden style="padding:10px 12px; background:var(--panel); border-bottom:1px solid var(--border);">
<textarea id="ab-cv-prompt-text" rows="8" style="width:100%; font-family:monospace; font-size:13px; font-weight:500; padding:10px; line-height:1.5; border:1px solid var(--border); border-radius:6px; background:var(--surface); color:var(--text); resize:vertical;" placeholder="LLM casting instructions..."></textarea>
<div style="display:flex; justify-content:flex-end; margin-top:8px; gap:8px; align-items:center;">
<select id="ab-cv-prompt-lib" class="btn-secondary btn-sm" style="max-width:200px; outline:none; border:1px solid var(--border); border-radius:4px; padding:4px 8px; font-size:12px; background:var(--surface);" aria-label="Load a saved prompt">
<option value="">\u2014 Load saved prompt \u2014</option>
</select>
<button class="btn-secondary btn-sm" id="ab-cv-prompt-del" title="Delete selected prompt" style="display:none; color:var(--error);"><span class="mdi mdi-delete-outline"></span></button>
<input type="text" id="ab-cv-prompt-name" style="width:140px; padding:4px 8px; font-size:12px; border:1px solid var(--border); border-radius:4px; background:var(--surface); color:var(--text); outline:none;" placeholder="Preset name...">
<button class="btn-primary btn-sm" id="ab-cv-prompt-save" title="Save current prompt as a preset"><span class="mdi mdi-content-save"></span> Save preset</button>
</div>
</div>
<div class="ab-cv-body">
<div class="ab-cv-feed-wrap">
<div class="ab-cv-feed" id="ab-cv-feed">
<div class="ab-skel-feed">
<div class="ab-skel-row ab-skel-narr"><div class="ab-skel-line" style="width:78%"></div><div class="ab-skel-line" style="width:55%"></div></div>
<div class="ab-skel-row ab-skel-dlg"><div class="ab-skel-spk"></div><div class="ab-skel-line" style="width:68%"></div></div>
<div class="ab-skel-row ab-skel-narr"><div class="ab-skel-line" style="width:91%"></div><div class="ab-skel-line" style="width:42%"></div></div>
<div class="ab-skel-row ab-skel-dlg"><div class="ab-skel-spk"></div><div class="ab-skel-line" style="width:73%"></div><div class="ab-skel-line" style="width:30%"></div></div>
<div class="ab-skel-row ab-skel-narr"><div class="ab-skel-line" style="width:60%"></div></div>
<div class="ab-skel-row ab-skel-dlg"><div class="ab-skel-spk"></div><div class="ab-skel-line" style="width:82%"></div></div>
<div class="ab-skel-row ab-skel-narr"><div class="ab-skel-line" style="width:88%"></div><div class="ab-skel-line" style="width:48%"></div></div>
<div class="ab-skel-row ab-skel-dlg"><div class="ab-skel-spk"></div><div class="ab-skel-line" style="width:64%"></div></div>
</div>
</div>
<button class="ab-cv-jump-btn" id="ab-cv-jump-btn" hidden title="Jump to latest"><span class="mdi mdi-chevron-double-down"></span> Live</button>
</div>
<div class="ab-cv-side" id="ab-cv-side">
<div class="ab-cv-side-head">
<span class="ab-cv-side-title">Characters found</span>
<button class="ab-cv-side-collapse" id="ab-cv-side-collapse" type="button" title="Collapse to avatars"><span class="mdi mdi-chevron-right"></span></button>
</div>
<div class="ab-cv-side-tools">
<input type="text" class="ab-cv-side-search" id="ab-cv-side-search" placeholder="Search characters\u2026" autocomplete="off">
<select class="ab-cv-side-sort" id="ab-cv-side-sort" title="Sort">
<option value="count">Lines</option>
<option value="alpha">A\u2013Z</option>
</select>
</div>
<div class="ab-cv-chars" id="ab-cv-chars">
<div class="ab-skel-chars">
${[38,62,45,28,54,35].map(w=>`<div class="ab-skel-char-row"><div class="ab-skel-dot"></div><div class="ab-skel-line" style="width:${w}%"></div><div class="ab-skel-num"></div></div>`).join("")}
</div>
</div>
</div>
</div>
<div class="ab-castpanel-foot" id="ab-cv-foot" style="border-top:1px solid var(--border); padding:10px 12px; display:flex; flex-wrap:wrap; gap:8px; justify-content:flex-end; align-items:center;">
<span class="ab-cv-status" id="ab-cv-status-msg" style="color:var(--subtext); font-size:12px; flex:1 1 220px; min-width:220px; display:none;">Ready to identify characters.</span>
<div class="reader-synth-track ab-castpanel-bar" hidden>
<div class="reader-synth-fill" id="ab-cv-fill"></div>
<div id="ab-cv-fill-text"></div>
</div>
<button class="btn-secondary btn-sm" id="ab-cv-cancel" style="color:var(--red); margin-right:8px;"><span class="mdi mdi-close"></span> Cancel</button>
<button class="btn-secondary btn-sm" id="ab-cv-start-recast-unk" style="display:none; margin-right:8px; color:var(--error);" title="Re-scan only the unknown segments"><span class="mdi mdi-account-question-outline"></span> Identify unknown characters</button>
<button class="btn-secondary btn-sm" id="ab-cv-start-recast" style="display:none; margin-right:8px;" title="Scan the text for characters and build the cast list"><span class="mdi mdi-refresh"></span> Identify all characters</button>
<button class="btn-primary btn-sm" id="ab-cv-start-cast" style="display:none;"><span class="mdi mdi-play"></span> Cast now</button>
</div>`;const AB_DEFAULT_PROMPT=`Du bist ein erfahrener Drehbuchautor und H\xF6rbuch-Regisseur. Deine Aufgabe ist es, einen Auszug aus einem deutschen Roman zu analysieren und ihn perfekt in einzelne Segmente f\xFCr Erz\xE4hler und Dialoge (w\xF6rtliche Rede) zu unterteilen.
WICHTIGE DEFINITION VON DIALOG:
Text ist NUR dann 'dialogue', wenn er explizit in Anf\xFChrungszeichen steht (z.B. \xBB...\xAB, \u201E...\u201C, "...", \xAB...\xBB, <<...>>) oder mit einem Gedankenstrich (\u2014) beginnt. ALLES ANDERE, einschlie\xDFlich Handlungsbeschreibungen (Inquit-Formeln), inneren Gedanken und Beschreibungen, MUSS als 'narration' (Erz\xE4hler) deklariert werden.
Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).
ABSATZ- UND KAPITELSTRUKTUR:
Eine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/Szenenanfang). Eine sehr kurze, alleinstehende Zeile direkt vor einer Leerzeile (z.B. "1. Kapitel", "Prolog", ein Zahlwort) ist eine Kapitel\xFCberschrift \u2014 immer 'narration'/'Narrator', niemals Dialog. Behalte Leerzeilen als eigenst\xE4ndige narration-Segmente oder als Teil des umgebenden Erz\xE4hler-Segments bei; erfinde daraus keinen Dialog und l\xF6sche sie nicht aus dem rekonstruierten Text.
GRAMMATIK-REGELN F\xDCR NARRATION:
- Inquit-Formeln / Sprecher-Tags sind IMMER narration: finite Sprechverben wie sagte, fragte, rief, entgegnete, murmelte, fl\xFCsterte, schrie, antwortete + Subjekt/Pronomen/Name (z.B. "murmelte er mit erstickter Stimme.", ", entgegnete Marcian kalt.").
- Action Beats sind IMMER narration: Ein Charakter handelt, blickt, geht, lacht, schweigt, hebt die Hand, dreht sich um, usw. \u2014 auch wenn direkt davor/danach Dialog steht.
- Grammatische Probe: Wenn der Text eine Erz\xE4hler-Aussage \xDCBER das Sprechen ist (Verb + Sprecher + Art und Weise), ist es narration. Nur die tats\xE4chlich ge\xE4u\xDFerten W\xF6rter innerhalb der Anf\xFChrungszeichen sind dialogue.
- Satzfragmente mit f\xFChrendem Komma/Punkt wie ", sagte sie leise." oder ". fragte Uriens." sind niemals eigenst\xE4ndige Dialoge; sie geh\xF6ren als narration zum Erz\xE4hlertext.
ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv \u2014 arbeite wie ein Lektor):
1. Direkte Zuordnung: "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) IMMER zum tats\xE4chlichen Namen auf: Das Pronomen meint die zuletzt genannte Person passenden Geschlechts.
2. Doppelpunkt-Regel: Endet ein Erz\xE4hlersatz mit ":", spricht das Subjekt dieses Satzes das folgende Zitat (z.B. "Dann richtete er sich auf und rief in die Runde:" \u2192 der zuvor genannte Charakter spricht das n\xE4chste Zitat; "Ein anderer fragte verschlafen:" \u2192 dieser andere spricht).
3. Nachgestellte Zuordnung: Der Erz\xE4hlersatz NACH einem Zitat verr\xE4t oft den Sprecher \u2014 auch bei unpers\xF6nlicher Formel: "\xBBWas machst du denn da?\xAB ert\xF6nte es \xFCber ihm. Karyla hatte ihren Hammer weggelegt und war her\xFCbergekommen." \u2192 Karyla sprach das Zitat.
4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat AKTIV HANDELT (tritt vor, dreht sich, hebt die Hand), spricht meist \u2014 ABER NUR bei Handlungsverben. Verben der WAHRNEHMUNG oder REAKTION (sp\xFCrte, h\xF6rte, f\xFChlte, sah, blickte, lauschte, bemerkte, nickte) kennzeichnen den ZUH\xD6RER, NICHT den Sprecher (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau).
5. Adressaten-Regel: "X wandte sich an Y" / "X sah Y an" \u2192 X spricht das n\xE4chste Zitat, und Y ist der wahrscheinlichste Antwortende.
6. Das Ping-Pong-Prinzip: Sprechen zwei Personen miteinander, wechseln sie sich strikt ab \u2014 auch \xFCber viele Zitate OHNE Inquit-Formeln hinweg. Verfolge die Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung und f\xFChre sie bis zum Szenenende fort. In einer Zwei-Personen-Szene ist 'Unknown' fast immer falsch.
7. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt sie zum Wissen oder Tonfall eines bestimmten Charakters?
8. Namenlose Sprecher: Auch Rollenbezeichnungen aus dem Text sind g\xFCltige Sprecher \u2014 nutze sie statt 'Unknown' (z.B. 'Ork', 'Der Fremde', 'Nachbar', 'W\xE4chter', 'Junge').
9. Wiederverwendung: Nutze EXAKT die Namen aus der Liste der bekannten Charaktere, FALLS der Name dort aufgef\xFChrt ist. Wenn ein neuer Charakter spricht, extrahiere seinen Namen direkt aus dem Text (z.B. 'Karyla', 'Uriens').
10. 'Unknown' NUR, wenn eine Zuordnung trotz aller obigen Regeln absolut unm\xF6glich ist. Bevorzuge immer einen genannten Charakter oder eine Rollenbezeichnung gegen\xFCber 'Unknown'.
11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende \u2014 die Rede geht danach unver\xE4ndert weiter, bis das tats\xE4chliche schlie\xDFende Anf\xFChrungszeichen erscheint.
12. Stimm-Ank\xFCndigung: Erw\xE4hnt ein Erz\xE4hlersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), geh\xF6rt diese Zeile dieser Person \u2014 nicht 'Unknown'.
13. ZUH\xD6RER-AUSSCHLUSS (negative Evidenz \u2014 sehr wichtig): Ein Name im Erz\xE4hlsatz neben einem Zitat ist H\xC4UFIG der Zuh\xF6rer, nicht der Sprecher. Schlie\xDFe eine Person als Sprecher AUS, wenn sie (a) im Nachbarsatz wahrnimmt oder reagiert (Marcian sp\xFCrte ihre Tr\xE4nen auf seiner Brust; Marcian h\xF6rte dar\xFCber hinweg \u2192 Marcian H\xD6RT ZU, jemand anderes spricht), oder (b) im Zitat selbst ANGEREDET wird (Vokativ nach Komma: M\xF6gen die G\xF6tter dich sch\xFCtzen, Marcian. \u2192 Marcian ist der ANGESPROCHENE). Wer angeredet wird, spricht nicht. Wenn durch diesen Ausschluss kein Kandidat mehr \xFCbrig bleibt, suche WEITER ZUR\xDCCK (letzte eindeutige Nennung, Ping-Pong-Kette, wer in der Szene anwesend ist) \u2014 der Ausschluss grenzt die Kandidaten nur ein und rechtfertigt f\xFCr sich allein NIEMALS 'Unknown'.
16. FORTSETZUNGS-REGEL (wichtiger als Ping-Pong): Beschreibt der Erzaehlsatz ZWISCHEN zwei Zitaten dieselbe Person, die gerade gesprochen hat (per Pronomen oder Name, z.B. "Leise schluchzte sie.", "Er atmete tief durch."), dann spricht diese Person WEITER \u2014 das folgende Zitat gehoert IHR, nicht dem Gespraechspartner. Das Ping-Pong-Prinzip (Regel 6) gilt NUR, wenn der Erzaehlsatz auf die ANDERE Person zeigt oder gar keinen Hinweis enthaelt. Beispiel: Cindira spricht, dann "Leise schluchzte sie." \u2192 das naechste Zitat ist wieder CINDIRA; ein folgendes "Marcian spuerte ihre Traenen" bestaetigt nur, dass Marcian zuhoert (Regel 13).
17. NEU AUFTRETENDE, UNBENANNTE HANDELNDE: Fuehrt der Erzaehlsatz unmittelbar vor einem Zitat eine unbenannte Person handelnd ein ("Eine Frau durchbrach die Kette der Krieger und warf sich...", "Ein Soldat trat vor"), dann spricht GENAU DIESE Person. Verwende die Rollenbezeichnung aus dem Text als Sprechernamen ("Eine Frau", "Ein Soldat") \u2014 niemals 'Unknown'. Spricht dieselbe Person spaeter erneut (erkennbar am Pronomen im Erzaehlsatz), nutze exakt DENSELBEN Rollennamen weiter, damit sie eine durchgehende Stimme behaelt.
18. EPITHETA / BESTIMMTE BESCHREIBUNGEN als Sprecher: Bezeichnungen wie "die Elfe", "der Zauberer", "der Zwerg", "der Hauptmann" meinen fast immer eine BEREITS BEKANNTE Figur. Loese sie auf den kanonischen Namen auf, sobald der Text die Verbindung herstellt (Nyrilla ist die Elfe \u2192 "Laut hallte die Stimme der Elfe durch den Gang." bedeutet: NYRILLA hat gesprochen). Nutze immer den kanonischen Namen als Sprecher, niemals das Epitheton und niemals 'Unknown', damit die Figur durchgehend dieselbe Stimme behaelt.
19. GEMISCHTE SEGMENTE TRENNEN: Enthaelt ein Segment sowohl Erzaehltext als auch ein Zitat ("Nyrilla richtete sich auf. Alle anderen wichen zurueck. \xBBIch werde jetzt ...\xAB"), dann zerlege es: der Erzaehlteil wird ein eigenes Segment mit speaker 'Narrator' und type 'narration', das Zitat ein eigenes Segment mit type 'dialogue' und der handelnden Person als Sprecher (hier NYRILLA, denn sie richtet sich unmittelbar davor auf \u2014 siehe Regel 4). Ein gemischtes Segment darf niemals als Ganzes 'Unknown' bleiben. ZERLEGE EBENSO Segmente, die REDEBEITRAEGE MEHRERER SPRECHER enthalten: Ein Befehl, der jemanden beim Namen anspricht, und die darauffolgende Antwort sind ZWEI Sprecher, nicht einer (z.B. "Bringt Fackeln ... Lysandra, du nimmst dir einige Krieger ..." = MARCIAN befiehlt; "Mit Vergn\xFCgen, ich hab doch gleich gesagt ..." = LYSANDRA antwortet). Merkmal: Wer im ersten Teil per Vokativ ANGESPROCHEN wird (Regel 13b), ist typischerweise der Sprecher des NAECHSTEN Redebeitrags. Lege fuer jeden Redebeitrag ein eigenes Segment mit dem jeweiligen Sprecher an, statt den ganzen Block als 'Unknown' zu lassen.
14. GETEILTE ZITATE / Pronomen-Inquit: Beginnt ein Segment mit Komma oder Kleinbuchstaben plus Sprechverb und Pronomen (, antwortete er knapp. / , murmelte sie leise.), ist es die abgetrennte Inquit-Formel des DAVORSTEHENDEN Zitats. Ordne jenes Zitat der Person zu, auf die das Pronomen zeigt: die zuletzt genannte Person passenden Geschlechts VOR dem Zitat (er \u2192 letzter Mann, sie \u2192 letzte Frau). Solche Zeilen d\xFCrfen niemals 'Unknown' bleiben.
15. MEHRERE KANDIDATEN in der N\xE4he: Stehen mehrere bekannte Namen im Umfeld, entscheide in dieser Reihenfolge: (a) streiche alle nach Regel 13 ausgeschlossenen (Zuh\xF6rer/Angeredete); (b) bevorzuge, wer grammatisches SUBJEKT eines Sprech- oder Handlungsverbs ist, gegen\xFCber jemandem in Objekt- oder Pr\xE4positionalstellung; (c) pr\xFCfe, wessen Wissensstand, Rolle und Sprechweise zum Inhalt passt; (d) nutze das Ping-Pong-Prinzip aus Regel 6. Erst wenn danach zwei gleichwertige Kandidaten bleiben, ist 'Unknown' zul\xE4ssig.
F\xDCR JEDES SEGMENT GIBST DU FOLGENDES AUS:
- speaker: 'Narrator' f\xFCr Narration/Erz\xE4hlertext, oder den EXAKTEN Namen des Charakters f\xFCr gesprochene Dialoge.
- type: 'narration' oder 'dialogue'
- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' BEH\xC4LTST du die umschlie\xDFenden Anf\xFChrungszeichen als Teil des Texts (z.B. \xBBHallo!\xAB bleibt \xBBHallo!\xAB) \u2014 entferne sie NICHT und lasse niemals nur eines der beiden \xFCbrig.
- emotion: Bei Dialogen 1-2 deutsche W\xF6rter, die den Tonfall beschreiben (z.B. w\xFCtend, fl\xFCsternd, \xE4ngstlich). Bei Narration leer lassen ('').
STRIKTE FORMAT- UND TEXTREGELN:
- Mische NIEMALS Narration und Dialog im selben Segment! Trenne sie strikt. Wenn ein Zitat durch eine Handlungsanweisung unterbrochen wird (\xBBNein\xAB, sagte sie, \xBBhalt.\xAB), erstelle 3 Segmente: dialogue ("\xBBNein\xAB"), narration (", sagte sie, "), dialogue ("\xBBhalt.\xAB").
- \xBBText?\xAB und \xBBText!\xAB sind vollst\xE4ndige Dialoge \u2014 das ?\xAB bzw. !\xAB schlie\xDFt das Zitat ab, auch wenn es ungewohnt aussieht.
- PDF-/OCR-SCHUTZ: Wenn ein \xBB oder \xAB offensichtlich fehlt, darf dieser eine Fehler NICHT den Rest der Passage als Dialog verschlucken. Schlie\xDFe ein offenes \xBB-Zitat am ersten plausiblen Satzende (? ! .), besonders wenn danach eine Inquit-Formel folgt ("fl\xFCsterte er", "sagte sie", "rief Uriens") oder normale Erz\xE4hlerhandlung weitergeht.
- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und h\xE4nge das schlie\xDFende \xAB an dessen Ende an, statt es als eigenes Segment stehen zu lassen \u2014 ein Anf\xFChrungszeichen darf NIE ein eigenes Segment f\xFCr sich bilden.
- Nur wenn ein offenes \xBB wirklich am Ende des Auszugs steht und danach KEINE Erz\xE4hlerhandlung/Inquit-Formel mehr folgt, behandle den Text ab \xBB bis Textende als 'dialogue'.
- Anf\xFChrungszeichen geh\xF6ren IMMER zum Dialog-Segment, NIEMALS zum Narration-Segment davor oder danach: Das schlie\xDFende \xAB am Ende einer Figurenrede geh\xF6rt ans ENDE des dialogue-Segments, nicht an den Anfang des folgenden narration-Segments. Das \xF6ffnende \xBB am Anfang einer Figurenrede geh\xF6rt an den ANFANG des dialogue-Segments, nicht ans Ende des vorherigen narration-Segments. Ein narration-Segment darf NIE mit einem einzelnen \xBB, \u201E, \u201A oder \u203A beginnen oder enden, und NIE mit einem einzelnen \xAB, ", ' oder \u2039 enden oder beginnen \u2014 verschiebe das Zeichen ins richtige Nachbar-Segment.
- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!`,normalizeCastingPrompt=prompt=>{let p=prompt||AB_DEFAULT_PROMPT;if(p=p.replace("- Wenn ein Auszug mit einem offenen \xBB-Zitat endet (kein schlie\xDFendes \xAB), behandle den Text ab \xBB bis Textende als 'dialogue'.",`- PDF-/OCR-SCHUTZ: Wenn ein \xBB oder \xAB offensichtlich fehlt, darf dieser eine Fehler NICHT den Rest der Passage als Dialog verschlucken. Schlie\xDFe ein offenes \xBB-Zitat am ersten plausiblen Satzende (? ! .), besonders wenn danach eine Inquit-Formel folgt ("fl\xFCsterte er", "sagte sie", "rief Uriens") oder normale Erz\xE4hlerhandlung weitergeht.
- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und entferne das einzelne \xAB aus dem ausgegebenen Text.
- Nur wenn ein offenes \xBB wirklich am Ende des Auszugs steht und danach KEINE Erz\xE4hlerhandlung/Inquit-Formel mehr folgt, behandle den Text ab \xBB bis Textende als 'dialogue'.`),p=p.replace("- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' ENTFERNST du die umschlie\xDFenden Anf\xFChrungszeichen.","- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' ENTFERNST du die umschlie\xDFenden Anf\xFChrungszeichen vollst\xE4ndig (nie nur ein einzelnes \xBB oder \xAB stehen lassen)."),p=p.replace("- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren!","- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren \u2014 abgesehen von entfernten \xE4u\xDFeren Dialog-Anf\xFChrungszeichen!"),p=p.replace("- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' ENTFERNST du die umschlie\xDFenden Anf\xFChrungszeichen vollst\xE4ndig (nie nur ein einzelnes \xBB oder \xAB stehen lassen).","- text: Der EXAKTE, wortw\xF6rtliche Text aus dem Auszug. Bei 'dialogue' BEH\xC4LTST du die umschlie\xDFenden Anf\xFChrungszeichen als Teil des Texts (z.B. \xBBHallo!\xAB bleibt \xBBHallo!\xAB) \u2014 entferne sie NICHT und lasse niemals nur eines der beiden \xFCbrig."),p=p.replace('- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und entferne das einzelne \xAB aus dem ausgegebenen Text.','- Wenn nur ein schlie\xDFendes \xAB nach einem kurzen Satz steht (z.B. "Der Tod tr\xE4gt rot. \xAB"), behandle den Satz davor als Dialog und h\xE4nge das schlie\xDFende \xAB an dessen Ende an, statt es als eigenes Segment stehen zu lassen \u2014 ein Anf\xFChrungszeichen darf NIE ein eigenes Segment f\xFCr sich bilden.'),p=p.replace("- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren \u2014 abgesehen von entfernten \xE4u\xDFeren Dialog-Anf\xFChrungszeichen!","- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!"),/Doppelpunkt-Regel/.test(p)||(p=p.replace(`ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv):
1. Direkte Zuordnung: Achte auf W\xF6rter wie "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) zum tats\xE4chlichen Namen auf.
2. Handlungs-Hinweise (Action Beats): Wenn ein Charakter eine Handlung ausf\xFChrt und direkt davor/danach w\xF6rtliche Rede steht, spricht meist dieser Charakter (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB").
3. Das Ping-Pong-Prinzip: Wenn zwei Personen sprechen, wechseln sie sich ab. Verfolge diese Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung.
4. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt die Aussage zum Wissen oder Tonfall eines bestimmten Charakters?
5. Wiederverwendung: Nutze EXAKT die Namen aus der Liste der bekannten Charaktere, FALLS der Name dort aufgef\xFChrt ist. Wenn ein neuer Charakter spricht, extrahiere seinen Namen direkt aus dem Text (z.B. 'Karyla', 'Uriens').
6. Unbekannte Sprecher: Nur wenn eine Zuordnung durch Kontext absolut nicht m\xF6glich ist, verwende 'Unknown'. Rate nicht blind, aber bevorzuge immer einen namentlich genannten Charakter gegen\xFCber 'Unknown'.`,`ANALYSE-REGELN F\xDCR DIE ZUORDNUNG DES SPRECHERS (Sei deduktiv \u2014 arbeite wie ein Lektor):
1. Direkte Zuordnung: "sagte [Name]", "fragte er", "rief sie". L\xF6se Pronomen (er/sie) IMMER zum tats\xE4chlichen Namen auf: Das Pronomen meint die zuletzt genannte Person passenden Geschlechts.
2. Doppelpunkt-Regel: Endet ein Erz\xE4hlersatz mit ":", spricht das Subjekt dieses Satzes das folgende Zitat (z.B. "Dann richtete er sich auf und rief in die Runde:" \u2192 der zuvor genannte Charakter spricht das n\xE4chste Zitat; "Ein anderer fragte verschlafen:" \u2192 dieser andere spricht).
3. Nachgestellte Zuordnung: Der Erz\xE4hlersatz NACH einem Zitat verr\xE4t oft den Sprecher \u2014 auch bei unpers\xF6nlicher Formel: "\xBBWas machst du denn da?\xAB ert\xF6nte es \xFCber ihm. Karyla hatte ihren Hammer weggelegt und war her\xFCbergekommen." \u2192 Karyla sprach das Zitat.
4. Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einem Zitat AKTIV HANDELT (tritt vor, dreht sich, hebt die Hand), spricht meist \u2014 ABER NUR bei Handlungsverben. Verben der WAHRNEHMUNG oder REAKTION (sp\xFCrte, h\xF6rte, f\xFChlte, sah, blickte, lauschte, bemerkte, nickte) kennzeichnen den ZUH\xD6RER, NICHT den Sprecher (z.B. "Thomas trat ans Fenster. \xBBEs regnet.\xAB"; "Ver\xE4chtlich sah sie ihn an. \xBBDas wirst du schon noch merken.\xAB" \u2192 sie spricht, und "sie" ist die zuletzt genannte Frau).
5. Adressaten-Regel: "X wandte sich an Y" / "X sah Y an" \u2192 X spricht das n\xE4chste Zitat, und Y ist der wahrscheinlichste Antwortende.
6. Das Ping-Pong-Prinzip: Sprechen zwei Personen miteinander, wechseln sie sich strikt ab \u2014 auch \xFCber viele Zitate OHNE Inquit-Formeln hinweg. Verfolge die Kette l\xFCckenlos zur\xFCck zur letzten eindeutigen Nennung und f\xFChre sie bis zum Szenenende fort. In einer Zwei-Personen-Szene ist 'Unknown' fast immer falsch.
7. Gruppen-Dialoge (3+ Personen): An wen richtet sich die Aussage? Passt sie zum Wissen oder Tonfall eines bestimmten Charakters?
8. Namenlose Sprecher: Auch Rollenbezeichnungen aus dem Text sind g\xFCltige Sprecher \u2014 nutze sie statt 'Unknown' (z.B. 'Ork', 'Der Fremde', 'Nachbar', 'W\xE4chter', 'Junge').
9. Wiederverwendung: Nutze EXAKT die Namen aus der Liste der bekannten Charaktere, FALLS der Name dort aufgef\xFChrt ist. Wenn ein neuer Charakter spricht, extrahiere seinen Namen direkt aus dem Text (z.B. 'Karyla', 'Uriens').
10. 'Unknown' NUR, wenn eine Zuordnung trotz aller obigen Regeln absolut unm\xF6glich ist. Bevorzuge immer einen genannten Charakter oder eine Rollenbezeichnung gegen\xFCber 'Unknown'.`)),/GRAMMATIK-REGELN FÜR NARRATION/.test(p)||(p=p.replace('Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).',`Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).
GRAMMATIK-REGELN F\xDCR NARRATION:
- Inquit-Formeln / Sprecher-Tags sind IMMER narration: finite Sprechverben wie sagte, fragte, rief, entgegnete, murmelte, fl\xFCsterte, schrie, antwortete + Subjekt/Pronomen/Name (z.B. "murmelte er mit erstickter Stimme.", ", entgegnete Marcian kalt.").
- Action Beats sind IMMER narration: Ein Charakter handelt, blickt, geht, lacht, schweigt, hebt die Hand, dreht sich um, usw. \u2014 auch wenn direkt davor/danach Dialog steht.
- Grammatische Probe: Wenn der Text eine Erz\xE4hler-Aussage \xDCBER das Sprechen ist (Verb + Sprecher + Art und Weise), ist es narration. Nur die tats\xE4chlich ge\xE4u\xDFerten W\xF6rter innerhalb der Anf\xFChrungszeichen sind dialogue.
- Satzfragmente mit f\xFChrendem Komma/Punkt wie ", sagte sie leise." oder ". fragte Uriens." sind niemals eigenst\xE4ndige Dialoge; sie geh\xF6ren als narration zum Erz\xE4hlertext.`)),/ABSATZ- UND KAPITELSTRUKTUR/.test(p)||(p=p.replace('Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).',`Markiere Text NIEMALS als Dialog, nur weil der Name eines Charakters erw\xE4hnt wird! (z.B. "Karyla rannte zur T\xFCr." ist Narration, KEIN Dialog).
ABSATZ- UND KAPITELSTRUKTUR:
Eine Leerzeile im Text markiert einen echten Absatzwechsel (oder einen Kapitel-/Szenenanfang). Eine sehr kurze, alleinstehende Zeile direkt vor einer Leerzeile (z.B. "1. Kapitel", "Prolog", ein Zahlwort) ist eine Kapitel\xFCberschrift \u2014 immer 'narration'/'Narrator', niemals Dialog. Behalte Leerzeilen als eigenst\xE4ndige narration-Segmente oder als Teil des umgebenden Erz\xE4hler-Segments bei; erfinde daraus keinen Dialog und l\xF6sche sie nicht aus dem rekonstruierten Text.`)),!/Gedankenstrich-Pause-Regel/.test(p)||!/ZUHÖRER-AUSSCHLUSS/.test(p)||!/FORTSETZUNGS-REGEL/.test(p)||!/EPITHETA/.test(p)){const newRules=`11. Gedankenstrich-Pause-Regel: Ein " - " MITTEN in einem Zitat ist eine Sprechpause DESSELBEN Sprechers, kein Zitatende \u2014 die Rede geht danach unver\xE4ndert weiter, bis das tats\xE4chliche schlie\xDFende Anf\xFChrungszeichen erscheint.
12. Stimm-Ank\xFCndigung: Erw\xE4hnt ein Erz\xE4hlersatz kurz vor einer noch nicht zugeordneten Zeile explizit die Stimme oder das beginnende Sprechen einer bestimmten Person (z.B. "Marcians Stimme wirkte nicht mehr so fest", "X setzte zum Sprechen an"), geh\xF6rt diese Zeile dieser Person \u2014 nicht 'Unknown'.
13. ZUH\xD6RER-AUSSCHLUSS (negative Evidenz \u2014 sehr wichtig): Ein Name im Erz\xE4hlsatz neben einem Zitat ist H\xC4UFIG der Zuh\xF6rer, nicht der Sprecher. Schlie\xDFe eine Person als Sprecher AUS, wenn sie (a) im Nachbarsatz wahrnimmt oder reagiert (Marcian sp\xFCrte ihre Tr\xE4nen auf seiner Brust; Marcian h\xF6rte dar\xFCber hinweg \u2192 Marcian H\xD6RT ZU, jemand anderes spricht), oder (b) im Zitat selbst ANGEREDET wird (Vokativ nach Komma: M\xF6gen die G\xF6tter dich sch\xFCtzen, Marcian. \u2192 Marcian ist der ANGESPROCHENE). Wer angeredet wird, spricht nicht. Wenn durch diesen Ausschluss kein Kandidat mehr \xFCbrig bleibt, suche WEITER ZUR\xDCCK (letzte eindeutige Nennung, Ping-Pong-Kette, wer in der Szene anwesend ist) \u2014 der Ausschluss grenzt die Kandidaten nur ein und rechtfertigt f\xFCr sich allein NIEMALS 'Unknown'.
16. FORTSETZUNGS-REGEL (wichtiger als Ping-Pong): Beschreibt der Erzaehlsatz ZWISCHEN zwei Zitaten dieselbe Person, die gerade gesprochen hat (per Pronomen oder Name, z.B. "Leise schluchzte sie.", "Er atmete tief durch."), dann spricht diese Person WEITER \u2014 das folgende Zitat gehoert IHR, nicht dem Gespraechspartner. Das Ping-Pong-Prinzip (Regel 6) gilt NUR, wenn der Erzaehlsatz auf die ANDERE Person zeigt oder gar keinen Hinweis enthaelt. Beispiel: Cindira spricht, dann "Leise schluchzte sie." \u2192 das naechste Zitat ist wieder CINDIRA; ein folgendes "Marcian spuerte ihre Traenen" bestaetigt nur, dass Marcian zuhoert (Regel 13).
17. NEU AUFTRETENDE, UNBENANNTE HANDELNDE: Fuehrt der Erzaehlsatz unmittelbar vor einem Zitat eine unbenannte Person handelnd ein ("Eine Frau durchbrach die Kette der Krieger und warf sich...", "Ein Soldat trat vor"), dann spricht GENAU DIESE Person. Verwende die Rollenbezeichnung aus dem Text als Sprechernamen ("Eine Frau", "Ein Soldat") \u2014 niemals 'Unknown'. Spricht dieselbe Person spaeter erneut (erkennbar am Pronomen im Erzaehlsatz), nutze exakt DENSELBEN Rollennamen weiter, damit sie eine durchgehende Stimme behaelt.
18. EPITHETA / BESTIMMTE BESCHREIBUNGEN als Sprecher: Bezeichnungen wie "die Elfe", "der Zauberer", "der Zwerg", "der Hauptmann" meinen fast immer eine BEREITS BEKANNTE Figur. Loese sie auf den kanonischen Namen auf, sobald der Text die Verbindung herstellt (Nyrilla ist die Elfe \u2192 "Laut hallte die Stimme der Elfe durch den Gang." bedeutet: NYRILLA hat gesprochen). Nutze immer den kanonischen Namen als Sprecher, niemals das Epitheton und niemals 'Unknown', damit die Figur durchgehend dieselbe Stimme behaelt.
19. GEMISCHTE SEGMENTE TRENNEN: Enthaelt ein Segment sowohl Erzaehltext als auch ein Zitat ("Nyrilla richtete sich auf. Alle anderen wichen zurueck. \xBBIch werde jetzt ...\xAB"), dann zerlege es: der Erzaehlteil wird ein eigenes Segment mit speaker 'Narrator' und type 'narration', das Zitat ein eigenes Segment mit type 'dialogue' und der handelnden Person als Sprecher (hier NYRILLA, denn sie richtet sich unmittelbar davor auf \u2014 siehe Regel 4). Ein gemischtes Segment darf niemals als Ganzes 'Unknown' bleiben. ZERLEGE EBENSO Segmente, die REDEBEITRAEGE MEHRERER SPRECHER enthalten: Ein Befehl, der jemanden beim Namen anspricht, und die darauffolgende Antwort sind ZWEI Sprecher, nicht einer (z.B. "Bringt Fackeln ... Lysandra, du nimmst dir einige Krieger ..." = MARCIAN befiehlt; "Mit Vergn\xFCgen, ich hab doch gleich gesagt ..." = LYSANDRA antwortet). Merkmal: Wer im ersten Teil per Vokativ ANGESPROCHEN wird (Regel 13b), ist typischerweise der Sprecher des NAECHSTEN Redebeitrags. Lege fuer jeden Redebeitrag ein eigenes Segment mit dem jeweiligen Sprecher an, statt den ganzen Block als 'Unknown' zu lassen.
14. GETEILTE ZITATE / Pronomen-Inquit: Beginnt ein Segment mit Komma oder Kleinbuchstaben plus Sprechverb und Pronomen (, antwortete er knapp. / , murmelte sie leise.), ist es die abgetrennte Inquit-Formel des DAVORSTEHENDEN Zitats. Ordne jenes Zitat der Person zu, auf die das Pronomen zeigt: die zuletzt genannte Person passenden Geschlechts VOR dem Zitat (er \u2192 letzter Mann, sie \u2192 letzte Frau). Solche Zeilen d\xFCrfen niemals 'Unknown' bleiben.
15. MEHRERE KANDIDATEN in der N\xE4he: Stehen mehrere bekannte Namen im Umfeld, entscheide in dieser Reihenfolge: (a) streiche alle nach Regel 13 ausgeschlossenen (Zuh\xF6rer/Angeredete); (b) bevorzuge, wer grammatisches SUBJEKT eines Sprech- oder Handlungsverbs ist, gegen\xFCber jemandem in Objekt- oder Pr\xE4positionalstellung; (c) pr\xFCfe, wessen Wissensstand, Rolle und Sprechweise zum Inhalt passt; (d) nutze das Ping-Pong-Prinzip aus Regel 6. Erst wenn danach zwei gleichwertige Kandidaten bleiben, ist 'Unknown' zul\xE4ssig.`,unknownRuleRe=/(\n\d+\. 'Unknown' NUR,[^\n]*'Unknown'\.)/;unknownRuleRe.test(p)?p=p.replace(unknownRuleRe,`$1
${newRules}`):p+=`
${newRules}`}return/gehören IMMER zum Dialog-Segment/.test(p)||(p=p.replace("- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!",`- Anf\xFChrungszeichen geh\xF6ren IMMER zum Dialog-Segment, NIEMALS zum Narration-Segment davor oder danach: Das schlie\xDFende \xAB am Ende einer Figurenrede geh\xF6rt ans ENDE des dialogue-Segments, nicht an den Anfang des folgenden narration-Segments. Das \xF6ffnende \xBB am Anfang einer Figurenrede geh\xF6rt an den ANFANG des dialogue-Segments, nicht ans Ende des vorherigen narration-Segments. Ein narration-Segment darf NIE mit einem einzelnen \xBB, \u201E, \u201A oder \u203A beginnen oder enden, und NIE mit einem einzelnen \xAB, ", ' oder \u2039 enden oder beginnen \u2014 verschiebe das Zeichen ins richtige Nachbar-Segment.
- Lasse NIEMALS W\xF6rter aus, fasse nicht zusammen, dupliziere nichts und erfinde keinen Text. Der kombinierte Text all deiner Segmente MUSS den Originaltext exakt und l\xFCckenlos Wort f\xFCr Wort rekonstruieren, EINSCHLIESSLICH der Anf\xFChrungszeichen!`)),p},rawPrompt=typeof _appSettings!="undefined"&&_appSettings.audiobook_prompt?_appSettings.audiobook_prompt:AB_DEFAULT_PROMPT,globalPrompt=normalizeCastingPrompt(rawPrompt);globalPrompt!==rawPrompt&&typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=globalPrompt,fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:globalPrompt})}).catch(()=>{})),panel.querySelector("#ab-cv-prompt-text").value=globalPrompt;const closePanel=()=>{if(panel.hidden=!0,panel.innerHTML="",typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1),typeof window.navReaderView=="function")window.navReaderView("main");else if(typeof window.showReaderView=="function")window.showReaderView("main");else{const mv=document.getElementById("reader-main-view");mv&&(mv.hidden=!1)}},setStoppingState=()=>{const cancelBtn=panel.querySelector("#ab-cv-cancel");cancelBtn&&(cancelBtn.disabled=!0,cancelBtn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span> Stopping...',cancelBtn.title="Stopping the current casting request");const status2=panel.querySelector("#ab-cv-status-msg");status2&&(status2.style.display="inline-block",status2.textContent="Stopping... completed passages will stay saved.")};panel.querySelector("#ab-cv-cancel").addEventListener("click",()=>{if(_audiobook.running){_audiobook.cancel=!0,setStoppingState(),typeof _audiobook.abort=="function"&&_audiobook.abort();return}closePanel()}),panel.querySelector("#ab-cv-prompt-btn").addEventListener("click",()=>{const p=panel.querySelector("#ab-cv-prompt-panel");p.hidden=!p.hidden;const chevron=panel.querySelector("#ab-cv-prompt-chevron");chevron&&(chevron.className=p.hidden?"mdi mdi-chevron-down":"mdi mdi-chevron-up")}),function(){const tBtn=panel.querySelector("#ab-cv-settings-toggle"),sBox=panel.querySelector("#ab-cv-settings"),chev=panel.querySelector("#ab-cv-settings-chevron");function apply(collapsed2){if(sBox&&(sBox.style.display=collapsed2?"none":"flex"),collapsed2){const p=panel.querySelector("#ab-cv-prompt-panel");p&&(p.hidden=!0)}chev&&(chev.className="mdi "+(collapsed2?"mdi-chevron-down":"mdi-chevron-up"))}let collapsed=!1;try{collapsed=localStorage.getItem("ttsvc_ab_settings_collapsed")==="1"}catch{}apply(collapsed),tBtn&&tBtn.addEventListener("click",()=>{collapsed=!collapsed;try{localStorage.setItem("ttsvc_ab_settings_collapsed",collapsed?"1":"0")}catch{}apply(collapsed)})}(),function(){const side=panel.querySelector("#ab-cv-side"),body=panel.querySelector(".ab-cv-body"),cBtn=panel.querySelector("#ab-cv-side-collapse");function apply(collapsed2){side&&side.classList.toggle("is-collapsed",collapsed2),body&&body.classList.toggle("side-collapsed",collapsed2),cBtn&&(cBtn.querySelector(".mdi").className="mdi "+(collapsed2?"mdi-chevron-left":"mdi-chevron-right")),cBtn&&(cBtn.title=collapsed2?"Expand character list":"Collapse to avatars")}let collapsed=!1;try{collapsed=localStorage.getItem("ttsvc_ab_side_collapsed")==="1"}catch{}apply(collapsed),cBtn&&cBtn.addEventListener("click",()=>{collapsed=!collapsed;try{localStorage.setItem("ttsvc_ab_side_collapsed",collapsed?"1":"0")}catch{}apply(collapsed)})}();let savedPrompts=[];try{savedPrompts=JSON.parse(localStorage.getItem("ttsvc_ab_prompts")||"[]")}catch{savedPrompts=[]}const libSelect=panel.querySelector("#ab-cv-prompt-lib"),delBtn=panel.querySelector("#ab-cv-prompt-del"),promptText=panel.querySelector("#ab-cv-prompt-text"),renderPromptLib=(selectedIdx=-1)=>{libSelect.innerHTML='<option value="">\u2014 Load saved prompt \u2014</option>'+savedPrompts.map((p,i)=>`<option value="${i}">${escHtml(p.name)}</option>`).join(""),selectedIdx>=0?(libSelect.value=selectedIdx,delBtn.style.display="block"):(libSelect.value="",delBtn.style.display="none")};renderPromptLib(),libSelect.addEventListener("change",()=>{const idx=parseInt(libSelect.value),nameInput=panel.querySelector("#ab-cv-prompt-name");!isNaN(idx)&&savedPrompts[idx]?(promptText.value=savedPrompts[idx].prompt,nameInput&&(nameInput.value=savedPrompts[idx].name),delBtn.style.display="block"):(nameInput&&(nameInput.value=""),delBtn.style.display="none")}),delBtn.addEventListener("click",()=>{const idx=parseInt(libSelect.value);isNaN(idx)||confirm("Delete this saved prompt preset?")&&(savedPrompts.splice(idx,1),localStorage.setItem("ttsvc_ab_prompts",JSON.stringify(savedPrompts)),renderPromptLib(),toast("Prompt deleted","success"))}),panel.querySelector("#ab-cv-prompt-save").addEventListener("click",async()=>{const val=promptText.value.trim();if(!val){toast("Prompt is empty","error");return}const name=panel.querySelector("#ab-cv-prompt-name").value.trim()||"Custom Prompt "+(savedPrompts.length+1);let targetIdx=parseInt(libSelect.value);!isNaN(targetIdx)&&savedPrompts[targetIdx]&&savedPrompts[targetIdx].name===name?savedPrompts[targetIdx].prompt=val:(savedPrompts.push({name,prompt:val}),targetIdx=savedPrompts.length-1),localStorage.setItem("ttsvc_ab_prompts",JSON.stringify(savedPrompts)),renderPromptLib(targetIdx),toast("Prompt preset saved","success"),typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=val);try{await fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:val})})}catch{}}),(()=>{const localSel=panel.querySelector("#ab-cv-llm-url"),globalSel=document.getElementById("llm-active-url");if(globalSel&&localSel){const currentVal=localSel.value;localSel.innerHTML=globalSel.innerHTML,localSel.value=currentVal||globalSel.value||""}})();const loadModels=()=>{const urlInput2=panel.querySelector("#ab-cv-llm-url"),sel=panel.querySelector("#ab-cv-llm-select");if(!sel||!urlInput2)return;const currentUrl=urlInput2.value.trim(),oldVal=sel.value;sel.innerHTML='<option value="">loading...</option>',fetch("/api/conversation/llm-models"+(currentUrl?"?url="+encodeURIComponent(currentUrl):"")).then(r=>r.json()).then(d=>{if(d.models&&d.models.length){const preferred=audiobookSafeLlmModel(oldVal||defaultModel);sel.innerHTML=d.models.map(m=>`<option value="${escHtml(m)}">${escHtml(m)}</option>`).join(""),preferred&&d.models.includes(preferred)?sel.value=preferred:oldVal&&d.models.includes(oldVal)&&!audiobookIsRouterModel(oldVal)?sel.value=oldVal:d.models.includes(defaultModel)&&(sel.value=defaultModel)}else sel.innerHTML=`<option value="${escHtml(defaultModel)}">${escHtml(defaultModel||"\u2014 fetch models \u2014")}</option>`}).catch(()=>{sel.innerHTML=`<option value="${escHtml(defaultModel)}">${escHtml(defaultModel||"\u2014 fetch models \u2014")}</option>`})};loadModels(),panel.querySelector("#ab-cv-llm-refresh").addEventListener("click",loadModels);const urlInput=panel.querySelector("#ab-cv-llm-url");urlInput&&urlInput.addEventListener("change",loadModels);const applyPromptAndRun=callback=>{const newPrompt=panel.querySelector("#ab-cv-prompt-text").value,choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt})}).finally(()=>{callback&&callback(savedChoice.url,savedChoice.model)})};if(isIdle){const hasSegs=_audiobook.segments&&_audiobook.segments.length>0;if(panel.querySelector("#ab-cv-status-msg").style.display="inline-block",hasSegs){const recastUnkBtn=panel.querySelector("#ab-cv-start-recast-unk"),recastBtn=panel.querySelector("#ab-cv-start-recast");recastUnkBtn.style.display="inline-block",recastBtn.style.display="inline-block",recastUnkBtn.addEventListener("click",()=>applyPromptAndRun(audiobookRecastUnknown)),recastBtn.addEventListener("click",async()=>{await confirmDialog("Start from scratch? This will discard the current cast for this book and rebuild the character definitions from the text.",{title:"Discard current cast?",okLabel:"Start from scratch",danger:!0})&&applyPromptAndRun(audiobookCast)}),panel.querySelector("#ab-cv-status-msg").textContent="Ready to identify characters."}else{const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn.style.display="inline-block",castBtn.addEventListener("click",()=>applyPromptAndRun(audiobookCast))}}else panel.querySelector("#ab-cv-cancel").innerHTML='<span class="mdi mdi-stop"></span> Stop Casting';const fill=panel.querySelector("#ab-cv-fill"),count=panel.querySelector("#ab-cv-count"),feed=panel.querySelector("#ab-cv-feed"),chars=panel.querySelector("#ab-cv-chars"),roster=new Map,characterRecords=new Map,identityNames=recOrSheet=>{const s=(recOrSheet==null?void 0:recOrSheet.sheet)||recOrSheet||{},out=new Set,add=(v,opts={})=>clSplitIdentityTokens(v,opts).forEach(x=>out.add(x));return add((recOrSheet==null?void 0:recOrSheet.name)||s.name),["aliases","first_name","last_name","full_name","title"].forEach(k=>add(s[k],{aliases:k==="aliases"})),[...out]};let _hlVer=0,_hlCache={ver:-1,rosterSize:-1,regex:null,byLower:new Map};const registerCharacterRecord=rec=>{if(rec!=null&&rec.name){_hlVer++;for(const n of identityNames(rec))characterRecords.set(n.toLowerCase(),rec)}},recordForName=name=>characterRecords.get(String(name||"").toLowerCase())||null,colorFor=(name,rec)=>{const key=String(name||"").toLowerCase(),stored=rec||characterRecords.get(key),color=stored?_abRecordColor(stored,name):_abDefaultCharacterColor(name);return roster.has(name)?roster.get(name).color=color:roster.set(name,{count:0,color}),roster.get(name).color},_abAvatarHtml=(name,color)=>{var _a3;const img=(_a3=recordForName(name))==null?void 0:_a3.image,c=color||colorFor(name);return img?`<span class="ab-char-dot ab-char-dot-img" style="border-color:${c}"><img src="${escHtml(img)}" alt=""></span>`:`<span class="ab-char-dot" style="background:${c}">${escHtml((name||"?")[0].toUpperCase())}</span>`},setCharacterColor=(name,color)=>{const safe=_abNormalizeColor(color,name),info=roster.get(name);info&&(info.color=safe);const key=String(name||"").toLowerCase(),rec=characterRecords.get(key);return rec&&(rec.color=safe,rec.sheet&&(rec.sheet.color=safe)),safe},highlightText2=text=>{if(!text)return"";let html=escHtml(text);if(_hlCache.ver!==_hlVer||_hlCache.rosterSize!==roster.size){const extraNames=[];new Set([...characterRecords.values()]).forEach(rec=>extraNames.push(...identityNames(rec)));const seen=new Set,names=[];for(const n of[...roster.keys(),...extraNames]){const k=n.toLowerCase();n.length<2||k==="narrator"||/^unknown|unbekannt/i.test(k)||seen.has(k)||(seen.add(k),names.push(n))}names.sort((a,b)=>b.length-a.length);const pattern=names.map(n=>n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")).join("|");_hlCache={ver:_hlVer,rosterSize:roster.size,regex:pattern?new RegExp(`\\b(${pattern})\\b`,"gi"):null,byLower:new Map(names.map(n=>[n.toLowerCase(),n]))}}return _hlCache.regex&&(html=html.replace(_hlCache.regex,match=>{const canon=_hlCache.byLower.get(match.toLowerCase())||match;return`<span class="ab-name-hit" data-name="${escHtml(canon)}" style="border-bottom: 2px solid ${colorFor(canon)}; font-weight: 600;">${match}</span>`})),html},feedWrap=panel.querySelector(".ab-cv-feed-wrap"),jumpBtn=panel.querySelector("#ab-cv-jump-btn"),_abTopbar=document.createElement("div");_abTopbar.className="ab-cv-topbar",feedWrap.insertBefore(_abTopbar,feedWrap.firstChild);const _abBar=document.createElement("div");_abBar.className="ab-char-bar",_abBar.hidden=!0,_abTopbar.appendChild(_abBar);const _abEditToolbar=document.createElement("div");_abEditToolbar.className="ab-edit-toolbar",_abEditToolbar.innerHTML=`
<button class="ab-edit-btn ab-edit-undo" type="button" title="Undo last cast edit" disabled><span class="mdi mdi-undo"></span></button>
<button class="ab-edit-btn ab-edit-redo" type="button" title="Redo cast edit" disabled><span class="mdi mdi-redo"></span></button>`,_abTopbar.appendChild(_abEditToolbar);const _abPageNav=document.createElement("div");_abPageNav.className="ab-page-nav",_abPageNav.hidden=!0,_abPageNav.innerHTML=`
<button class="ab-page-nav-btn ab-page-prev" type="button" title="Previous cast page"><span class="mdi mdi-chevron-left"></span></button>
<select class="ab-page-select" aria-label="Jump to cast page"></select>
<button class="ab-page-nav-btn ab-page-next" type="button" title="Next cast page"><span class="mdi mdi-chevron-right"></span></button>
<button class="ab-page-nav-btn ab-page-source" type="button" title="Back to this source page in Reader"><span class="mdi mdi-book-open-page-variant"></span></button>`,_abTopbar.appendChild(_abPageNav);const _abFontCtl=document.createElement("div");_abFontCtl.className="ab-font-ctl",_abFontCtl.innerHTML=`
<button class="ab-page-nav-btn ab-font-dec" type="button" title="Decrease text size">A\u2212</button>
<button class="ab-page-nav-btn ab-font-inc" type="button" title="Increase text size">A+</button>`,_abTopbar.appendChild(_abFontCtl);const _abApplyFont=()=>{let v=1;try{v=parseFloat(localStorage.getItem("ttsvc_reh_stage_scale"))||1}catch{}feed.style.setProperty("--reh-stage-scale",String(v))},_abFontStep=d=>{let v=1;try{v=parseFloat(localStorage.getItem("ttsvc_reh_stage_scale"))||1}catch{}v=Math.round(Math.min(2,Math.max(.7,v+d))*100)/100;try{localStorage.setItem("ttsvc_reh_stage_scale",String(v))}catch{}_abApplyFont(),typeof rehApplyStageFont=="function"&&rehApplyStageFont()};_abFontCtl.querySelector(".ab-font-dec").addEventListener("click",()=>_abFontStep(-.1)),_abFontCtl.querySelector(".ab-font-inc").addEventListener("click",()=>_abFontStep(.1)),_abApplyFont();let _abCharRows=[],_abNavIdx=0,_abDetailEl=null,_abCurrentPageNum=1;const _abClearHL=()=>{feed.querySelectorAll(".ab-cv-row.ab-char-hl, .ab-cv-row.ab-char-focus").forEach(r=>r.classList.remove("ab-char-hl","ab-char-focus"))},_abCloseProfile=()=>{_abDetailEl&&(_abDetailEl.remove(),_abDetailEl=null),feed.style.display="",jumpBtn&&typeof _userScrolled!="undefined"&&(jumpBtn.hidden=!_userScrolled),_abCharRows.forEach(r=>r.classList.add("ab-char-hl"));const btn=_abBar.querySelector(".ab-char-bar-profile");btn&&(btn.innerHTML='<span class="mdi mdi-account-details-outline"></span> Profil')},_abCloseBar=()=>{_abBar.hidden=!0,_abBar.innerHTML="",_abCloseProfile(),_abClearHL(),_abCharRows=[],chars.querySelectorAll(".ab-char-item").forEach(el=>el.classList.remove("is-active")),typeof _abSyncTopbar=="function"&&_abSyncTopbar()},_abNav=(dir,pool)=>{const rows=pool||_abCharRows;if(!rows.length)return;_abNavIdx=(_abNavIdx+dir+rows.length)%rows.length,feed.querySelectorAll(".ab-cv-row.ab-char-focus").forEach(r=>r.classList.remove("ab-char-focus")),rows[_abNavIdx].classList.add("ab-char-focus"),_abScrollIntoView(rows[_abNavIdx],{block:"center"});const pos=_abBar.querySelector(".ab-char-bar-pos");pos&&(pos.textContent=_abNavIdx+1+" / "+rows.length)},_abRefreshCharacterColors=name=>{const names=name?[name]:[...roster.keys()];for(const n of names)colorFor(n);feed.querySelectorAll(".ab-cv-row").forEach(row=>{const seg=row.__seg;if(!seg)return;const speakerName=seg.type!=="dialogue"||!seg.speaker||seg.speaker.toLowerCase()==="narrator"?"Narrator":seg.speaker,c=colorFor(speakerName),spk=row.querySelector(".ab-cv-spk"),txt=row.querySelector(".ab-cv-txt");spk&&(!name||speakerName.toLowerCase()===name.toLowerCase())&&(spk.style.color=c),txt&&(txt.innerHTML=highlightText2(seg.text||""))}),renderRoster();const selected=_abBar.hidden?null:_abBar.dataset.charName;if(selected){const dot=_abBar.querySelector(".ab-char-bar-dot");dot&&(dot.style.background=colorFor(selected))}},_abShowProfile=async name=>{var _a3,_b3,_c3,_d3,_e3,_f3,_g2;_abCloseProfile(),feed.style.display="none",jumpBtn&&(jumpBtn.hidden=!0);const profileBtn=_abBar.querySelector(".ab-char-bar-profile");profileBtn&&(profileBtn.innerHTML='<span class="mdi mdi-arrow-left"></span> Skript');const title=((_a3=window.readerState)==null?void 0:_a3.title)||"";let rec=null;try{const all=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(title):typeof clGetAll=="function"?await clGetAll():[];for(const r of all||[])registerCharacterRecord(r);rec=recordForName(name)}catch{}const detail=document.createElement("div");if(detail.className="ab-char-detail-panel",_abDetailEl=detail,!rec){detail.innerHTML='<button class="ab-char-detail-back"><span class="mdi mdi-arrow-left"></span> Zur\xFCck zum Skript</button><div style="padding:24px;opacity:.6;font-size:14px">Kein Charakterblatt \u2013 zuerst \u201ECast all character roles\u201C ausf\xFChren.</div>',feedWrap.appendChild(detail),detail.querySelector(".ab-char-detail-back").addEventListener("click",_abCloseProfile);return}registerCharacterRecord(rec);const sh=rec.sheet||{},charColor=setCharacterColor(rec.name,rec.color||sh.color||colorFor(rec.name,rec)),accent2=clHslToHex((clNameHue(rec.name)+40)%360,58,30),tier=String(sh.tier||"").toLowerCase(),tierLabel=tier==="main"?"Hauptcharakter":tier==="supporting"?"Nebencharakter":"Nebenfigur",voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",lineCount=_abStr(sh.line_count),pct=((_b3=rec.sheet)==null?void 0:_b3.moral_alignment_score)!=null?Math.max(0,Math.min(100,rec.sheet.moral_alignment_score)):null,arcMap={"good-to-bad":"\u2198 Entwicklung zum B\xF6sen","bad-to-good":"\u2197 Wandel zum Guten",complex:"\u2195 Komplex","stable-good":"\u2192 Stabil gut","stable-bad":"\u2192 Stabil b\xF6se",neutral:"\u2192 Neutral"},avatarHtml=rec.image?`<div class="lcd-avatar"><img src="${rec.image}" alt="${escHtml(rec.name)}"></div>`:`<div class="lcd-avatar" style="background:${charColor}">${escHtml((rec.name||"?")[0].toUpperCase())}</div>`,field=(lbl,val,sk)=>{const v=_abStr(val);return!v&&!sk?"":`<div class="lcd-field"><div class="lcd-field-label">${lbl}</div><div class="lcd-field-value${sk?" ab-editable":""}"${sk?` data-sk="${sk}"`:""}>${escHtml(v)}</div></div>`},srcItems=Array.isArray(rec.sources)?rec.sources:[],sourcesHtml=srcItems.length?`<div class="lcd-section-full">
<div class="lcd-section-label"><span class="mdi mdi-map-marker-outline"></span> Quellen im Text</div>
<div class="lcd-sources">${srcItems.map(s=>`<span class="lcd-source-clickable ab-cd-src" data-page="${s.page||0}">Seite ${s.page||"?"}</span>`).join("")}</div>
</div>`:"";detail.innerHTML=`<button class="ab-char-detail-back"><span class="mdi mdi-arrow-left"></span> Zur\xFCck zum Skript</button>
<div class="lcd-header" style="--lc1:${charColor};--lc2:${accent2}; position:relative;">
${avatarHtml}
<div class="lcd-header-body">
<div class="lcd-name">${escHtml(rec.name)}</div>
${_abStr(sh.title)?`<div class="lcd-aliases">${escHtml(_abStr(sh.title))}</div>`:""}
${_abStr(sh.archetype)?`<div class="lcd-archetype">${escHtml(_abStr(sh.archetype))}</div>`:""}
<div class="lcd-tier-gender"><span class="lcd-tier">${tierLabel}</span>${lineCount?`<span class="lcd-linecount">${escHtml(lineCount)} lines</span>`:""}</div>
</div>
<div class="ab-cd-header-actions">
<button class="ab-cd-refine-btn" title="Refine this character only"><span class="mdi mdi-refresh"></span> Auto refine</button>
<button class="ab-cd-edit-btn" title="Charakterblatt bearbeiten"><span class="mdi mdi-pencil-outline"></span></button>
</div>
</div>
<div class="lcd-body ab-cd-body">
<div class="lcd-voice-top">
<div class="lcd-section-label"><span class="mdi mdi-account-voice"></span> Stimme</div>
<span class="lcd-voice-label">${voiceId?escHtml(voiceId):'<span style="opacity:.5">Noch keine Stimme</span>'}</span>
<button class="ab-cd-pick">Ausw\xE4hlen</button><button class="ab-cd-auto">Automatisch</button><button class="ab-cd-online">Online suchen</button>
<label class="ab-cd-color" title="Charakterfarbe \xE4ndern"><span class="mdi mdi-palette-outline"></span><input type="color" value="${charColor}" aria-label="Charakterfarbe"></label>
</div>
${pct!=null?`<div class="lcd-align-section"><div class="lcd-section-label"><span class="mdi mdi-scale-balance"></span> Moralische Gesinnung</div><div class="lcd-align-bar-wrap"><span class="lcd-align-label">B\xF6se</span><div class="lcd-align-bar"><div class="lcd-align-dot" style="left:${pct}%"></div></div><span class="lcd-align-label">Gut</span></div><div class="lcd-align-arc">${arcMap[sh.arc_direction||"neutral"]||"\u2192"} \xB7 ${pct>=70?"Rechtschaffen":pct<=30?"B\xF6se":"Ambivalent"} (${pct}/100)</div></div>`:""}
<div class="lcd-sections">
<div class="lcd-section"><div class="lcd-section-label"><span class="mdi mdi-card-account-details-outline"></span> Identit\xE4t</div>${field("Vorname",sh.first_name,"first_name")}${field("Nachname",sh.last_name,"last_name")}${field("Geschlecht",sh.gender,"gender")}${field("Titel",sh.title,"title")}${field("Beruf / Rolle",sh.profession,"profession")}${field("Auch bekannt als",sh.aliases,"aliases")}</div>
<div class="lcd-section"><div class="lcd-section-label"><span class="mdi mdi-account-outline"></span> Erscheinung</div>${field("K\xF6rperlich",sh.physical,"physical")}${field("Kleidung",sh.clothing,"clothing")}</div>
<div class="lcd-section"><div class="lcd-section-label"><span class="mdi mdi-drama-masks"></span> Pers\xF6nlichkeit</div>${field("Eigenheiten",sh.mannerisms,"mannerisms")}${field("Stimme & Sprache",sh.voice_pattern,"voice_pattern")}</div>
<div class="lcd-section"><div class="lcd-section-label"><span class="mdi mdi-book-open-outline"></span> Geschichte</div>${field("Hintergrund",sh.backstory,"backstory")}${field("Motivation",sh.motivation,"motivation")}</div>
<div class="lcd-section"><div class="lcd-section-label"><span class="mdi mdi-sword"></span> F\xE4higkeiten</div>${field("Fertigkeiten",sh.skills,"skills")}${field("Besondere F\xE4higkeiten",sh.capabilities,"capabilities")}</div>
<div class="lcd-section-full"><div class="lcd-section-label"><span class="mdi mdi-account-group-outline"></span> Beziehungen</div>${field("",sh.relationships,"relationships")}</div>
${sourcesHtml}
</div>
</div>`,feedWrap.appendChild(detail),detail.querySelector(".ab-char-detail-back").addEventListener("click",_abCloseProfile),(_c3=detail.querySelector(".ab-cd-color input"))==null||_c3.addEventListener("input",async e=>{var _a4;const color=setCharacterColor(rec.name,e.target.value);rec.color=color,rec.sheet||(rec.sheet={}),rec.sheet.color=color,(_a4=detail.querySelector(".lcd-header"))==null||_a4.style.setProperty("--lc1",color);const av=detail.querySelector(".lcd-avatar");av&&!av.querySelector("img")&&(av.style.background=color),_abRefreshCharacterColors(rec.name),clearTimeout(_audiobook._colorSaveTimer),_audiobook._colorSaveTimer=setTimeout(async()=>{try{typeof clPut=="function"&&await clPut(rec)}catch{}},400)}),(_d3=detail.querySelector(".ab-cd-pick"))==null||_d3.addEventListener("click",()=>{typeof _openVoicePicker=="function"&&_openVoicePicker(detail.querySelector(".lcd-voice-top"),rec,async()=>{const up=(typeof clGetAll=="function"?await clGetAll():[]).find(r=>r.id===rec.id);up&&(_abCloseProfile(),_abShowProfile(up.name))})}),(_e3=detail.querySelector(".ab-cd-auto"))==null||_e3.addEventListener("click",async()=>{if(typeof _autoAssignVoice=="function"){await _autoAssignVoice(rec);const up=(typeof clGetAll=="function"?await clGetAll():[]).find(r=>r.id===rec.id);up&&(_abCloseProfile(),_abShowProfile(up.name))}}),(_f3=detail.querySelector(".ab-cd-online"))==null||_f3.addEventListener("click",()=>{typeof _charSearchOnline=="function"&&_charSearchOnline(rec)}),(_g2=detail.querySelector(".ab-cd-refine-btn"))==null||_g2.addEventListener("click",async()=>{const name2=String(rec.name||"").trim();if(name2){if(typeof window.audiobookRecastSelectedCharacters=="function")return window.audiobookRecastSelectedCharacters([name2]);if(typeof window.csForReaderSelective=="function")return window.csForReaderSelective([name2]);toast("Character refinement is unavailable right now","error")}});const _saveTimers=new Map,editBtn=detail.querySelector(".ab-cd-edit-btn");editBtn==null||editBtn.addEventListener("click",function(){var _a4;const editing=detail.classList.toggle("ab-cd-editing");this.innerHTML=editing?'<span class="mdi mdi-check"></span>':'<span class="mdi mdi-pencil-outline"></span>',this.title=editing?"Fertig":"Charakterblatt bearbeiten",detail.querySelectorAll(".ab-editable").forEach(el=>{el.contentEditable=editing?"true":"false",editing&&el.addEventListener("input",function(){const sk=el.dataset.sk;clearTimeout(_saveTimers.get(sk)),_saveTimers.set(sk,setTimeout(async()=>{rec.sheet||(rec.sheet={}),rec.sheet[sk]=el.textContent.trim(),["aliases","first_name","last_name","full_name","title"].includes(sk)&&registerCharacterRecord(rec),typeof clPut=="function"&&await clPut(rec)},900))})}),editing&&((_a4=detail.querySelector(".ab-editable"))==null||_a4.focus())}),detail.querySelectorAll(".ab-cd-src").forEach(el=>{el.addEventListener("click",()=>{const pg=parseInt(el.dataset.page,10);pg&&(typeof navTo=="function"&&navTo("s-reader"),setTimeout(()=>{var _a4,_b4;const pages=(_a4=window.readerState)==null?void 0:_a4.pages;pages&&pages.length>=pg&&((_b4=pages[pg-1])!=null&&_b4.pageDiv)?_abScrollIntoView(pages[pg-1].pageDiv,{block:"start"}):typeof toast=="function"&&toast('Buch im \u201EVorlesen"-Bereich \xF6ffnen, dann nochmal klicken',"info")},300))})})},_abSelectChar=name=>{if(!_abBar.hidden&&_abBar.dataset.charName===name){_abCloseBar();return}_abCloseBar(),chars.querySelectorAll(".ab-char-item").forEach(el=>el.classList.toggle("is-active",el.dataset.name===name)),_abCharRows=Array.from(feed.querySelectorAll(".ab-cv-row")).filter(r=>{var _a3;return((_a3=r.__seg)==null?void 0:_a3.speaker)&&r.__seg.speaker.toLowerCase()===name.toLowerCase()}),_abNavIdx=0;const color=colorFor(name);_abBar.dataset.charName=name,_abBar.hidden=!1,_abSyncTopbar(),_abBar.innerHTML=`<span class="ab-char-bar-dot" style="background:${color}">${(name||"?")[0].toUpperCase()}</span>
<div class="ab-char-bar-info">
<span class="ab-char-bar-name">${escHtml(name)}</span>
<span class="ab-char-bar-cnt">${_abCharRows.length} Zeilen</span>
</div>
<div class="ab-char-bar-nav">
<button class="ab-char-bar-prev" title="Vorherige Zeile"><span class="mdi mdi-chevron-up"></span></button>
<span class="ab-char-bar-pos">1 / ${_abCharRows.length}</span>
<button class="ab-char-bar-next" title="N\xE4chste Zeile"><span class="mdi mdi-chevron-down"></span></button>
</div>
<input class="ab-char-bar-search" type="search" placeholder="In Zeilen suchen\u2026" autocomplete="off">
<button class="ab-char-bar-profile" title="Charakterblatt \xF6ffnen"><span class="mdi mdi-account-details-outline"></span> Profil</button>
<button class="ab-char-bar-close" title="Auswahl aufheben"><span class="mdi mdi-close"></span></button>`,_abCharRows.forEach(r=>r.classList.add("ab-char-hl")),_abCharRows.length&&(_abCharRows[0].classList.add("ab-char-focus"),_abScrollIntoView(_abCharRows[0],{block:"center"})),_abBar.querySelector(".ab-char-bar-prev").addEventListener("click",()=>_abNav(-1)),_abBar.querySelector(".ab-char-bar-next").addEventListener("click",()=>_abNav(1)),_abBar.querySelector(".ab-char-bar-profile").addEventListener("click",()=>{_abDetailEl?_abCloseProfile():_abShowProfile(name)}),_abBar.querySelector(".ab-char-bar-close").addEventListener("click",_abCloseBar);const inp=_abBar.querySelector(".ab-char-bar-search");let _st=null;const _doSearch=jump=>{const q=inp.value.trim().toLowerCase(),pool=q?_abCharRows.filter(r=>{var _a3,_b3;return(((_a3=r.__seg)==null?void 0:_a3.text)||((_b3=r.querySelector(".ab-cv-txt"))==null?void 0:_b3.textContent)||"").toLowerCase().includes(q)}):_abCharRows;feed.querySelectorAll(".ab-cv-row.ab-char-focus").forEach(r=>r.classList.remove("ab-char-focus")),pool.length?(jump?_abNavIdx=(_abNavIdx+1)%pool.length:_abNavIdx=0,pool[_abNavIdx].classList.add("ab-char-focus"),_abScrollIntoView(pool[_abNavIdx],{block:"center"}),_abBar.querySelector(".ab-char-bar-pos").textContent=_abNavIdx+1+" / "+pool.length):_abBar.querySelector(".ab-char-bar-pos").textContent="0 Treffer"};inp.addEventListener("input",()=>{clearTimeout(_st),_st=setTimeout(()=>_doSearch(!1),200)}),inp.addEventListener("keydown",e=>{e.key==="Enter"&&(e.preventDefault(),_doSearch(!0))})};let _abRosterFilter="",_abRosterSort="count";const renderRoster=()=>{const q=_abRosterFilter.trim().toLowerCase();let items=[...roster.entries()].filter(([n,info])=>info.count>0&&(!q||n.toLowerCase().includes(q)));if(items.sort(_abRosterSort==="alpha"?(a,b)=>a[0].localeCompare(b[0]):(a,b)=>b[1].count-a[1].count),!items.length){chars.innerHTML=q?'<span class="ab-cv-empty">No matches</span>':`<div class="ab-skel-chars">${[38,62,45,28,54,35].map(w=>`<div class="ab-skel-char-row"><div class="ab-skel-dot"></div><div class="ab-skel-line" style="width:${w}%"></div><div class="ab-skel-num"></div></div>`).join("")}</div>`;return}const prev=_abBar.hidden?null:_abBar.dataset.charName;chars.innerHTML=items.map(([n,info])=>{var _a3;const color=info.color||colorFor(n);return`<div class="ab-char-item${prev===n?" is-active":""}" data-name="${escHtml(n)}">
${_abAvatarHtml(n,color)}
<span class="ab-char-name">${escHtml(n)}</span>
<b class="ab-char-count">${((_a3=roster.get(n))==null?void 0:_a3.count)||0}</b>
<button class="ab-char-alias-btn" data-name="${escHtml(n)}" title="Add alias / also known as"><span class="mdi mdi-tag-plus-outline"></span></button>
</div>`}).join(""),chars.querySelectorAll(".ab-char-item").forEach(item=>{item.addEventListener("click",()=>_abSelectChar(item.dataset.name))}),chars.querySelectorAll(".ab-char-alias-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),_abOpenAliasPopup(btn.dataset.name,btn)})})};(()=>{const searchInp=panel.querySelector("#ab-cv-side-search"),sortSel=panel.querySelector("#ab-cv-side-sort");try{_abRosterSort=localStorage.getItem("ttsvc_ab_side_sort")||"count"}catch{}sortSel&&(sortSel.value=_abRosterSort);let _t2=null;searchInp==null||searchInp.addEventListener("input",()=>{clearTimeout(_t2),_t2=setTimeout(()=>{_abRosterFilter=searchInp.value,renderRoster()},120)}),sortSel==null||sortSel.addEventListener("change",()=>{_abRosterSort=sortSel.value;try{localStorage.setItem("ttsvc_ab_side_sort",_abRosterSort)}catch{}renderRoster()})})();let _abAliasPopup=null;function _abCloseAliasPopup(){_abAliasPopup&&(_abAliasPopup.remove(),_abAliasPopup=null)}function _abOpenAliasPopup(name,anchorEl){_abCloseAliasPopup();const otherNames=[...roster.entries()].filter(([n,info])=>info.count>0&&n.toLowerCase()!==name.toLowerCase()&&!/^Narrator$/i.test(n)&&!/^Unknown|Unbekannt/i.test(n)).sort((a,b)=>b[1].count-a[1].count),listId="ab-alias-roster-list",el=document.createElement("div");el.className="ab-alias-popup",el.innerHTML=`
<div class="ab-alias-popup-title">Also known as \u2014 <b>${escHtml(name)}</b></div>
<input type="text" class="ab-alias-popup-inp" placeholder="e.g. Garthai, der Fremde \u2014 or pick a recognized character below" autocomplete="off" list="${listId}">
<datalist id="${listId}">
${otherNames.map(([n,info])=>`<option value="${escHtml(n)}">${escHtml(n)} (${info.count} lines)</option>`).join("")}
</datalist>
${otherNames.length?`<div class="ab-alias-popup-hint">Or merge with an already-found character:</div>
<div class="ab-alias-popup-list">
${otherNames.slice(0,8).map(([n,info])=>`<button type="button" class="ab-alias-merge-opt" data-name="${escHtml(n)}"><span class="ab-alias-merge-name">${_abAvatarHtml(n,info.color)}${escHtml(n)}</span><span class="ab-alias-merge-count">${info.count}</span></button>`).join("")}
</div>`:""}
<div class="ab-alias-popup-actions">
<button type="button" class="btn-secondary btn-sm ab-alias-cancel">Cancel</button>
<button type="button" class="btn-primary btn-sm ab-alias-save">Add</button>
</div>`,document.body.appendChild(el);const rect=anchorEl.getBoundingClientRect();el.style.left=Math.min(rect.left,window.innerWidth-280)+"px",el.style.top=Math.min(rect.bottom+4,window.innerHeight-340)+"px";const inp=el.querySelector(".ab-alias-popup-inp");setTimeout(()=>inp.focus(),30);const otherByLower=new Map(otherNames.map(([n])=>[n.toLowerCase(),n])),doSave=async mergeName=>{var _a3,_b3;const alias=mergeName||inp.value.trim();if(!alias){_abCloseAliasPopup();return}const mergeTarget=otherByLower.get(alias.toLowerCase());if(_abCloseAliasPopup(),mergeTarget){const busy=_abShowBusyOverlay(`Merging "${mergeTarget}" into ${name}\u2026`,!0);await new Promise(r=>requestAnimationFrame(r));try{const book=((_a3=window.readerState)==null?void 0:_a3.title)||"",rec=await clUpsert(book,{name,aliases:alias});rec&&(registerCharacterRecord(rec),_hlCache&&(_hlCache.ver=-1));const{changed:n,segs}=_abMergeCharacters(mergeTarget,name);n&&(await _abPatchScatteredRows(segs,(done,total2)=>busy.setProgress(done,total2))||await _abRedrawSegmentsChunked(_abActiveSegments().arr,(done,total2)=>busy.setProgress(done,total2)),_abPersistManualEdit()),toast(n?`Merged "${mergeTarget}" into ${name} (${n} line${n!==1?"s":""})`:`"${alias}" added as an alias for ${name}`,"success")}catch(err){toast("Could not merge: "+(err.message||err),"error")}finally{busy.remove()}}else try{const book=((_b3=window.readerState)==null?void 0:_b3.title)||"",rec=await clUpsert(book,{name,aliases:alias});rec&&(registerCharacterRecord(rec),_hlCache&&(_hlCache.ver=-1)),renderRoster(),toast(`"${alias}" added as an alias for ${name}`,"success")}catch(err){toast("Could not save alias: "+(err.message||err),"error")}};el.querySelector(".ab-alias-save").addEventListener("click",()=>doSave()),el.querySelector(".ab-alias-cancel").addEventListener("click",()=>_abCloseAliasPopup()),el.querySelectorAll(".ab-alias-merge-opt").forEach(btn=>{btn.addEventListener("click",()=>doSave(btn.dataset.name))}),inp.addEventListener("keydown",e=>{e.key==="Enter"?(e.preventDefault(),doSave()):e.key==="Escape"&&(e.preventDefault(),_abCloseAliasPopup())}),setTimeout(()=>{document.addEventListener("click",function onDoc(e){!el.contains(e.target)&&e.target!==anchorEl&&(_abCloseAliasPopup(),document.removeEventListener("click",onDoc))})},0),_abAliasPopup=el}function _abMergeCharacters(fromName,intoName){const active=_abActiveSegments();if(!active.arr.length)return{changed:0,arr:active.arr,segs:[]};_abPushEditState(active.key,active.arr);let changed=0;const segs=[];for(const s of active.arr)s.type==="dialogue"&&s.speaker&&s.speaker.toLowerCase()===fromName.toLowerCase()&&(s.speaker=intoName,changed++,segs.push(s));return{changed,arr:active.arr,segs}}(async()=>{var _a3;const title=((_a3=window.readerState)==null?void 0:_a3.title)||"";try{const records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(title):typeof clGetAll=="function"?await clGetAll():[];for(const rec of records||[])if(rec!=null&&rec.name){registerCharacterRecord(rec);for(const alias of identityNames(rec)){const rosterName=[...roster.keys()].find(n=>n.toLowerCase()===alias.toLowerCase());rosterName&&setCharacterColor(rosterName,_abRecordColor(rec,rec.name))}}_abRefreshCharacterColors()}catch{}})();const MAXROWS=Number.POSITIVE_INFINITY;let _userScrolled=!1;feed.addEventListener("scroll",()=>{var _a3;const atBottom=feed.scrollTop+feed.clientHeight>=feed.scrollHeight-80;_userScrolled=!atBottom,jumpBtn&&(jumpBtn.hidden=atBottom);const labels=Array.from(feed.querySelectorAll(".ab-cv-page-label[data-page]"));let current=_abCurrentPageNum;for(const label of labels){const r=(_a3=label.closest(".ab-cv-page"))==null?void 0:_a3.getBoundingClientRect(),fr=feed.getBoundingClientRect();if(r&&r.top<=fr.top+80)current=Number(label.dataset.page)||current;else break}current&&_abSetCurrentPage(current)},{passive:!0});const _scrollToBottom=()=>{feed.scrollTop=feed.scrollHeight};jumpBtn&&jumpBtn.addEventListener("click",()=>{_userScrolled=!1,jumpBtn.hidden=!0,_scrollToBottom()});const trim=()=>{if(!_userScrolled){if(Number.isFinite(MAXROWS)){let rows=feed.querySelectorAll(".ab-cv-row, .ab-cv-note, .ab-cv-divider");for(;rows.length>MAXROWS;){const first=rows[0],page=first.closest(".ab-cv-page");first.remove(),page&&!page.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider")&&page.remove(),rows=feed.querySelectorAll(".ab-cv-row, .ab-cv-note, .ab-cv-divider")}}_scrollToBottom()}};let _abCurPage=null;const _abJumpToReaderPage=async pageNum=>{const pg=parseInt(pageNum,10);if(pg){if(typeof window.readerJumpToPage=="function"){await window.readerJumpToPage(pg);return}typeof navTo=="function"&&navTo("s-reader"),typeof window.navReaderView=="function"?window.navReaderView("main"):typeof window.showReaderView=="function"&&window.showReaderView("main"),setTimeout(async()=>{var _a3,_b3,_c3;const pageDiv=(_c3=(_b3=(_a3=window.readerState)==null?void 0:_a3.pages)==null?void 0:_b3[pg-1])==null?void 0:_c3.pageDiv;pageDiv?(typeof window.readerRenderPage=="function"&&await window.readerRenderPage(pg-1),_abScrollIntoView(pageDiv,{block:"start",inline:"nearest"})):typeof toast=="function"&&toast("Source page is not loaded in Reader yet","info")},250)}},_abNewPage=(label,pageNum)=>{_abCurPage&&!_abCurPage.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider")&&_abCurPage.remove();const p=document.createElement("div");if(p.className="ab-cv-page",label){const pageAttr=pageNum?` data-page="${escHtml(String(pageNum))}"`:"";p.innerHTML=`<button class="ab-cv-page-label"${pageAttr} type="button" title="Back to source page in Reader"><span class="mdi mdi-book-open-page-variant"></span> ${escHtml(label)} <span class="mdi mdi-arrow-u-left-top ab-cv-page-back"></span></button>`}return feed.appendChild(p),_abCurPage=p,pageNum&&(_abSetCurrentPage(pageNum),_abUpdatePageNav()),p},_abPage=()=>_abCurPage||_abNewPage(),_abPageNumbers=()=>{var _a3,_b3,_c3;const set=new Set,readerPages=((_a3=window.readerState)==null?void 0:_a3.mode)==="pdf"&&((_c3=(_b3=window.readerState)==null?void 0:_b3.pages)==null?void 0:_c3.length)||0;if(readerPages>0)for(let i=1;i<=readerPages;i++)set.add(i);else(_audiobook.pageMarks||[]).forEach(m=>set.add((m.page||0)+1));return feed.querySelectorAll(".ab-cv-page-label[data-page]").forEach(el=>{const n=parseInt(el.dataset.page,10);n&&set.add(n)}),[...set].sort((a,b)=>a-b)},_abSetCurrentPage=pageNum=>{const pg=parseInt(pageNum,10);if(!pg)return;_abCurrentPageNum=pg;const sel=_abPageNav.querySelector(".ab-page-select");sel&&[...sel.options].some(o=>Number(o.value)===pg)&&(sel.value=String(pg))},_abUpdatePageNav=()=>{const pages=_abPageNumbers(),sel=_abPageNav.querySelector(".ab-page-select");if(!sel||!pages.length){_abPageNav.hidden=!0,typeof _abSyncTopbar=="function"&&_abSyncTopbar();return}const old=Number(sel.value)||_abCurrentPageNum||pages[0];sel.innerHTML=pages.map(n=>`<option value="${n}">Page ${n}</option>`).join(""),_abPageNav.hidden=pages.length<=1,_abSetCurrentPage(pages.includes(old)?old:pages[0]),typeof _abSyncTopbar=="function"&&_abSyncTopbar()},_abScrollToCastPage=pageNum=>{const pg=parseInt(pageNum,10);if(!pg)return;let label=feed.querySelector(`.ab-cv-page-label[data-page="${CSS.escape(String(pg))}"]`);if(!label){const active=_abActiveSegments();active.arr.length&&_abRedrawSegments(active.arr),label=feed.querySelector(`.ab-cv-page-label[data-page="${CSS.escape(String(pg))}"]`)}if(pg===1){_userScrolled=!0,jumpBtn&&(jumpBtn.hidden=!1);const firstPage=(label==null?void 0:label.closest(".ab-cv-page"))||feed.firstElementChild;firstPage?_abScrollIntoView(firstPage,{block:"start"}):feed.scrollTo({top:0,behavior:"smooth"}),_abSetCurrentPage(pg);return}if(!label){toast("This cast page is not available yet. Use the book button to jump to the source page.","info"),_abSetCurrentPage(pg);return}_userScrolled=!0,jumpBtn&&(jumpBtn.hidden=!1),_abScrollIntoView(label.closest(".ab-cv-page"),{block:"start"}),_abSetCurrentPage(pg)},_abStepPage=dir=>{var _a3;const pages=_abPageNumbers();if(!pages.length)return;const current=Number((_a3=_abPageNav.querySelector(".ab-page-select"))==null?void 0:_a3.value)||_abCurrentPageNum||pages[0],idx=Math.max(0,pages.indexOf(current)),next=pages[Math.max(0,Math.min(pages.length-1,idx+dir))];_abScrollToCastPage(next)};(_a2=_abPageNav.querySelector(".ab-page-select"))==null||_a2.addEventListener("change",e=>_abScrollToCastPage(e.target.value)),(_b2=_abPageNav.querySelector(".ab-page-prev"))==null||_b2.addEventListener("click",()=>_abStepPage(-1)),(_c2=_abPageNav.querySelector(".ab-page-next"))==null||_c2.addEventListener("click",()=>_abStepPage(1)),(_d2=_abPageNav.querySelector(".ab-page-source"))==null||_d2.addEventListener("click",()=>{var _a3;const pg=Number((_a3=_abPageNav.querySelector(".ab-page-select"))==null?void 0:_a3.value)||_abCurrentPageNum;_abJumpToReaderPage(pg)});const _abUndoStack=[],_abRedoStack=[],_abHistoryLimit=40,_abSyncTopbar=()=>{const hasSelection=!_abBar.hidden,hasHistory=_abUndoStack.length>0||_abRedoStack.length>0;_abEditToolbar.hidden=!hasHistory,_abTopbar.hidden=!1},_abActiveSegments=()=>_audiobook.running&&Array.isArray(_audiobook.liveSegments)?{key:"liveSegments",arr:_audiobook.liveSegments}:Array.isArray(_audiobook.segments)?{key:"segments",arr:_audiobook.segments}:Array.isArray(_audiobook.liveSegments)?{key:"liveSegments",arr:_audiobook.liveSegments}:{key:"segments",arr:[]},_abCloneSegments=segs=>(segs||[]).map(s=>({...s})),_abSyncRosterList=segs=>{const names=[];for(const s of segs||[]){const sp=(s==null?void 0:s.type)==="dialogue"&&s.speaker?s.speaker:"";!sp||/^Narrator$/i.test(sp)||/^Unknown|Unbekannt/i.test(sp)||names.some(n=>n.toLowerCase()===sp.toLowerCase())||names.push(sp)}_audiobook.roster=names},_abPersistManualEdit=()=>{const active=_abActiveSegments();if(_audiobook.lastText){const done=_audiobook.completedChunks||active.arr.length,total2=_audiobook.completedTotal||done;setTimeout(()=>_abSaveDraft(active.arr||[],_audiobook.roster||[],_audiobook.lastText,done,total2),50)}_audiobookDebouncedSave()},_abUpdateEditButtons=()=>{const undo=_abEditToolbar.querySelector(".ab-edit-undo"),redo=_abEditToolbar.querySelector(".ab-edit-redo");undo&&(undo.disabled=!_abUndoStack.length),redo&&(redo.disabled=!_abRedoStack.length),_abSyncTopbar()},_abPushEditState=(keyOverride,arrOverride)=>{const active=arrOverride?{key:keyOverride||"segments",arr:arrOverride}:_abActiveSegments();if(!active.arr.length)return!1;for(_abUndoStack.push({key:active.key,segments:_abCloneSegments(active.arr)});_abUndoStack.length>_abHistoryLimit;)_abUndoStack.shift();return _abRedoStack.length=0,_abUpdateEditButtons(),!0},_abRowSpeaker=s=>{const isNarrator=s.type!=="dialogue"||!s.speaker||String(s.speaker).toLowerCase()==="narrator";return{isNarrator,speakerName:isNarrator?"Narrator":s.speaker}},_abIsHeadingSeg=s=>{if((s==null?void 0:s.type)==="dialogue")return!1;const t=String((s==null?void 0:s.text)||"").trim();return!t||t.length>60?!1:/^(prolog(ue)?|epilog(ue)?|kapitel|chapter|teil|buch|part|book|akt|szene|scene)\b/i.test(t)||/^\d+[\.\)]?\s*(kapitel|chapter)?$/i.test(t)?!0:t.split(/\s+/).length<=7&&!/[.!?;:,«»"]/.test(t)},_abRowFromSegment=(s,extraClass="")=>{const{isNarrator,speakerName}=_abRowSpeaker(s),c=colorFor(speakerName),row=document.createElement("div");return row.className=`ab-cv-row${extraClass?" "+extraClass:""}${isNarrator?" is-narr":""}${_abIsHeadingSeg(s)?" is-heading":""}`,row.__seg=s,row.innerHTML=`<span class="ab-cv-row-tools">
<button class="ab-cv-row-tool ab-cv-edit-text" type="button" title="Edit text"><span class="mdi mdi-pencil-outline"></span></button>
<button class="ab-cv-row-tool ab-cv-merge-prev" type="button" title="Merge with previous segment"><span class="mdi mdi-arrow-collapse-up"></span></button>
<button class="ab-cv-row-tool ab-cv-merge-next" type="button" title="Merge with next segment"><span class="mdi mdi-arrow-collapse-down"></span></button>
</span>
<span class="ab-cv-spk" style="color:${c}" title="Click to assign character">${escHtml(speakerName)}${s.emotion?' <span style="text-transform:lowercase; font-weight:normal; opacity:0.8">('+escHtml(s.emotion)+")</span>":""}</span>
<span class="ab-cv-txt">${highlightText2(s.text||"")}</span>`,row},_abPatchScatteredRows=(segs,onProgress)=>new Promise(resolve=>{if(!segs||!segs.length){resolve(!0);return}const bySeg=new Set(segs),targets=[...feed.querySelectorAll(".ab-cv-row")].filter(r=>r.__seg&&bySeg.has(r.__seg));if(!targets.length){resolve(!1);return}let i=0;const step=()=>{const end=Math.min(i+150,targets.length);for(;i<end;i++)targets[i].replaceWith(_abRowFromSegment(targets[i].__seg));onProgress&&onProgress(i,targets.length),i<targets.length?requestAnimationFrame(step):(_abRecountRoster(_abActiveSegments().arr),_abClearHL(),_abUpdatePageNav(),resolve(!0))};requestAnimationFrame(step)}),_abPatchRowRange=(oldRows,newSegs,activeArr)=>{const rows=(oldRows||[]).filter(Boolean);if(!rows.length)return!1;const parent=rows[0].parentNode;if(!parent||rows.some(r=>r.parentNode!==parent))return!1;const beforeCounts=_abRosterCountsFromSegs(rows.map(r=>r.__seg).filter(Boolean)),afterCounts=_abRosterCountsFromSegs(newSegs||[]),frag=document.createDocumentFragment();for(const seg of newSegs||[])frag.appendChild(_abRowFromSegment(seg));parent.insertBefore(frag,rows[0]);for(const row of rows)row.remove();return _abApplyRosterDelta(beforeCounts,afterCounts),_abQueueRosterRender(),_abClearHL(),_abUpdatePageNav(),!0},_abAdjacentRow=(row,dir)=>{let cur=dir<0?row==null?void 0:row.previousElementSibling:row==null?void 0:row.nextElementSibling;for(;cur&&!(cur.classList&&cur.classList.contains("ab-cv-row"));)cur=dir<0?cur.previousElementSibling:cur.nextElementSibling;return cur||null},_abStartRowEdit=row=>{if(!row||!row.__seg||row.querySelector(".ab-cv-edit-ta"))return;const s=row.__seg,txtEl=row.querySelector(".ab-cv-txt");if(!txtEl)return;const ta=document.createElement("textarea");ta.className="ab-cv-edit-ta",ta.value=s.text||"",txtEl.replaceWith(ta),ta.focus(),ta.setSelectionRange(ta.value.length,ta.value.length);const commit=save=>{if(save){const val=ta.value;val!==s.text&&(_abPushEditState(),s.text=val,_abPersistManualEdit())}ta.replaceWith(txtEl),txtEl.innerHTML=highlightText2(s.text||"")};ta.addEventListener("keydown",e=>{e.key==="Enter"&&(e.ctrlKey||e.metaKey)?(e.preventDefault(),commit(!0)):e.key==="Escape"&&(e.preventDefault(),commit(!1))}),ta.addEventListener("blur",()=>commit(!0))},_abRosterCountsFromSegs=segs=>{const counts=new Map;for(const s of segs||[]){const{speakerName}=_abRowSpeaker(s||{});counts.set(speakerName,(counts.get(speakerName)||0)+1)}return counts},_abApplyRosterCounts=counts=>{for(const info of roster.values())info.count=0;for(const[name,count2]of counts||[]){const info=roster.get(name)||{count:0,color:colorFor(name)};info.count=count2,roster.set(name,info)}_audiobook.roster=[...roster.entries()].filter(([n,info])=>info.count>0&&!/^Narrator$/i.test(n)&&!/^Unknown|Unbekannt/i.test(n)).map(([n])=>n)},_abApplyRosterDelta=(beforeCounts,afterCounts)=>{const names=new Set([...(beforeCounts||new Map).keys(),...(afterCounts||new Map).keys()]);for(const name of names){const before=(beforeCounts==null?void 0:beforeCounts.get(name))||0,after=(afterCounts==null?void 0:afterCounts.get(name))||0;if(before===after)continue;const info=roster.get(name)||{count:0,color:colorFor(name)};info.count=Math.max(0,(info.count||0)+(after-before)),roster.set(name,info)}_audiobook.roster=[...roster.entries()].filter(([n,info])=>info.count>0&&!/^Narrator$/i.test(n)&&!/^Unknown|Unbekannt/i.test(n)).map(([n])=>n)};let _abRosterRenderRaf=0;const _abQueueRosterRender=()=>{_abRosterRenderRaf||(_abRosterRenderRaf=requestAnimationFrame(()=>{_abRosterRenderRaf=0,renderRoster()}))},_abRecountRoster=segs=>{_abApplyRosterCounts(_abRosterCountsFromSegs(segs||[])),_abQueueRosterRender()},_abRedrawSegments=segs=>{const selectedChar=!_abBar.hidden&&_abBar.dataset.charName?_abBar.dataset.charName:"";feed.innerHTML="",_abCurPage=null;let lastPage=null;for(const s of segs||[])s.page!=null&&s.page!==lastPage&&(_abNewPage("Page "+s.page,s.page),lastPage=s.page),_abPage().appendChild(_abRowFromSegment(s));_abRecountRoster(segs||[]),_abClearHL(),_abUpdatePageNav(),selectedChar&&(_abBar.hidden=!0,_abSelectChar(selectedChar))};let _abRedrawGen=0;const _abRedrawSegmentsChunked=(segs,onProgress,batchSize=150)=>new Promise(resolve=>{const selectedChar=!_abBar.hidden&&_abBar.dataset.charName?_abBar.dataset.charName:"",myGen=++_abRedrawGen;feed.innerHTML="",_abCurPage=null;const list=segs||[];let i=0,lastPage=null;const step=()=>{if(myGen!==_abRedrawGen){resolve();return}const end=Math.min(i+batchSize,list.length);for(;i<end;i++){const s=list[i];s.page!=null&&s.page!==lastPage&&(_abNewPage("Page "+s.page,s.page),lastPage=s.page),_abPage().appendChild(_abRowFromSegment(s))}onProgress&&onProgress(i,list.length),i<list.length?requestAnimationFrame(step):(_abRecountRoster(list),_abClearHL(),_abUpdatePageNav(),selectedChar&&(_abBar.hidden=!0,_abSelectChar(selectedChar)),resolve())};requestAnimationFrame(step)}),_abRestoreEditState=snap=>{if(!snap)return;const restored=_abCloneSegments(snap.segments);snap.key==="liveSegments"?_audiobook.liveSegments=restored:_audiobook.segments=restored,_abRedrawSegments(restored),_abPersistManualEdit()},_abUndoEdit=()=>{const active=_abActiveSegments();!_abUndoStack.length||!active.arr.length||(_abRedoStack.push({key:active.key,segments:_abCloneSegments(active.arr)}),_abRestoreEditState(_abUndoStack.pop()),_abUpdateEditButtons())},_abRedoEdit=()=>{const active=_abActiveSegments();!_abRedoStack.length||!active.arr.length||(_abUndoStack.push({key:active.key,segments:_abCloneSegments(active.arr)}),_abRestoreEditState(_abRedoStack.pop()),_abUpdateEditButtons())};(_e2=_abEditToolbar.querySelector(".ab-edit-undo"))==null||_e2.addEventListener("click",_abUndoEdit),(_f2=_abEditToolbar.querySelector(".ab-edit-redo"))==null||_f2.addEventListener("click",_abRedoEdit),_abSyncTopbar();let assignModeSeg=null,assignModeRow=null,assignPopup=document.getElementById("ab-cv-assign-popup");if(!assignPopup){assignPopup=document.createElement("div"),assignPopup.id="ab-cv-assign-popup",assignPopup.style.cssText="position:fixed; z-index:2001; display:none; background:var(--surface); border:1px solid var(--border); border-radius:8px; box-shadow:0 10px 25px rgba(0,0,0,0.4); padding:10px; width:220px; max-height:320px; flex-direction:column; gap:8px;";const header=document.createElement("div");header.style.cssText="display:flex; justify-content:space-between; align-items:center; margin-bottom:-4px; margin-top:-4px;",header.innerHTML='<span style="font-size:11px; font-weight:600; color:var(--subtext); text-transform:uppercase;">Assign to</span><span class="mdi mdi-close" style="cursor:pointer; font-size:16px; color:var(--subtext); padding:4px;" title="Close (Esc)"></span>',assignPopup.appendChild(header);const inp=document.createElement("input");inp.type="text",inp.placeholder="Search or add character...",inp.style.cssText="width:100%; padding:6px; font-size:13px; border:1px solid var(--border); border-radius:4px; background:var(--bg); color:var(--text);",assignPopup.appendChild(inp);const list=document.createElement("div");list.className="ab-cv-popup-list",list.style.cssText="display:flex; flex-direction:column; gap:2px; overflow-y:auto; max-height:220px; margin-right:-4px; padding-right:4px;",assignPopup.appendChild(list),document.body.appendChild(assignPopup),header.querySelector(".mdi-close").addEventListener("click",()=>{var _a3;return(_a3=assignPopup._close)==null?void 0:_a3.call(assignPopup)}),document.addEventListener("mousedown",e=>{var _a3;assignPopup.style.display!=="none"&&!assignPopup.contains(e.target)&&!e.target.closest(".ab-cv-spk")&&!e.target.closest(".ab-cv-txt")&&((_a3=assignPopup._close)==null||_a3.call(assignPopup))}),document.addEventListener("keydown",e=>{var _a3;e.key==="Escape"&&assignPopup.style.display!=="none"&&((_a3=assignPopup._close)==null||_a3.call(assignPopup))}),inp.addEventListener("keydown",e=>{var _a3,_b3;if(e.key==="Enter"){const addBtn=assignPopup.querySelector(".ab-cv-popup-add-btn"),visibleBtns=Array.from(assignPopup.querySelectorAll(".ab-cv-popup-list > div[data-name]")).filter(b=>b.style.display!=="none");if(addBtn&&addBtn.style.display!=="none")addBtn.click();else if(visibleBtns.length===1)visibleBtns[0].click();else{const val=inp.value.trim();val&&((_a3=assignPopup._assignName)==null||_a3.call(assignPopup,val))}}else e.key==="Escape"&&((_b3=assignPopup._close)==null||_b3.call(assignPopup))}),inp.addEventListener("input",()=>{const val=inp.value.trim().toLowerCase(),listItems=assignPopup.querySelectorAll(".ab-cv-popup-list > div[data-name]");let exactMatch=!1;listItems.forEach(item=>{const name=item.dataset.name.toLowerCase();name===val&&(exactMatch=!0),name.includes(val)?item.style.display="flex":item.style.display="none"});let addBtn=assignPopup.querySelector(".ab-cv-popup-add-btn");val&&!exactMatch&&val!=="narrator"?(addBtn||(addBtn=document.createElement("div"),addBtn.className="ab-cv-popup-add-btn",addBtn.style.cssText="padding:6px 8px; font-size:12px; cursor:pointer; border-radius:4px; display:flex; align-items:center; gap:6px; transition:background 0.1s; font-weight:600; color:var(--primary); border-top:1px solid var(--border); margin-top:4px; padding-top:8px;",addBtn.onmouseover=()=>addBtn.style.background="var(--panel)",addBtn.onmouseout=()=>addBtn.style.background="transparent"),addBtn.innerHTML=`<span style="font-size:14px; font-weight:bold;">+</span> Add "${escHtml(inp.value.trim())}"`,addBtn.onclick=()=>{var _a3;return(_a3=assignPopup._assignName)==null?void 0:_a3.call(assignPopup,inp.value.trim())},addBtn.style.display="flex",assignPopup.querySelector(".ab-cv-popup-list").appendChild(addBtn)):addBtn&&(addBtn.style.display="none")})}function closeAssignPopup(){assignPopup.style.display="none",assignModeRow&&assignModeRow.classList.remove("is-assigning"),assignModeSeg=null,assignModeRow=null}function assignName(name){if(!assignModeSeg)return;const active=_abActiveSegments();_abPushEditState(active.key,active.arr);const isNarrator=name.toLowerCase()==="narrator",_oldSpk=assignModeSeg.speaker;if(_oldSpk&&roster.has(_oldSpk)){const _old=roster.get(_oldSpk);_old.count>0&&_old.count--}assignModeSeg.speaker=isNarrator?"Narrator":name,assignModeSeg.type=isNarrator?"narration":"dialogue",isNarrator&&(assignModeSeg.emotion=""),assignModeRow.classList.remove("is-assigning");const speakerName=isNarrator?"Narrator":name,c=colorFor(speakerName);roster.has(speakerName)||roster.set(speakerName,{count:0,color:c}),roster.get(speakerName).count++,assignModeRow.className="ab-cv-row"+(isNarrator?" is-narr":""),assignModeRow.querySelector(".ab-cv-spk").innerHTML=`${escHtml(speakerName)}${assignModeSeg.emotion?' <span style="text-transform:lowercase; font-weight:normal; opacity:0.8">('+escHtml(assignModeSeg.emotion)+")</span>":""}`,assignModeRow.querySelector(".ab-cv-spk").style.color=c,assignModeRow.querySelector(".ab-cv-spk").title="Click to assign character",_abRecountRoster(_abActiveSegments().arr),toast(`Assigned to ${isNarrator?"Narrator":name}`,"success"),closeAssignPopup(),_abPersistManualEdit()}function _abOpenAssignPopup(row,anchorEl,prefill){var _a3;if(!row||!row.__seg||row.classList.contains("is-processing"))return;assignModeRow&&assignModeRow.classList.remove("is-assigning"),assignModeSeg=row.__seg,assignModeRow=row,row.classList.add("is-assigning"),assignPopup._assignName=assignName,assignPopup._close=closeAssignPopup,window.getSelection().removeAllRanges(),assignPopup.style.display="flex";const rect=anchorEl.getBoundingClientRect(),top=Math.min(rect.bottom+4,window.innerHeight-300);assignPopup.style.top=top+"px",assignPopup.style.left=rect.left+"px";const list=assignPopup.querySelector(".ab-cv-popup-list");list.innerHTML="";const narrBtn=document.createElement("div");narrBtn.dataset.name="Narrator",narrBtn.style.cssText="padding:6px 8px; font-size:12px; cursor:pointer; border-radius:4px; display:flex; align-items:center; gap:6px; transition:background 0.1s; font-weight:600; color:var(--subtext); border-bottom:1px solid var(--border); margin-bottom:4px; padding-bottom:8px;",narrBtn.innerHTML='<span style="font-size:13px;">\u{1F4D6}</span> Narrator',narrBtn.onmouseover=()=>narrBtn.style.background="var(--panel)",narrBtn.onmouseout=()=>narrBtn.style.background="transparent",narrBtn.onclick=()=>assignName("Narrator"),list.appendChild(narrBtn);const items=[...roster.entries()].sort((a,b)=>b[1].count-a[1].count);for(const[n,info]of items){const btn=document.createElement("div");btn.dataset.name=n,btn.style.cssText="padding:6px 8px; font-size:12px; cursor:pointer; border-radius:4px; display:flex; align-items:center; gap:6px; transition:background 0.1s; font-weight:600; color:var(--text);";const img=(_a3=recordForName(n))==null?void 0:_a3.image;btn.innerHTML=(img?`<span style="width:18px;height:18px;border-radius:50%;flex-shrink:0;overflow:hidden;"><img src="${escHtml(img)}" alt="" style="width:100%;height:100%;object-fit:cover;"></span>`:`<span style="width:8px;height:8px;border-radius:50%;background:${info.color};flex-shrink:0;"></span>`)+` ${escHtml(n)}`,btn.onmouseover=()=>btn.style.background="var(--panel)",btn.onmouseout=()=>btn.style.background="transparent",btn.onclick=()=>assignName(n),list.appendChild(btn)}const inp=assignPopup.querySelector("input");inp.value=prefill||"",setTimeout(()=>{inp.focus(),prefill&&inp.dispatchEvent(new Event("input"))},50)}const _abIsUnknownSeg=s=>!(s!=null&&s.speaker)||/^Unknown|Unbekannt/i.test(s.speaker),_abIsNarrSeg=s=>(s==null?void 0:s.type)!=="dialogue"||!s.speaker||/^Narrator$/i.test(s.speaker),_abMergedSegment=(a,b)=>{var _a3;let base=a;(_abIsUnknownSeg(a)&&!_abIsUnknownSeg(b)||_abIsNarrSeg(a)&&!_abIsNarrSeg(b))&&(base=b);const type=_abIsNarrSeg(base)?"narration":"dialogue";return{speaker:type==="narration"?"Narrator":base.speaker||"Unknown",type,emotion:type==="dialogue"&&(base.emotion||a.emotion||b.emotion)||"",text:audiobookJoinSegmentText(a.text||"",b.text||""),page:(_a3=a.page)!=null?_a3:b.page}},_abMergeByRow=(row,dir)=>{const active=_abActiveSegments(),idx=active.arr.indexOf(row==null?void 0:row.__seg);if(idx<0)return;const leftIdx=dir<0?idx-1:idx,rightIdx=dir<0?idx:idx+1;if(leftIdx<0||rightIdx>=active.arr.length){toast("No adjacent segment to merge","info");return}const leftRow=dir<0?_abAdjacentRow(row,-1):row,rightRow=dir<0?row:_abAdjacentRow(row,1);_abPushEditState(active.key,active.arr);const merged=_abMergedSegment(active.arr[leftIdx],active.arr[rightIdx]);active.arr.splice(leftIdx,2,merged),active.key==="segments"?_audiobook.segments=active.arr:_audiobook.liveSegments=active.arr,!leftRow||!rightRow||leftRow.parentNode!==rightRow.parentNode?_abRedrawSegmentsChunked(active.arr):_abPatchRowRange([leftRow,rightRow],[merged],active.arr)||_abRedrawSegmentsChunked(active.arr),_abPersistManualEdit(),toast("Segments merged","success")};feed.addEventListener("click",e=>{var _a3;const pageLabel=e.target.closest(".ab-cv-page-label[data-page]");if(pageLabel){e.preventDefault(),e.stopPropagation(),_abJumpToReaderPage(pageLabel.dataset.page);return}const editBtn=e.target.closest(".ab-cv-edit-text");if(editBtn){e.preventDefault(),e.stopPropagation(),_abStartRowEdit(editBtn.closest(".ab-cv-row"));return}const mergeBtn=e.target.closest(".ab-cv-row-tool");if(mergeBtn){e.preventDefault(),e.stopPropagation(),_abMergeByRow(mergeBtn.closest(".ab-cv-row"),mergeBtn.classList.contains("ab-cv-merge-prev")?-1:1);return}const spk=e.target.closest(".ab-cv-spk");if(spk){_abOpenAssignPopup(spk.closest(".ab-cv-row"),spk,"");return}if(_abJustHandledSelection){_abJustHandledSelection=!1;return}const txtEl=e.target.closest(".ab-cv-txt");if(txtEl&&window.getSelection().isCollapsed){if(!assignModeRow)return;const row=txtEl.closest(".ab-cv-row"),nameHit=e.target.closest(".ab-name-hit"),clickedWord=nameHit?nameHit.dataset.name||nameHit.textContent.trim():(_a3=_abWordRangeAtPoint(e.clientX,e.clientY))==null?void 0:_a3.word;if(assignModeRow!==row){if(clickedWord){const inp=assignPopup.querySelector("input");inp.value=clickedWord,inp.dispatchEvent(new Event("input")),inp.focus()}return}if(nameHit)_abOpenAssignPopup(row,nameHit,clickedWord);else{const hit=_abWordRangeAtPoint(e.clientX,e.clientY);if(hit){const rect=hit.range.getBoundingClientRect();_abOpenAssignPopup(row,{getBoundingClientRect:()=>rect},hit.word)}}}});let _abJustHandledSelection=!1,_abHoverHL=document.getElementById("ab-word-hover");_abHoverHL||(_abHoverHL=document.createElement("div"),_abHoverHL.id="ab-word-hover",_abHoverHL.className="ab-word-hover",_abHoverHL.hidden=!0,document.body.appendChild(_abHoverHL));let _abHoverRaf=0,_abHoverPt=null;const _abHideHoverHL=()=>{_abHoverHL.hidden=!0};feed.addEventListener("mousemove",e=>{_abHoverPt={x:e.clientX,y:e.clientY,target:e.target},!_abHoverRaf&&(_abHoverRaf=requestAnimationFrame(()=>{var _a3;_abHoverRaf=0;const pt=_abHoverPt;if(!pt||!((_a3=pt.target)!=null&&_a3.closest))return;if(!pt.target.closest(".ab-cv-txt")||pt.target.closest(".ab-name-hit")||feed.querySelector(".ab-cv-edit-ta")){_abHideHoverHL();return}const hit=_abWordRangeAtPoint(pt.x,pt.y);if(!hit){_abHideHoverHL();return}const r=hit.range.getBoundingClientRect();if(!r.width){_abHideHoverHL();return}_abHoverHL.style.left=r.left-2+"px",_abHoverHL.style.top=r.top-1+"px",_abHoverHL.style.width=r.width+4+"px",_abHoverHL.style.height=r.height+2+"px",_abHoverHL.hidden=!1}))}),feed.addEventListener("mouseleave",_abHideHoverHL),feed.addEventListener("scroll",_abHideHoverHL,{passive:!0}),feed.addEventListener("dblclick",e=>{const hit=e.target.closest(".ab-name-hit");if(!hit)return;const row=hit.closest(".ab-cv-row"),name=hit.dataset.name;row&&name&&(_abOpenAssignPopup(row,hit,""),assignName(name))});let splitBtn=document.getElementById("ab-cv-split-btn");splitBtn||(splitBtn=document.createElement("button"),splitBtn.id="ab-cv-split-btn",splitBtn.className="btn-secondary btn-sm",splitBtn.innerHTML='<span class="mdi mdi-call-split"></span> Split text to Unknown Speaker',splitBtn.style.cssText="position:fixed; z-index:2000; display:none; background:var(--accent); color:#fff; border:none; box-shadow:0 4px 12px rgba(0,0,0,0.3);",document.body.appendChild(splitBtn));let currentSplitState=null;const _abOffsetInEl=(el,node,offset)=>{const r=document.createRange();return r.selectNodeContents(el),r.setEnd(node,offset),r.toString().length};return document.addEventListener("selectionchange",()=>{if(!feed)return;const sel=window.getSelection();if(sel.isCollapsed||!feed.contains(sel.anchorNode)){splitBtn&&(splitBtn.style.display="none"),currentSplitState=null;return}const txtSpan=sel.anchorNode.nodeType===3?sel.anchorNode.parentNode.closest(".ab-cv-txt"):sel.anchorNode.closest(".ab-cv-txt");if(!txtSpan){splitBtn.style.display="none";return}const row=txtSpan.closest(".ab-cv-row");if(!row||!row.__seg)return;const rawText=sel.toString(),text=rawText.trim();if(text.length>0){if(assignModeSeg&&text.length<40&&!text.includes(`
`)){splitBtn.style.display="none";return}const range=sel.getRangeAt(0),rect=range.getBoundingClientRect();splitBtn.style.display="block",splitBtn.style.top=rect.bottom+8+"px",splitBtn.style.left=Math.max(10,rect.left+rect.width/2-100)+"px";const startOffset=_abOffsetInEl(txtSpan,range.startContainer,range.startOffset),endOffset=_abOffsetInEl(txtSpan,range.endContainer,range.endOffset);currentSplitState={row,text,rawText,seg:row.__seg,startOffset,endOffset}}else splitBtn.style.display="none",currentSplitState=null}),splitBtn.addEventListener("click",()=>{if(!currentSplitState)return;const{row,seg,startOffset,endOffset}=currentSplitState,fullText=seg.text||"",before=fullText.slice(0,startOffset),selectedText=fullText.slice(startOffset,endOffset),after=fullText.slice(endOffset);if(!selectedText)return;let arr=_audiobook.segments||[],globalIdx=arr.indexOf(seg);globalIdx===-1&&Array.isArray(_audiobook.liveSegments)&&(arr=_audiobook.liveSegments,globalIdx=arr.indexOf(seg)),globalIdx!==-1&&_abPushEditState(arr===_audiobook.liveSegments?"liveSegments":"segments",arr);const newSegs=[];if(before.length&&newSegs.push({speaker:seg.speaker,type:seg.type,emotion:seg.emotion,text:before,page:seg.page}),newSegs.push({speaker:"Unknown",type:"dialogue",emotion:"",text:selectedText,page:seg.page}),after.length&&newSegs.push({speaker:seg.speaker,type:seg.type,emotion:seg.emotion,text:after,page:seg.page}),globalIdx!==-1)arr.splice(globalIdx,1,...newSegs),_abPatchRowRange([row],newSegs,arr)||_abRedrawSegments(arr),_abPersistManualEdit();else{console.warn("[audiobook] split during casting: DOM-only, segment not yet in array");const frag=document.createDocumentFragment();for(const s of newSegs)frag.appendChild(_abRowFromSegment(s));row.parentNode.insertBefore(frag,row),row.remove()}splitBtn.style.display="none",window.getSelection().removeAllRanges(),toast("Segment split! You can now assign a character to the Unknown block.","success")}),feed.addEventListener("mouseup",()=>{const sel=window.getSelection();if(sel.isCollapsed||!feed.contains(sel.anchorNode))return;const text=sel.toString().trim();!text||text.length>=40||text.includes(`
`)||/^[»„“"']/.test(text)||assignModeSeg&&(_abJustHandledSelection=!0,assignName(text),sel.removeAllRanges())}),chars.addEventListener("click",e=>{const chip=e.target.closest(".ab-chip");if(chip){let name="";for(const n of chip.childNodes)n.nodeType===3&&(name+=n.nodeValue);if(name=name.trim(),assignModeSeg)name&&assignName(name);else if(name){const rows=Array.from(feed.querySelectorAll(".ab-cv-row")).filter(r=>r.__seg&&r.__seg.speaker===name);if(!rows.length)return;const currentY=feed.scrollTop;let target=rows.find(r=>r.offsetTop-feed.offsetTop>currentY+10);target||(target=rows[0]),_abScrollIntoView(target,{block:"center"}),target.style.transition="background 0.3s",target.style.background="var(--accent-hover, rgba(100, 150, 255, 0.2))",setTimeout(()=>{target.parentNode&&(target.style.background="")},1e3)}}}),{recountRoster(allSegs){_abRecountRoster(allSegs||[])},rebuild(segs){var _a3,_b3;this.clearProcessing(),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),_abRedrawSegmentsChunked(segs||[])},update(done){const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!1);const pct=Math.round(done/total*100);fill&&(fill.style.width=pct+"%"),count&&(count.hidden=!1,count.textContent=`passage ${done} / ${total}`);const fillText=panel.querySelector("#ab-cv-fill-text");fillText&&(fillText.textContent=`${pct}% (Passage ${done} of ${total})`)},processing(text){this._procRow&&this._procRow.remove(),this._thinkRaw="",this._thinkDone=!1,this._procRow=document.createElement("div"),this._procRow.className="ab-cv-row is-processing ab-cv-llm-row";const isLong=text.length>160,preview=escHtml(text.slice(0,160))+(isLong?"\u2026":"");this._procRow.innerHTML=`
<div class="ab-cv-llm-head">
<span class="ab-cv-spk" style="color:var(--blue)"><span class="mdi mdi-loading mdi-spin"></span> LLM Reading\u2026</span>
<button class="ab-cv-expand-btn" type="button" title="Show passage &amp; thinking" aria-expanded="false"><span class="mdi mdi-chevron-right"></span></button>
</div>
<div class="ab-cv-llm-preview">${preview}</div>
<div class="ab-cv-llm-split" hidden>
<div class="ab-cv-llm-col">
<div class="ab-cv-llm-col-label"><span class="mdi mdi-text-long"></span> Passage</div>
<pre class="ab-cv-llm-pre">${escHtml(text)}</pre>
</div>
<div class="ab-cv-llm-col">
<div class="ab-cv-llm-col-label ab-cv-think-label"><span class="mdi mdi-head-dots-horizontal-outline"></span> LLM Thinking\u2026</div>
<pre class="ab-cv-think"></pre>
</div>
</div>
`,this._procRow.querySelector(".ab-cv-expand-btn").addEventListener("click",function(){const split=this.closest(".ab-cv-llm-row").querySelector(".ab-cv-llm-split"),open=this.getAttribute("aria-expanded")==="true";this.setAttribute("aria-expanded",String(!open)),split.hidden=open;const icon=this.querySelector("span");icon&&(icon.className=open?"mdi mdi-chevron-right":"mdi mdi-chevron-down")}),_abPage().appendChild(this._procRow),trim()},thinking(delta){if(!this._procRow||!delta||this._thinkDone)return;this._thinkRaw=(this._thinkRaw||"")+delta;const think=this._procRow.querySelector(".ab-cv-think"),label=this._procRow.querySelector(".ab-cv-think-label");if(!think)return;const openAt=this._thinkRaw.indexOf("<think>");let shown,isReal=!1,closeAt=-1;if(openAt>=0){const afterOpen=this._thinkRaw.slice(openAt+7);closeAt=afterOpen.indexOf("</think>"),shown=closeAt>=0?afterOpen.slice(0,closeAt):afterOpen,isReal=!0}else shown=this._thinkRaw;label&&(label.innerHTML=isReal?'<span class="mdi mdi-head-dots-horizontal-outline"></span> LLM Thinking\u2026':'<span class="mdi mdi-code-json"></span> Live Output (raw \u2014 no reasoning exposed)'),think.textContent=shown.trim().slice(-2e4),think.scrollTop=think.scrollHeight,closeAt>=0&&(this._thinkDone=!0)},clearProcessing(){this._procRow&&(this._procRow.remove(),this._procRow=null)},addSegments(segs){var _a3,_b3;this.clearProcessing(),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const makeRow=s=>{const{speakerName}=_abRowSpeaker(s);return colorFor(speakerName),roster.get(speakerName).count++,_abRowFromSegment(s)},CHUNK=80,first=segs.slice(0,CHUNK),frag0=document.createDocumentFragment();if(first.forEach(s=>frag0.appendChild(makeRow(s))),_abPage().appendChild(frag0),renderRoster(),segs.length>CHUNK){let i=CHUNK;const next=()=>{if(i>=segs.length){trim(),renderRoster();return}const batch=document.createDocumentFragment(),end=Math.min(i+CHUNK,segs.length);for(;i<end;i++)batch.appendChild(makeRow(segs[i]));_abPage().appendChild(batch),renderRoster(),setTimeout(next,0)};setTimeout(next,0)}else trim()},note(text){const r=document.createElement("div");r.className="ab-cv-note",r.textContent=text,_abPage().appendChild(r),trim()},divider(prevIdx,nextIdx){this.clearProcessing();const r=document.createElement("div");r.className="ab-cv-divider ab-cv-divider-expand";const gapFrom=Math.max(0,prevIdx>=0?prevIdx+1:0),segsAll=_audiobook.segments||[],gapTo=Math.min(segsAll.length-1,nextIdx>=0?nextIdx-1:segsAll.length-1),gapCount=Math.max(0,gapTo-gapFrom+1);r.innerHTML=`\u22EF <span class="ab-cv-divider-hint">${gapCount>0?gapCount+" line"+(gapCount!==1?"s":"")+" hidden \u2014 ":""}click to expand</span>`,r.title="Click to reveal the text between these passages",r.addEventListener("click",()=>{const gap=segsAll.slice(gapFrom,gapTo+1);if(!gap.length){r.remove();return}const PEEK=20,show=gap.slice(0,PEEK),rest=gap.slice(PEEK),frag=document.createDocumentFragment();for(const s of show){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`<span class="ab-cv-spk" style="color:${c}" title="Click to assign character">${escHtml(spk)}${s.emotion?' <span style="text-transform:lowercase;font-weight:normal;opacity:.8">('+escHtml(s.emotion)+")</span>":""}</span><span class="ab-cv-txt">${highlightText2(s.text||"")}</span>`,frag.appendChild(row)}if(rest.length){const next=document.createElement("div");next.className="ab-cv-divider ab-cv-divider-expand",next.innerHTML=`\u22EF <span class="ab-cv-divider-hint">${rest.length} line${rest.length!==1?"s":""} hidden \u2014 click to expand</span>`,next.title="Click to reveal more";const restSegs=rest;next.addEventListener("click",function(){const f2=document.createDocumentFragment();for(const s of restSegs){const isNarr=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator",spk=isNarr?"Narrator":s.speaker,c=colorFor(spk),row=document.createElement("div");row.className="ab-cv-row ab-cv-ctx-row"+(isNarr?" is-narr":""),row.__seg=s,row.innerHTML=`<span class="ab-cv-spk" style="color:${c}" title="Click to assign character">${escHtml(spk)}${s.emotion?' <span style="text-transform:lowercase;font-weight:normal;opacity:.8">('+escHtml(s.emotion)+")</span>":""}</span><span class="ab-cv-txt">${highlightText2(s.text||"")}</span>`,f2.appendChild(row)}next.replaceWith(f2)}),frag.appendChild(next)}r.replaceWith(frag)}),_abPage().appendChild(r),trim()},pagemark(pageNum){this.clearProcessing(),_abNewPage("Page "+pageNum,pageNum),trim()},complete(summary,onOpen,onRecast,onRecastUnknown){var _a3,_b3;count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0);const completedSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length?_audiobook.segments:typeof allSegments!="undefined"&&allSegments.length?allSegments:null;completedSegments&&_abRedrawSegmentsChunked(completedSegments);const cancelBtn=panel.querySelector("#ab-cv-cancel");cancelBtn&&(cancelBtn.hidden=!0);const foot=panel.querySelector("#ab-cv-foot");foot.hidden=!1;const sideEl=panel.querySelector(".ab-cv-side");if(sideEl){let doneEl=sideEl.querySelector(".ab-cv-side-done");doneEl||(doneEl=document.createElement("div"),doneEl.className="ab-cv-side-done",sideEl.appendChild(doneEl)),doneEl.innerHTML=`<span class="ab-cv-done"><span class="mdi mdi-check-circle-outline"></span> ${escHtml(summary)}</span>`}const continueBtnHtml=_audiobook.completedChunks>0&&_audiobook.completedTotal>0&&_audiobook.completedChunks<_audiobook.completedTotal?`<button class="btn-primary btn-sm" id="ab-cv-continue" style="margin-right:8px;" title="Continue casting from passage ${_audiobook.completedChunks+1} of ${_audiobook.completedTotal}, keeping already-cast passages"><span class="mdi mdi-play-circle-outline"></span> Continue casting</button>`:"";foot.innerHTML=`<span style="flex:1 1 220px; min-width:220px;"></span>${continueBtnHtml}<button type="button" class="btn-secondary btn-sm ab-foot-trigger" id="ab-cv-menu-identify" style="margin-right:8px;" title="Scan or verify character speakers"><span class="mdi mdi-text-search"></span> Identify Characters <span class="mdi mdi-chevron-down"></span></button><button type="button" class="btn-secondary btn-sm ab-foot-trigger" id="ab-cv-menu-cast" style="margin-right:8px;" title="Generate character sheet profiles"><span class="mdi mdi-account-multiple-plus-outline"></span> Cast Characters <span class="mdi mdi-chevron-down"></span></button><button type="button" class="btn-secondary btn-sm ab-foot-trigger" id="ab-cv-menu-viewcast" style="margin-right:8px;" title="View or export the cast"><span class="mdi mdi-eye-outline"></span> Cast <span class="mdi mdi-chevron-down"></span></button><button class="btn-primary btn-sm" id="ab-cv-open-reh" title="Open Script Rehearser to assign voices to each character and rehearse the cast"><span class="mdi mdi-drama-masks"></span> Open Script Rehearser</button>`;const verificationPrompt=`Du bist ein Qualit\xE4tspr\xFCfer f\xFCr die Sprecherzuordnung eines bereits analysierten deutschen H\xF6rbuch-Textes. Eine erste KI hat jedem Segment bereits einen Sprecher zugewiesen ('Narrator' oder einen Charakternamen, ggf. 'Unknown'). Du bekommst diese Zuweisung NICHT \u2014 du siehst nur den Text und musst selbst neu urteilen, ob die Zeile zu Narration oder zu gesprochener Rede eines Charakters geh\xF6rt, und falls Dialog: zu welchem. Das ist eine Gegenprobe, keine Neuklassifizierung von Grund auf: dein Job ist nicht "wer k\xF6nnte das gesagt haben", sondern "ist diese Zeile wirklich Narration, oder wurde hier gesprochene Rede in den Erz\xE4hltext hineingezogen?".
WORAUF DU PR\xDCFST \u2014 IN DIESER REIHENFOLGE (h\xF6chste Priorit\xE4t zuerst):
1. ZUERST alle 'Unknown'-Zeilen: L\xF6se den Sprecher \xFCber Kontext auf (Inquit-Formeln, Ping-Pong-Wechsel in Zwiegespr\xE4chen, Adressaten-Bezug, zuletzt genannte Person). 'Unknown' bleibt nur, wenn wirklich keine Ableitung m\xF6glich ist. Das ist die dringendste Kategorie \u2014 eine Zeile ohne jeden Sprecher ist schlimmer als eine falsch zugeordnete.
2. DANACH Zeilenketten mit demselben Sprecher in Folge (2 oder mehr aufeinanderfolgende Zeilen \u2014 egal ob 'Narrator' oder ein Charakter): Das ist die zweith\xE4ufigste Fehlerquelle. Pr\xFCfe jede Zeile in einer solchen Kette einzeln: Ist das wirklich durchgehend derselbe Sprecher, oder wurde eine Sprecherwechsel-Zeile (z.B. eine Antwort einer anderen Figur, oder gesprochene Rede ohne erhaltene Anf\xFChrungszeichen) f\xE4lschlich in die Kette hineingezogen? Pr\xFCfe besonders bei 'Narrator'-Ketten: Ist wirklich jede Zeile neutrale Erz\xE4hlerbeschreibung, oder ist tats\xE4chlich eine Aussage dabei, die ein Charakter so gesagt haben k\xF6nnte \u2014 nur ohne erhaltene Anf\xFChrungszeichen (typisch bei PDF/OCR-Extraktion)? Pr\xFCfe Tonfall, Wortwahl und Perspektive: Klingt eine der Zeilen wie eine direkte Aussage/Reaktion einer Person in der Szene (Ich-Form, Anrede, Ausruf, Frage), nicht wie neutrale Erz\xE4hlerbeschreibung? Dann ist SIE Dialog, nicht Narration \u2014 auch wenn keine \xBB \xAB vorhanden sind, und auch wenn die Zeilen davor/danach in derselben Kette echt narrativ sind.
3. ZULETZT ein allgemeiner Plausibilit\xE4ts-Check bei allen \xFCbrigen, bereits vermuteten Sprechern: Passt Tonfall/Wortwahl der Zeile zu dem, was \xFCber diese Figur im bisherigen Text bekannt ist (Sprechweise, Haltung, Beziehung zu anderen Figuren)? Wenn eine Zeile im Kontext eindeutig zu einer ANDEREN, im Text erkennbaren Person passt, korrigiere sie dorthin.
4. Echte Narration NICHT anfassen: Reine Beschreibung, Handlung, \xDCberg\xE4nge, Kapitelanf\xE4nge bleiben 'Narrator'. Nur weil eine Figur erw\xE4hnt wird, wird der Satz nicht zu ihrem Dialog.
DEDUKTIONS-WERKZEUGE f\xFCr die Sprecherzuordnung:
- Doppelpunkt-Regel: Endet der Satz davor mit ":", spricht dessen Subjekt das Folgende.
- Nachgestellte Zuordnung: Folgt einer als 'Narrator' markierten Zeile direkt eine kurze Inquit-Formel (Sprechverb + Name/Pronomen, z.B. "entgegnete Oberst von Blautann.", ", meldete sich Lysandra zu Wort.", "murmelte er."), dann war die VORHERIGE Zeile in Wahrheit das Zitat dieser Person \u2014 auch ohne Anf\xFChrungszeichen. Das ist der h\xE4ufigste Fehler der ersten Zuordnung: gesprochene Rede wird f\xE4lschlich als Narration markiert, weil die Anf\xFChrungszeichen bei der Extraktion verloren gingen.
- Handlungs-Hinweise (Action Beats): Wer unmittelbar vor oder nach einer fraglichen Zeile AKTIV HANDELT (aufblickt, sich umdreht, den Kopf sch\xFCttelt), ist der wahrscheinlichste Sprecher dieser Zeile.
- ZUH\xD6RER-AUSSCHLUSS (negative Evidenz): Wahrnehmungs- und Reaktionsverben kennzeichnen den ZUH\xD6RER, nicht den Sprecher ("Marcian sp\xFCrte ihre Tr\xE4nen", "Marcian h\xF6rte dar\xFCber hinweg" \u2192 Marcian h\xF6rt zu, jemand anderes spricht). Ebenso ist eine Person, die im Zitat per Vokativ ANGEREDET wird ("..., Marcian."), nicht der Sprecher. Pr\xFCfe diese Ausschl\xFCsse ZUERST, bevor du einen Namen aus dem Nachbarsatz \xFCbernimmst \u2014 das ist die h\xE4ufigste Fehlerquelle bei nicht zugeordneten Zeilen.
- Pronomen-Inquit bei geteilten Zitaten: Eine Zeile wie ", antwortete er knapp." geh\xF6rt zum vorherigen Zitat; l\xF6se das Pronomen auf die zuletzt genannte Person passenden Geschlechts VOR dem Zitat auf (er \u2192 letzter Mann, sie \u2192 letzte Frau).
- Ping-Pong-Prinzip: Zwei Personen im Gespr\xE4ch wechseln sich strikt ab, auch ohne Tags \u2014 verfolge die Kette zur letzten eindeutigen Nennung zur\xFCck.
- Pronomen-Aufl\xF6sung: er/sie/es meint die zuletzt genannte Person passenden Geschlechts.
- Rollenbezeichnungen sind g\xFCltige Sprecher (z.B. 'Ork', 'Der Fremde', 'W\xE4chter') statt 'Unknown'.
F\xDCR JEDES SEGMENT AUSGABE:
- speaker: 'Narrator' f\xFCr Narration, oder EXAKT der Name des Charakters (nutze den in der bekannten Charakterliste gef\xFChrten Namen, nicht einen Spitznamen/Alias, falls der Kontext eindeutig zuordnet).
- type: 'narration' oder 'dialogue'
- text: EXAKT der WORTW\xD6RTLICHE Originaltext \u2014 KEINE \xC4nderungen, KEINE Auslassungen, KEINE Erg\xE4nzungen.
- emotion: Bei Dialogen 1-2 deutsche W\xF6rter f\xFCr den Tonfall. Bei Narration leer ('').
ABSOLUTE REGELN:
- Alle Segmente zusammen M\xDCSSEN den Originaltext exakt, l\xFCckenlos und wortgetreu rekonstruieren.
- Erfinde NIEMALS Text. Lasse NIEMALS W\xF6rter weg. F\xFCge NIEMALS etwas hinzu.
- Mische NIEMALS Narration und Dialog in einem Segment.
- Sei konservativ: \xE4ndere eine Zeile nur, wenn du nach dieser Pr\xFCfung wirklich zu einem ANDEREN Ergebnis kommst als naheliegend w\xE4re \u2014 nicht jede Zeile muss sich \xE4ndern.`;window._abVerificationPrompt=verificationPrompt;const runVerificationPass=()=>{const choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);closePanel(),audiobookRecastUnknown(savedChoice.url,savedChoice.model,{prompt:verificationPrompt,includeNarrator:!0})},runConsistencyPass=async()=>{const active=_abActiveSegments(),segs=active.arr;if(!segs.length){toast("Nothing cast yet to check","error");return}const byName=new Map;segs.forEach((s,idx)=>{s.type!=="dialogue"||!s.speaker||/^Narrator$/i.test(s.speaker)||/^Unknown|Unbekannt/i.test(s.speaker)||(byName.has(s.speaker)||byName.set(s.speaker,[]),byName.get(s.speaker).push(idx))});const MIN_LINES=4,MAX_LINES_PER_CALL=60,candidates=[...byName.entries()].filter(([,idxs])=>idxs.length>=MIN_LINES);if(!candidates.length){toast("No characters with enough lines yet to check for consistency","error");return}const choice2=audiobookCurrentCastLlm(panel),savedChoice2=audiobookSaveLlmChoice(choice2.url,choice2.model);closePanel(),_abPushEditState(active.key,segs);const busy=_abShowBusyOverlay(`Checking voice consistency for ${candidates.length} character${candidates.length!==1?"s":""}\u2026`,!0);let checked=0,flaggedTotal=0,failedNames=[];const touchedSegs=[],knownNames=[...roster.keys()];try{for(const[name,idxs]of candidates){busy.setProgress(checked,candidates.length);let sampleIdxs=idxs;if(idxs.length>MAX_LINES_PER_CALL){const step=idxs.length/MAX_LINES_PER_CALL;sampleIdxs=Array.from({length:MAX_LINES_PER_CALL},(_,i)=>idxs[Math.floor(i*step)])}const lines=sampleIdxs.map(i=>({index:i,text:segs[i].text}));try{const r=await fetch("/api/audiobook-consistency-check",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({character:name,lines,known_characters:knownNames.filter(n=>n.toLowerCase()!==name.toLowerCase()),llm_url:savedChoice2.url,model:savedChoice2.model})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const data=await r.json(),outliers=Array.isArray(data==null?void 0:data.outliers)?data.outliers:[];for(const o of outliers){let idx=o.index;const quote=String(o.quote||"").trim().toLowerCase(),matchesQuote=i=>{var _a4;return!quote||(((_a4=segs[i])==null?void 0:_a4.text)||"").toLowerCase().includes(quote.slice(0,40))};if((typeof idx!="number"||!segs[idx]||segs[idx].speaker!==name||!matchesQuote(idx))&&(idx=quote?sampleIdxs.find(i=>segs[i].speaker===name&&matchesQuote(i)):void 0),typeof idx!="number"||!segs[idx]||segs[idx].speaker!==name)continue;const suggested=String(o.suggested_speaker||"").trim();let newSpeaker=null;!suggested||/^unknown|unbekannt$/i.test(suggested)?newSpeaker="Unknown":newSpeaker=knownNames.find(n=>n.toLowerCase()===suggested.toLowerCase())||null,!(!newSpeaker||newSpeaker===name)&&(segs[idx].speaker=newSpeaker,newSpeaker==="Unknown"&&(segs[idx].type="dialogue"),touchedSegs.push(segs[idx]),flaggedTotal++)}}catch(err){failedNames.push(name),console.error("[consistency check]",name,err)}checked++,busy.setProgress(checked,candidates.length)}}finally{busy.remove()}flaggedTotal&&(await _abPatchScatteredRows(touchedSegs,()=>{})||await _abRedrawSegmentsChunked(active.arr),_abPersistManualEdit());const failSuffix=failedNames.length?` (${failedNames.length} character${failedNames.length!==1?"s":""} failed to check: ${failedNames.slice(0,5).join(", ")})`:"";toast((flaggedTotal?`Consistency check: ${flaggedTotal} line${flaggedTotal!==1?"s":""} reassigned across ${checked} character${checked!==1?"s":""}`:`Consistency check: no mismatches found across ${checked} character${checked!==1?"s":""}`)+failSuffix,failedNames.length&&!flaggedTotal?"error":"success")};foot.querySelector("#ab-cv-open-reh").addEventListener("click",async()=>{closePanel(),await audiobookOpenCurrentInRehearser()});const applyPromptAndRun2=callback=>{const newPrompt=panel.querySelector("#ab-cv-prompt-text").value,choice=audiobookCurrentCastLlm(panel),savedChoice=audiobookSaveLlmChoice(choice.url,choice.model);typeof _appSettings!="undefined"&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt})}).finally(()=>{closePanel(),callback&&callback(savedChoice.url,savedChoice.model)})},runIdentifyAll=async()=>{await confirmDialog("Start from scratch? This will discard the current cast for this book and rebuild the character definitions from the text.",{title:"Discard current cast?",okLabel:"Start from scratch",danger:!0})&&applyPromptAndRun2(audiobookCast)},runIdentifyUnknown=()=>applyPromptAndRun2(audiobookRecastUnknown),runRepairNarration=()=>{const segs=_audiobook.segments||[],n=typeof _abSuspectNarration=="function"?_abSuspectNarration(segs).size:0;if(!n){toast("No narration lines look like mislabeled dialogue","success");return}toast(`Re-checking ${n} narration line${n!==1?"s":""} that look like spoken dialogue\u2026`,"info"),audiobookRecastUnknown(null,null,{repairNarration:!0,prompt:verificationPrompt})},runCastAll=()=>{typeof window.csForReader=="function"?window.csForReader():typeof csForReader=="function"?csForReader():toast("Character sheets not loaded yet","error")},runCastFresh=async()=>{await confirmDialog("Discard every generated character sheet for this book and rebuild every profile from a blank slate? This cannot be undone.",{title:"Discard all character sheets?",okLabel:"Discard & rebuild",danger:!0})&&(typeof window.csForReader=="function"?window.csForReader({fresh:!0}):typeof csForReader=="function"?csForReader({fresh:!0}):toast("Character sheets not loaded yet","error"))},runCastSelected=()=>{if(!existing.length){toast("No character sheets found yet for this book","error");return}_abOpenRecastSelectPopup(existing)},runCastContinueUncasted=async()=>{if(typeof clGetAllByTagOrBook!="function"||typeof csForReaderSelective!="function"||typeof CS_DETAIL_FIELDS=="undefined"){toast("Character sheets not loaded yet","error");return}let recs=[];try{recs=await clGetAllByTagOrBook(bookTitle)}catch{}const recByName=new Map;recs.forEach(r=>{var _a4;const name=String(r.name||"").trim().toLowerCase();if(name&&recByName.set(name,r),typeof clSplitIdentityTokens=="function")for(const a of clSplitIdentityTokens((_a4=r==null?void 0:r.sheet)==null?void 0:_a4.aliases,{aliases:!0})){const key=a.toLowerCase();recByName.has(key)||recByName.set(key,r)}});const roster2=(_audiobook.roster||[]).filter(n=>n&&!/^unknown$|^unbekannt$/i.test(n.trim())),MIN_LINES_TO_TRY=10,lineCounts=new Map;(_audiobook.segments||[]).forEach(s=>{if((s==null?void 0:s.type)!=="dialogue"||!s.speaker)return;const k=String(s.speaker).trim().toLowerCase();lineCounts.set(k,(lineCounts.get(k)||0)+1)});const tooSparse=[],incomplete=roster2.filter(n=>{const rec=recByName.get(String(n).trim().toLowerCase());return rec&&CS_DETAIL_FIELDS.some(f=>String((rec.sheet||{})[f]||"").trim())?!1:(lineCounts.get(String(n).trim().toLowerCase())||0)<MIN_LINES_TO_TRY?(tooSparse.push(n),!1):!0});if(!incomplete.length){toast(tooSparse.length?`Nothing left to try \u2014 ${tooSparse.length} character${tooSparse.length!==1?"s have":" has"} fewer than ${MIN_LINES_TO_TRY} lines and ${tooSparse.length!==1?"were":"was"} skipped`:"Every character already has a profile with detail \u2014 nothing to continue","success");return}toast(`Continuing ${incomplete.length} uncasted character${incomplete.length!==1?"s":""}`+(tooSparse.length?` (skipped ${tooSparse.length} with fewer than ${MIN_LINES_TO_TRY} lines)`:"")+"\u2026","info"),csForReaderSelective(incomplete)},runViewCast=()=>{var _a4;if((_a4=document.getElementById("s-caststudio"))!=null&&_a4.classList.contains("is-active")&&typeof window.showStudioPhase=="function"){window.showStudioPhase(3);return}try{sessionStorage.setItem("ttsvc_cast_return","reader")}catch{}typeof navTo=="function"&&navTo("s-library"),typeof navLibraryView=="function"&&navLibraryView("characters")},runLibraryCleanup=async()=>{if(typeof clGetAllByTagOrBook!="function"||typeof clDelete!="function"){toast("Character library is not available right now","error");return}const roster2=(_audiobook.roster||[]).filter(n=>n&&!/^unknown$|^unbekannt$/i.test(n.trim()));if(!roster2.length){toast("No current roster to clean up against","error");return}const rosterSet=new Set(roster2.map(n=>n.trim().toLowerCase()));let recs=[];try{recs=await clGetAllByTagOrBook(bookTitle)}catch{}const toDelete=recs.filter(r=>!rosterSet.has(String(r.name||"").trim().toLowerCase()));if(!toDelete.length){toast(`Library already matches the current ${roster2.length}-name roster \u2014 nothing to remove`,"success");return}const preview=toDelete.slice(0,12).map(r=>r.name).join(", ")+(toDelete.length>12?`, +${toDelete.length-12} more`:"");if(!await confirmDialog(`Remove ${toDelete.length} character record${toDelete.length!==1?"s":""} that aren't in the current ${roster2.length}-name roster? This cannot be undone.
${preview}`,{title:"Clean up character library?",okLabel:`Remove ${toDelete.length}`,danger:!0}))return;let removed=0;for(const r of toDelete)try{await clDelete(r.id),removed++}catch{}toast(`Removed ${removed} character record${removed!==1?"s":""} not in the current roster`,"success")},bookTitle=((_a3=window.readerState)==null?void 0:_a3.title)||"";let existing=[];(async()=>{if(!(!bookTitle||typeof clGetAllByTagOrBook!="function"))try{existing=await clGetAllByTagOrBook(bookTitle)}catch{}})();const identifyMenu=foot.querySelector("#ab-cv-menu-identify"),castMenu=foot.querySelector("#ab-cv-menu-cast"),viewCastMenu=foot.querySelector("#ab-cv-menu-viewcast");identifyMenu==null||identifyMenu.addEventListener("click",()=>_abToggleFootMenu(identifyMenu,[{icon:"mdi-refresh",label:"1. Identify all characters",title:"Scan the text and build the cast list from scratch",onClick:runIdentifyAll,danger:!0},{icon:"mdi-auto-fix",label:"2. Auto-repair cast (2.1\u20132.5)",title:"Runs the repair sequence automatically: fix mislabeled dialogue, resolve unknown speakers, repeat while it helps, then a full verification only if still needed",onClick:()=>audiobookAutoRepairCast()},{icon:"mdi-comment-alert-outline",label:"2.1 Repair mislabeled dialogue",title:"Finds spoken lines that were typed as narration (usually because the \xBB \xAB quote marks were lost in extraction) and re-checks only those \u2014 far cheaper than verifying every narration line",onClick:runRepairNarration},{icon:"mdi-account-question-outline",label:"2.2 Identify unknown characters",title:"Re-scan only the unknown segments with the current prompt",onClick:runIdentifyUnknown},{icon:"mdi-repeat",label:"2.3 Run until < N unknown\u2026",title:"Repeats recast-unknown + narrator-verify passes automatically until the Unknown-speaker count drops below a target, or progress stalls",onClick:()=>{const input=window.prompt("Stop once fewer than this many Unknown speakers remain:","10");if(input==null)return;const threshold=parseInt(input,10);if(!Number.isFinite(threshold)||threshold<0){toast("Enter a whole number of 0 or more","error");return}audiobookRecastUntilThreshold(threshold)}},{icon:"mdi-shield-check-outline",label:"2.4 Verify all characters",title:"Second-pass plausibility check that keeps the existing cast and only corrects uncertain matches",onClick:runVerificationPass},{icon:"mdi-account-search-outline",label:"2.5 Check voice consistency",title:"Third-pass check: gathers every line already credited to each character across the whole book and flags any that don\u2019t match their established voice",onClick:runConsistencyPass}])),window._abRunConsistencyPass=runConsistencyPass,castMenu==null||castMenu.addEventListener("click",()=>_abToggleFootMenu(castMenu,[{icon:"mdi-account-multiple-plus-outline",label:"Cast all character roles",title:"Generate / refresh the character sheets for every cast character",onClick:runCastAll},{icon:"mdi-account-arrow-right-outline",label:"Continue uncasted characters",title:"Only generate profiles for characters with no detail yet \u2014 skips anyone already fully cast",onClick:runCastContinueUncasted},{icon:"mdi-account-check-outline",label:"Cast selected character roles",title:existing.length?"Generate / refresh the character sheets for selected cast characters":"No character sheets found yet for this book",disabled:!existing.length,onClick:runCastSelected},{divider:!0},{icon:"mdi-refresh",label:"New recast (discard & rebuild all)",title:"Discard every generated character sheet and rebuild every profile from scratch",onClick:runCastFresh,danger:!0}])),viewCastMenu==null||viewCastMenu.addEventListener("click",()=>_abToggleFootMenu(viewCastMenu,[{icon:"mdi-eye-outline",label:"View cast",title:"View the cast overview in the library",onClick:runViewCast},{icon:"mdi-folder-zip-outline",label:"Export cast archive (.zip)",title:"Download one zip: the cast as a readable Markdown script plus a Markdown sheet per character",onClick:audiobookExportCastMd},{divider:!0},{icon:"mdi-broom",label:"Clean up library to current roster",title:"Remove saved character records that aren't in the current roster \u2014 old spelling-variant duplicates and superseded names from past casting runs",onClick:runLibraryCleanup,danger:!0}])),(_b3=foot.querySelector("#ab-cv-continue"))==null||_b3.addEventListener("click",()=>applyPromptAndRun2((u,m)=>audiobookCast(u,m,{startIndex:_audiobook.completedChunks,segments:_audiobook.segments,roster:_audiobook.roster,narrationOnly:_audiobook.narratedPassages,degraded:_audiobook.degraded}))),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},done(options={}){var _a3,_b3,_c3,_d3,_e3;if(!(options.stopped||_audiobook.cancel)){closePanel();return}this.clearProcessing(),count&&(count.hidden=!0);const castBar=panel.querySelector(".ab-castpanel-bar");castBar&&(castBar.hidden=!0),(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove();const message=options.message||"Casting stopped before any passage completed.";if(feed.querySelector(".ab-cv-row, .ab-cv-note, .ab-cv-divider, .ab-cv-page")){if(options.message){const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}}else{feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='<span class="ab-cv-empty">No completed characters yet.</span>');const foot=panel.querySelector("#ab-cv-foot"),hasSegments=Array.isArray(_audiobook.segments)&&_audiobook.segments.length>0;foot.hidden=!1,foot.innerHTML=`<span class="ab-cv-done"><span class="mdi mdi-stop-circle-outline"></span> ${escHtml(message)}</span><span style="flex:1"></span><button class="btn-secondary btn-sm" id="ab-cv-stopped-back" style="margin-right:8px;"><span class="mdi mdi-arrow-left"></span> Back to reader</button>${hasSegments?'<button class="btn-secondary btn-sm" id="ab-cv-stopped-review" style="margin-right:8px;"><span class="mdi mdi-drama-masks"></span> Open Script Rehearser</button>':""}<button class="btn-primary btn-sm" id="ab-cv-stopped-recast"><span class="mdi mdi-refresh"></span> Cast again</button>`,(_c3=foot.querySelector("#ab-cv-stopped-back"))==null||_c3.addEventListener("click",closePanel),(_d3=foot.querySelector("#ab-cv-stopped-review"))==null||_d3.addEventListener("click",async()=>{closePanel(),await audiobookOpenCurrentInRehearser()}),(_e3=foot.querySelector("#ab-cv-stopped-recast"))==null||_e3.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audiobook_prompt:newPrompt||""})}).finally(()=>{closePanel(),audiobookCast(choice.url,choice.model)})}),panel.classList.add("ab-castpanel-done"),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!1)},loadingRestore(){var _a3,_b3;(_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent="Loading saved cast\u2026",feed.appendChild(r),chars.innerHTML='<span class="ab-cv-empty">restoring\u2026</span>';const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent="Checking saved cast before starting a new one.");const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="none")},setFreshState(message="Ready to identify characters."){var _a3,_b3;if((_a3=feed.querySelector(".ab-skel-feed"))==null||_a3.remove(),(_b3=chars.querySelector(".ab-skel-chars"))==null||_b3.remove(),!feed.querySelector(".ab-cv-row, .ab-cv-divider, .ab-cv-page")){feed.innerHTML="",_abCurPage=null;const r=document.createElement("div");r.className="ab-cv-note",r.textContent=message,feed.appendChild(r)}chars.querySelector(".ab-char-item")||(chars.innerHTML='<span class="ab-cv-empty">No saved cast found.</span>');const statusMsg=panel.querySelector("#ab-cv-status-msg");statusMsg&&(statusMsg.style.display="inline-block",statusMsg.textContent=message);const castBtn=panel.querySelector("#ab-cv-start-cast");castBtn&&(castBtn.style.display="inline-block",castBtn.addEventListener("click",()=>{var _a4;const newPrompt=(_a4=panel.querySelector("#ab-cv-prompt-text"))==null?void 0:_a4.value,currentChoice=audiobookCurrentCastLlm(panel),choice=audiobookSaveLlmChoice(currentChoice.url,currentChoice.model);typeof _appSettings!="undefined"&&newPrompt&&(_appSettings.audiobook_prompt=newPrompt),audiobookCast(choice.url,choice.model)}))}}}async function audiobookRecastUnknown(overrideUrl,overrideModel,options={}){var _a2,_b2,_c2,_d2,_e2,_f2;if(_audiobook.running)return;const segs=_audiobook.segments;if(!segs||!segs.length)return;const originalSegments=segs.map(s=>({...s})),countUnknownDialogue=arr=>(arr||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length,beforeUnknownCount=countUnknownDialogue(segs),preResolved=audiobookResolveUnknowns(segs,[],_audiobook.roster||[]);preResolved.length&&toast(`${preResolved.length} Unknown line${preResolved.length!==1?"s":""} resolved by grammar rules`,"success");const unknownIdxs=[],_abRepairSuspects=options.repairNarration?_abSuspectNarration(segs):null;for(let i=0;i<segs.length;i++){const s=segs[i],isUnknownDialogue=s.type==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)),isNarratorCandidate=options.includeNarrator&&s.type==="narration"||options.repairNarration&&s.type==="narration"&&_abRepairSuspects&&_abRepairSuspects.has(i);(isUnknownDialogue||isNarratorCandidate)&&unknownIdxs.push(i)}if(!unknownIdxs.length){toast("No Unknown speakers found","success"),audiobookShowPreview();return}_audiobook.running=!0,_audiobook.cancel=!1,typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!0);const ac=new AbortController;_audiobook.abort=()=>ac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang();let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel()),promptOverride=typeof options.prompt=="string"?options.prompt:null;!promptOverride&&options.repairNarration&&typeof window._abVerificationPrompt=="string"&&(promptOverride=window._abVerificationPrompt);const groups=audiobookRecastGroups(unknownIdxs,segs),view=audiobookCastView(unknownIdxs.length,llm_url,model);view.recountRoster(segs);const _rcAliasMap=new Map;try{const bookTitle=((_a2=window.readerState)==null?void 0:_a2.title)||"",records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(bookTitle):[];for(const rec of records||[])if(rec!=null&&rec.name&&(_rcAliasMap.set(rec.name.toLowerCase(),rec.name),typeof clSplitIdentityTokens=="function"))for(const a of clSplitIdentityTokens(rec.aliases,{aliases:!0}))_rcAliasMap.set(a.toLowerCase(),rec.name)}catch{}const canonicalizeSpeaker=name=>name&&_rcAliasMap.get(name.toLowerCase())||name;view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_b2=document.getElementById("ab-cv-llm-url"))==null?void 0:_b2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_c2=document.getElementById("ab-cv-llm-select"))==null?void 0:_c2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Character definition stopped before any lines were updated."}),toast("Character definition stopped. Existing cast preserved.","info");return}}let done=0,prevIdx=-2,groupsSinceSave=0;const pendingReplacements=new Map,normalizeRecastSegment=(seg,fallback)=>{const type=(seg==null?void 0:seg.type)==="narration"?"narration":"dialogue";return{speaker:type==="narration"?"Narrator":canonicalizeSpeaker((seg==null?void 0:seg.speaker)||(fallback==null?void 0:fallback.speaker)||"Unknown"),type,emotion:type==="dialogue"&&((seg==null?void 0:seg.emotion)||(fallback==null?void 0:fallback.emotion))||"",text:(seg==null?void 0:seg.text)||(fallback==null?void 0:fallback.text)||""}};try{for(let group of groups){if(_audiobook.cancel)break;const unresolvedGroup=[];for(const idx of group)audiobookIsSpeechTagOnly((_d2=segs[idx])==null?void 0:_d2.text)?(segs[idx].type="narration",segs[idx].speaker="Narrator",segs[idx].emotion=""):unresolvedGroup.push(idx);if(!unresolvedGroup.length){group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1],view.update(done,`Correcting narration tags ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}group=unresolvedGroup,group[0]!==prevIdx+1&&view.divider(prevIdx,group[0]),prevIdx=group[group.length-1];const recastCtx=audiobookRecastContext(segs,group),passageText=recastCtx.text,lineLabel=group.length>1?`Attributing lines ${done+1}-${done+group.length} / ${unknownIdxs.length}\u2026`:`Attributing line ${done+1} / ${unknownIdxs.length}\u2026`;view.update(done,lineLabel),view.processing(passageText.trim());let data=null;const recastBody=audiobookAttributeBody({text:passageText.trim(),known_characters:_audiobook.roster.slice(-40),recent:recastCtx.recent||"",language,llm_url:((_e2=document.getElementById("ab-cv-llm-url"))==null?void 0:_e2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_f2=document.getElementById("ab-cv-llm-select"))==null?void 0:_f2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_RECAST_TIMEOUT_MS)},promptOverride);try{try{data=await audiobookAttributeStream(recastBody,view,ac.signal,AUDIOBOOK_RECAST_TIMEOUT_MS)}catch(streamErr){if(streamErr.name==="AbortError")throw streamErr;const r=await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify(recastBody)},AUDIOBOOK_RECAST_TIMEOUT_MS+5e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||e.error||r.statusText||"HTTP "+r.status)}data=await r.json()}}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0;break}view.note(`API Error while checking ${group.length>1?"a group of Unknown lines":"Unknown line"}: ${err.message||err}`);for(const idx of group)view.addSegments([segs[idx]]),done++;continue}if(data&&data.segments){const used=new Set;for(const idx of group){const targetSeg=segs[idx],seqMatch=audiobookFindReturnedSegmentSequence(targetSeg,data.segments,used);if(seqMatch&&seqMatch.segs.length>1){const repl=seqMatch.segs.map(s=>normalizeRecastSegment(s,targetSeg)).filter(s=>(s.text||"").trim()),hasResolved=repl.some(s=>s.type==="narration"||s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)),hasUnresolvedDialogue=repl.some(s=>s.type==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker)));if(repl.length&&hasResolved&&!hasUnresolvedDialogue){seqMatch.idxs.forEach(i=>used.add(i)),pendingReplacements.set(idx,repl),repl.forEach(s=>{s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)&&!_audiobook.roster.includes(s.speaker)&&_audiobook.roster.push(s.speaker)});continue}}const match=audiobookFindReturnedSegment(targetSeg,data.segments,used);if(match&&match.seg.speaker){const speaker=match.seg.type==="narration"?"Narrator":canonicalizeSpeaker(match.seg.speaker);if(!speaker||/^Unknown|Unbekannt/i.test(speaker))continue;used.add(match.idx),targetSeg.type=match.seg.type==="narration"?"narration":"dialogue",targetSeg.speaker=speaker,targetSeg.emotion=targetSeg.type==="dialogue"&&(match.seg.emotion||targetSeg.emotion)||"",targetSeg.type==="dialogue"&&!_audiobook.roster.includes(speaker)&&_audiobook.roster.push(speaker)}}}for(const idx of group)view.addSegments(pendingReplacements.get(idx)||[segs[idx]]),done++;groupsSinceSave++,groupsSinceSave>=10&&_audiobook.lastText&&(groupsSinceSave=0,_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_audiobook.completedChunks||0,_audiobook.completedTotal||0),view.recountRoster(segs))}view.update(_audiobook.cancel?done:unknownIdxs.length)}catch(e){e.name!=="AbortError"&&view.note("Error defining unknowns: "+e.message)}finally{_audiobook.running=!1,_audiobook.abort=null}try{pendingReplacements.size&&[...pendingReplacements.entries()].sort((a,b)=>b[0]-a[0]).forEach(([idx,repl])=>segs.splice(idx,1,...repl));const{segments:deduped,removed:dupRemoved}=_audiobookDedupNearbyDuplicates(segs);dupRemoved&&(segs.splice(0,segs.length,...deduped),view.note(`Removed ${dupRemoved} duplicated line${dupRemoved!==1?"s":""} introduced by this verification pass.`));const afterUnknownCount=countUnknownDialogue(segs);if(afterUnknownCount>beforeUnknownCount){segs.splice(0,segs.length,...originalSegments);const how=_audiobook.cancel?"Stopped run":"Quality run";view.note(`${how} rolled back: Unknown segments increased from ${beforeUnknownCount} to ${afterUnknownCount}. Existing cast preserved.`),toast(`${how} rolled back because it increased Unknown speakers.`,"error")}const _rcDone=_audiobook.completedChunks||segs.length,_rcTotal=_audiobook.completedTotal||_rcDone;if(_audiobook.cancel){_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal),view.done({stopped:!0,message:"Character definition stopped. Existing cast preserved."}),toast("Character definition stopped. Existing cast preserved.","info");return}_audiobook.lastText&&_abSaveDraft(_audiobook.segments||[],_audiobook.roster||[],_audiobook.lastText,_rcDone,_rcTotal);const speakers=new Set(segs.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${segs.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)}catch(e){console.error("[audiobookRecastUnknown] post-processing failed",e),view.done({stopped:!0,message:"Finished checking speakers, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}const AB_AUTO_REPAIR_KEY="ab-auto-repair-enabled";function audiobookAutoRepairEnabled(){try{return localStorage.getItem(AB_AUTO_REPAIR_KEY)!=="0"}catch{return!0}}async function audiobookAutoRepairCast(threshold=10,maxRounds=4){if(!(_audiobook.segments||[]).length){toast("Nothing cast yet to repair","error");return}const countUnknown=()=>(_audiobook.segments||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Cast looks clean \u2014 ${prev} unknown speaker${prev!==1?"s":""}`,"success");return}toast(`Auto-repair: ${prev} unknown speakers \u2014 running repair passes\u2026`,"info");for(let round=1;round<=maxRounds;round++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Auto-repair stopped.","info");return}const now=countUnknown();if(now<=threshold){toast(`Auto-repair done: ${now} unknown left after ${round} round${round!==1?"s":""}.`,"success");return}if(now>=prev)break;prev=now}toast(`2.4 Still ${prev} unknown \u2014 running full verification pass\u2026`,"info"),await audiobookRecastUnknown(null,null,{includeNarrator:!0});const afterVerify=countUnknown();if(typeof window._abRunConsistencyPass=="function"&&!_audiobook.cancel){toast("2.5 Checking voice consistency\u2026","info");try{await window._abRunConsistencyPass()}catch(e){console.warn("[auto-repair 2.5]",e)}}const final=countUnknown();toast(final<=threshold?`Auto-repair done: ${final} unknown left.`:`Auto-repair finished: ${final} unknown remain (target was under ${threshold}).`,final<=threshold?"success":"error")}window.audiobookAutoRepairCast=audiobookAutoRepairCast;async function audiobookRecastUntilThreshold(threshold=10,maxPasses=8){if(_audiobook.running){toast("Casting is already running","error");return}const segs=_audiobook.segments;if(!segs||!segs.length){toast("No cast to check","error");return}const countUnknown=()=>(segs||[]).filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length;let prev=countUnknown();if(prev<=threshold){toast(`Already at ${prev} unknown speakers (target: <${threshold})`,"success");return}toast(`Running recast + verify passes until under ${threshold} unknown speakers (currently ${prev})\u2026`,"info");for(let pass=1;pass<=maxPasses;pass++){if(await audiobookRecastUnknown(null,null,{repairNarration:!0}),_audiobook.cancel){toast("Stopped \u2014 cancelled mid-pass.","info");return}const now=countUnknown();if(now<=threshold){toast(`Done: ${now} unknown speakers remain (target reached in ${pass} pass${pass!==1?"es":""}).`,"success");return}if(now>=prev){toast(`Stopped after ${pass} pass${pass!==1?"es":""}: no further progress (${now} unknown remain, target was <${threshold}). Likely GPU/LLM contention or genuinely ambiguous lines.`,"error");return}prev=now}toast(`Stopped after ${maxPasses} passes: ${prev} unknown speakers remain (target was <${threshold}).`,"error")}window.audiobookRecastUntilThreshold=audiobookRecastUntilThreshold;async function audiobookOpenCastView(){var _a2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}const _curBook=window.readerState&&readerState.savedId||null;_curBook&&_audiobook.bookId&&_audiobook.bookId!==_curBook&&(_audiobook.segments=null,_audiobook.lastText=null,_audiobook.roster=null),_audiobook.bookId=_curBook;const llm_url=audiobookLlmUrl(),model=audiobookLlmModel(),chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text];if(_audiobook.segments&&_audiobook.segments.length>0&&_audiobook.lastText===text){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.rebuild(_audiobook.segments);const speakers=new Set(_audiobook.segments.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${_audiobook.segments.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown);return}const _applyDraft=(draft,view,source)=>{const draftDone=Number.isFinite(Number(draft.done))?Number(draft.done):0,draftTotal=Number.isFinite(Number(draft.total))?Number(draft.total):0;_abStampSegmentPages(draft.segments,draft.pageMarks,text),_audiobook.segments=draft.segments,_audiobook.roster=draft.roster||[],_audiobook.lastText=text,_audiobook.pageMarks=draft.pageMarks||[],_audiobook.rehId=draft.rehId||null,_audiobook.completedChunks=draftDone>0?draftDone:0,_audiobook.completedTotal=draftTotal>0?draftTotal:0,view.rebuild(draft.segments);const ageMs=Date.now()-(draft.savedAt||0),ageMins=Math.round(ageMs/6e4),ageStr=ageMins<1?"gerade eben":ageMins<60?`vor ${ageMins} Min.`:`vor ${Math.round(ageMins/60)} Std.`,pct=draftTotal>0?Math.max(0,Math.min(100,Math.round(draftDone/draftTotal*100))):100,wasDone=draftTotal>0&&draftDone>=draftTotal,canContinue=draftDone>0&&draftTotal>0&&draftDone<draftTotal,src=source==="server"?"\u2601\uFE0F Vom Server geladen":"\u{1F504} Autosave wiederhergestellt";view.note(`${src} \u2014 ${pct}% vollst\xE4ndig, gespeichert ${ageStr}.${canContinue?' Casting wurde unterbrochen \u2014 \u201EContinue casting" setzt an Passage '+(draftDone+1)+' fort, \u201EIdentify all characters" f\xFCr komplette Neuauswertung.':wasDone?"":" Fortschritt wurde gesichert."}`);const speakers=new Set(draft.segments.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),summary=`${speakers.size} Charakter${speakers.size!==1?"e":""} \xB7 ${draft.segments.length} Segmente`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown)},_localDraft=_abLoadDraft(text);if(_localDraft&&_localDraft.segments&&_localDraft.segments.length>0){const view=audiobookCastView(chunks.length,llm_url,model,!1),bookId2=_abBookId();let chosen=_localDraft,source="local",serverDraft=null;if(bookId2){try{serverDraft=await _abLoadDraftServer(bookId2)}catch{}if(serverDraft&&(serverDraft.savedAt||0)>(_localDraft.savedAt||0)){chosen=serverDraft,source="server";try{localStorage.setItem(_abDraftKey(bookId2),JSON.stringify(serverDraft))}catch{}}}_applyDraft(chosen,view,source),bookId2&&source==="local"&&(!serverDraft||(_localDraft.savedAt||0)>(serverDraft.savedAt||0))&&fetch(`/api/reader/docs/${encodeURIComponent(bookId2)}/scripts/cast`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({..._localDraft,bookId:bookId2,title:((_a2=window.readerState)==null?void 0:_a2.title)||_localDraft.title||""})}).catch(()=>{});return}const bookId=_abBookId();if(bookId){const view=audiobookCastView(chunks.length,llm_url,model,!1);view.loadingRestore();try{const serverDraft=await _abLoadDraftServer(bookId);if(serverDraft&&serverDraft.segments&&serverDraft.segments.length>0){try{localStorage.setItem(_abDraftKey(bookId),JSON.stringify(serverDraft))}catch{}_applyDraft(serverDraft,view,"server")}else view.setFreshState("No saved cast was found for this saved book. Starting a new cast will create a new draft.")}catch{view.setFreshState("Saved cast could not be checked. Browser autosave was already searched; starting a new cast will create a new draft.")}return}audiobookCastView(chunks.length,llm_url,model,!0)}async function audiobookCast(overrideUrl,overrideModel,resume){var _a2,_b2;if(_audiobook.running)return;const text=audiobookScopeText();if(!text){toast("Import a document first","error");return}if(typeof parseScript!="function"){toast("Rehearser not loaded yet \u2014 try again in a moment","error");return}_audiobook.running=!0,!!_abBookId()||!await _abEnsureLibraryBook()&&typeof toast=="function"&&toast("Autosave will use this browser only until the book is saved to the library.","info");const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,AUDIOBOOK_CHUNK_CHARS):[text],startIndex=resume&&resume.startIndex>0&&resume.startIndex<chunks.length?resume.startIndex:0,isResume=startIndex>0;_audiobook.cancel=!1,isResume||_abClearDraft(),typeof window.setNavCastingBadge=="function"&&window.setNavCastingBadge(!0);const ac=new AbortController;_audiobook.abort=()=>ac.abort(),overrideUrl&&typeof overrideUrl!="string"&&(overrideUrl=null);const llm_url=overrideUrl||audiobookLlmUrl(),language=audiobookLang(text);let model=audiobookSafeLlmModel(overrideModel||audiobookLlmModel());const view=audiobookCastView(chunks.length,llm_url,model,!1);isResume&&resume.segments&&resume.segments.length&&view.rebuild(resume.segments),view.processing("Waking up LLM model (this may take a few minutes if cold-booting)\u2026");try{await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify({text:"Wake up.",known_characters:[],recent:"",language,llm_url:((_a2=document.getElementById("ab-cv-llm-url"))==null?void 0:_a2.value.trim())||llm_url,model:audiobookSafeLlmModel(((_b2=document.getElementById("ab-cv-llm-select"))==null?void 0:_b2.value)||model),timeout_seconds:audiobookTimeoutSeconds(AUDIOBOOK_WARMUP_TIMEOUT_MS)})},AUDIOBOOK_WARMUP_TIMEOUT_MS+5e3)}catch(err){if(err.name==="AbortError"){_audiobook.cancel=!0,_audiobook.running=!1,_audiobook.abort=null,view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}}const allSegments2=isResume?resume.segments.slice():[];_audiobook.liveSegments=allSegments2;const roster=isResume?(resume.roster||[]).slice():[];let narrationOnly=isResume&&resume.narrationOnly||0,degraded=isResume&&resume.degraded||0,completedChunks=startIndex;_abStartDraftAutosave(()=>{allSegments2.length&&_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)});const _pgMarks=(_audiobook.pageMarks||[]).slice();_pgMarks.length&&_pgMarks[0].offset<=2&&_pgMarks.shift();let _pgMarkIdx=0,_pgCharPos=0,_curPageNum=1;if(isResume){for(let k=0;k<startIndex;k++)_pgCharPos+=chunks[k].length+1;for(;_pgMarkIdx<_pgMarks.length&&_pgMarks[_pgMarkIdx].offset<=_pgCharPos;)_pgMarkIdx++;_pgMarkIdx>0&&(_curPageNum=_pgMarks[_pgMarkIdx-1].page+1)}try{for(let i=startIndex;i<chunks.length&&!_audiobook.cancel;i++){for(view.update(i);_pgMarkIdx<_pgMarks.length&&_pgCharPos>=_pgMarks[_pgMarkIdx].offset;)_curPageNum=_pgMarks[_pgMarkIdx].page+1,view.pagemark(_curPageNum),_pgMarkIdx++;if(_pgCharPos+=chunks[i].length+1,!audiobookHasDialogue(chunks[i])){const seg={speaker:"Narrator",type:"narration",text:chunks[i],emotion:"",page:_curPageNum};allSegments2.push(seg),narrationOnly++,view.addSegments([seg]),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length);continue}const recent=allSegments2.filter(s=>s.type==="dialogue"&&s.speaker&&!/^Unknown|Unbekannt/i.test(s.speaker)).slice(-6).map(s=>`${s.speaker}: ${(s.text||"").slice(0,80)}`).join(`
`);view.processing(chunks[i]);const attributeChunk=async(chunkText,recentCtx,timeoutMs=AUDIOBOOK_ATTRIBUTION_TIMEOUT_MS)=>{var _a3,_b3;const body={text:chunkText,known_characters:roster.slice(-40),recent:recentCtx,language,llm_url:((_a3=document.getElementById("ab-cv-llm-url"))==null?void 0:_a3.value.trim())||llm_url,model:audiobookSafeLlmModel(((_b3=document.getElementById("ab-cv-llm-select"))==null?void 0:_b3.value)||model),timeout_seconds:audiobookTimeoutSeconds(timeoutMs)};try{const d=await audiobookAttributeStream(body,view,ac.signal,timeoutMs);return Array.isArray(d.segments)?d.segments:null}catch(err){if(err.name==="AbortError")throw err}try{const r=await audiobookFetchWithTimeout("/api/attribute-dialogue",{method:"POST",headers:{"Content-Type":"application/json"},signal:ac.signal,body:JSON.stringify(body)},timeoutMs+5e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||e.error||r.statusText||"HTTP "+r.status)}const d=await r.json();return Array.isArray(d.segments)?d.segments:null}catch(err){if(err.name==="AbortError")throw err;return{error:err.message}}};let segs;try{let result=await attributeChunk(chunks[i],recent),data=null;if(result&&!result.error)data=result;else if(result&&result.error){view.note(`\u26A0\uFE0F Passage ${i+1} timed out \u2014 retrying in two halves\u2026`);const half=Math.floor(chunks[i].length/2),splitAt=chunks[i].lastIndexOf(" ",half)||half,chunkA=chunks[i].slice(0,splitAt).trim(),chunkB=chunks[i].slice(splitAt).trim(),resA=await attributeChunk(chunkA,recent,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS),resB=await attributeChunk(chunkB,recent,AUDIOBOOK_ATTRIBUTION_RETRY_TIMEOUT_MS),segsA=Array.isArray(resA)?resA:null,segsB=Array.isArray(resB)?resB:null;if(segsA||segsB){const arrA=segsA||audiobookSplitByQuotes(chunkA),arrB=segsB||audiobookSplitByQuotes(chunkB);data=[...arrA,...arrB],segsA||(degraded++,view.note(`\u26A0\uFE0F Passage ${i+1} (first half) \u2014 auto-detected (retry also failed)`)),segsB||(degraded++,view.note(`\u26A0\uFE0F Passage ${i+1} (second half) \u2014 auto-detected (retry also failed)`))}else view.note(`\u274C Passage ${i+1} \u2014 LLM error: ${result.error}`),data=null}segs=Array.isArray(data)?data:data&&Array.isArray(data.segments)?data.segments:[];const hasDialogue=segs.some(s=>s.type==="dialogue");if(!segs.length||!hasDialogue&&audiobookHasDialogue(chunks[i])){segs=audiobookSplitByQuotes(chunks[i]),degraded++;const named=segs.filter(s=>s.type==="dialogue"&&!/^Unknown|Unbekannt/i.test(s.speaker)).length;view.note(`Passage ${i+1} \u2014 auto-detected dialogue${named?` (${named} speaker${named!==1?"s":""} from tags)`:" (set speakers in review)"}`)}else{let cleanedSegs=[];for(let s of segs){if(s.text=s.text||"",s.type==="dialogue"&&audiobookIsSpeechTagOnly(s.text)&&(s.type="narration",s.speaker="Narrator",s.emotion=""),s.type==="dialogue"&&s.text.trim().length>10){const tText=s.text.trim(),idx=chunks[i].indexOf(tText);if(idx!==-1){const surround=chunks[i].slice(Math.max(0,idx-8),idx)+chunks[i].slice(idx+tText.length,idx+tText.length+8);/[«»„“”"‟‚‘’›‹『「—–]/.test(surround)||(s.type="narration",s.speaker="Narrator",s.emotion="")}}if(cleanedSegs.length>0){let last=cleanedSegs[cleanedSegs.length-1];if(s.text.trim()===last.text.trim())if(s.type==="dialogue"&&last.type!=="dialogue"){cleanedSegs[cleanedSegs.length-1]=s;continue}else{if(s.type!=="dialogue"&&last.type==="dialogue")continue;continue}if(s.type==="dialogue"&&last.type==="narration"){const tS=s.text.trim(),tL=last.text.trim();tL.endsWith(tS)&&(last.text=tL.slice(0,-tS.length).trim(),last.text||cleanedSegs.pop())}}s.text.trim()&&cleanedSegs.push(s)}let mergedSegs=[];for(let s of cleanedSegs){if(mergedSegs.length>0){let last=mergedSegs[mergedSegs.length-1];if(s.type==="narration"&&last.type==="narration"&&(s.speaker||"Narrator").toLowerCase()==="narrator"&&(last.speaker||"Narrator").toLowerCase()==="narrator"){/[.!?]$/.test(last.text.trim())?last.text=last.text.trimEnd()+`
`+s.text.trimStart():last.text=last.text.trimEnd()+" "+s.text.trimStart();continue}}mergedSegs.push(s)}let respiltSegs=[];for(const s of mergedSegs)s.type==="narration"&&audiobookHasDialogue(s.text)?respiltSegs.push(...audiobookSplitByQuotes(s.text)):respiltSegs.push(s);segs=respiltSegs}}catch(chunkErr){if(chunkErr.name==="AbortError")throw chunkErr;degraded++,segs=audiobookSplitByQuotes(chunks[i]),view.note(`\u274C Passage ${i+1} \u2014 unexpected error (${chunkErr.message}) \u2014 auto-detected dialogue instead`)}audiobookResolveUnknowns(segs,allSegments2.slice(-2),roster),segs.forEach(s=>{const speakerName=s.type!=="dialogue"||!s.speaker||s.speaker.toLowerCase()==="narrator"?"Narrator":s.speaker;!/^Unknown|Unbekannt/i.test(speakerName)&&!roster.includes(speakerName)&&roster.push(speakerName)}),segs.forEach(s=>{s.page=_curPageNum,allSegments2.push(s)}),view.addSegments(segs),completedChunks=i+1,_abSaveDraft(allSegments2,roster,text,completedChunks,chunks.length)}view.update(_audiobook.cancel?completedChunks:chunks.length)}catch(err){err.name==="AbortError"?(_audiobook.cancel=!0,view.note("Casting stopped. Completed passages were preserved.")):(_audiobook.cancel=!0,view.note(`\u274C Casting stopped \u2014 unexpected error: ${err.message}`))}finally{_abStopDraftAutosave(),_audiobook.running=!1,_audiobook.abort=null}if(!allSegments2.length){if(_audiobook.cancel){view.done({stopped:!0,message:"Casting stopped before any passage completed."}),toast("Casting stopped before any passages were saved.","info");return}view.done(),toast("No segments produced","error");return}try{const _quoteFixedSegs=_audiobookFixOrphanedQuoteMarks(allSegments2),{segments:_rejoined,merged:_mergedCount}=_audiobookMergeSplitSentences(_quoteFixedSegs);_mergedCount&&console.info("[cast] rejoined",_mergedCount,"sentence(s) split by quoted terms");const _mergedSegs=_audiobookMergeAdjacentSameSpeaker(_rejoined);allSegments2.length=0,allSegments2.push(..._mergedSegs),_audiobook.segments=allSegments2,_audiobook.lastText=text,_audiobook.roster=roster,_audiobook.narratedPassages=narrationOnly,_audiobook.degraded=degraded,_audiobook.completedChunks=_audiobook.cancel?completedChunks:chunks.length,_audiobook.completedTotal=chunks.length,_abSaveDraft(allSegments2,roster,text,_audiobook.completedChunks,chunks.length),audiobookSaveAsRehearsal({silent:!0}),_audiobook.cancel&&toast("Casting stopped early. Progress preserved.","info");const speakers=new Set(allSegments2.filter(s=>s.type==="dialogue"&&s.speaker).map(s=>s.speaker)),stoppedEarly=_audiobook.cancel&&completedChunks<chunks.length,summary=`${stoppedEarly?"Stopped early \xB7 ":""}${speakers.size} character${speakers.size!==1?"s":""} \xB7 ${allSegments2.length} segments`;view.complete(summary,audiobookShowPreview,audiobookCast,audiobookRecastUnknown),!stoppedEarly&&!_audiobook.cancel&&audiobookAutoRepairEnabled()&&allSegments2.filter(s=>(s==null?void 0:s.type)==="dialogue"&&(!s.speaker||/^Unknown|Unbekannt/i.test(s.speaker))).length>10&&setTimeout(()=>{audiobookAutoRepairCast().catch(e=>console.warn("[auto-repair]",e))},800)}catch(e){console.error("[audiobookCast] post-processing failed",e),view.done({stopped:!0,message:"Finished casting, but saving/cleanup failed: "+e.message+" \u2014 your progress up to this point is kept in memory; try Save or re-open the book to confirm it persisted."}),toast("Casting finished but cleanup failed: "+e.message,"error")}}async function audiobookOpenCurrentInRehearser(){var _a2;const segs=_audiobook.segments||[];if(!segs.length)return toast("No cast to open","error"),!1;if(typeof window.rehLoadRecord=="function"&&typeof _audiobookBuildRehRecord=="function")try{const rec=await _audiobookBuildRehRecord(segs);return _audiobook.rehId&&(rec.id=_audiobook.rehId),(_a2=document.getElementById("audiobook-preview"))==null||_a2.remove(),typeof navTo=="function"&&navTo("s-rehearser"),window.rehLoadRecord(rec),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),toast("Opened cast in Script Rehearser","success"),audiobookSaveAsRehearsal({silent:!0}).catch(()=>{}),!0}catch(err){console.warn("[audiobook] failed to build rehearser record directly, falling back:",err)}await audiobookSaveAsRehearsal({silent:!0});const{script,emotions}=audiobookBuildScript(segs);return await audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions),rehState.lines.length&&typeof buildScriptPage=="function"?(buildScriptPage(),typeof showPhase=="function"&&showPhase(3),typeof highlightCurrentLine=="function"&&highlightCurrentLine()):typeof showPhase=="function"&&showPhase(3),!0}function audiobookShowPreview(){var _a2;audiobookOpenCurrentInRehearser()}function audiobookApplyPreviewAndOpen(){const segs=_audiobook.segments;document.querySelectorAll("#audiobook-seglist .audiobook-seg-sp").forEach(inp=>{const i=+inp.dataset.i,v=inp.value.trim()||"Narrator";segs[i].speaker=v,segs[i].type=v.toLowerCase()==="narrator"?"narration":"dialogue"}),document.querySelectorAll("#audiobook-seglist .audiobook-seg-emo").forEach(inp=>{const i=+inp.dataset.i;segs[i].emotion=inp.value.trim()});const{script,emotions}=audiobookBuildScript(segs);audiobookOpenInRehearser(script,readerState.title||"Audiobook",emotions)}function audiobookBuildScript(segments){let script="";const emotions=[],marks=_audiobook.pageMarks||[],src=_audiobook.lastText||"";let markIdx=0,searchPos=0;marks.length&&marks[0].offset<=2&&(markIdx=1);for(const s of segments){const t=(s.text||"").trim();if(!t)continue;if(src&&markIdx<marks.length){const probe=t.slice(0,24),at=probe?src.indexOf(probe,searchPos):-1,pos=at>=0?at:searchPos;for(at>=0&&(searchPos=at+probe.length);markIdx<marks.length&&pos>=marks[markIdx].offset;)script.trim()&&(script+=`
\f${marks[markIdx].page+1}
`),markIdx++}s.type==="dialogue"&&s.speaker&&s.speaker.toLowerCase()!=="narrator"?(script+=`
`+s.speaker.toUpperCase()+`
`+t+`
`,emotions.push(s.emotion||"")):script+=`
`+t+`
`}return{script:script.trim(),emotions}}async function audiobookOpenInRehearser(script,title,dialogueEmotions){$("reh-script-text")&&($("reh-script-text").value=script),$("reh-script-title")&&($("reh-script-title").value=title),typeof navTo=="function"&&navTo("s-rehearser");const btn=$("reh-parse-btn");btn?btn.click():typeof parseScript=="function"&&(rehState.lines=parseScript(script));let speakers=0,lines=0;if(window.rehState&&Array.isArray(rehState.lines)){let k=0;rehState.lines.forEach(l=>{if(l.type==="dialog"){const e=dialogueEmotions[k++];e&&(l.emotion=e),lines++}}),speakers=Object.keys(rehState.cast||{}).filter(s=>!String(s).includes("NARRATOR")).length}let saved=!1;if(typeof saveToLibrary=="function")try{rehState.savedId=null,await saveToLibrary(),saved=!0}catch{}toast(`Cast ${speakers} character${speakers!==1?"s":""} \xB7 ${lines} lines`+(saved?" \u2014 saved to Rehearser \u2192 Bibliothek":""),"success")}function audiobookIsChapter(line){if(line.type==="act"||line.type==="scene")return!0;const t=(typeof stripMarkdown=="function"?stripMarkdown(line.text||""):line.text||"").trim();if(!t||t.length>60)return!1;const NUM=/^(?:[ivxlcdm]+(?=[.\s]|$)|\d{1,3})\.?\s*/i,rest=t.replace(NUM,""),KEYWORD=/^(chapter|kapitel|chap\.?|part|book|prologue|epilogue|prolog|epilog|teil)\b/i;if(!KEYWORD.test(rest))return!1;const afterKeyword=rest.replace(KEYWORD,"").trim();return afterKeyword===""||/^[ivxlcdm\d]{1,6}\.?$/i.test(afterKeyword)?!0:/^[ivxlcdm\d]{0,6}\.?\s*[-:–—]\s*.{1,40}$/i.test(afterKeyword)}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<fmt.channels;ch++){const data=rendered.getChannelData(Math.min(ch,rendered.numberOfChannels-1));for(let i=0;i<frames;i++){const s=Math.max(-1,Math.min(1,data[i]));pcm[i*fmt.channels+ch]=s<0?s*32768:s*32767}}return _buildWavBlob(new Uint8Array(pcm.buffer),fmt)}catch(e){return console.error("[audiobook export] chapter sound decode failed",e),toast("Could not use the custom chapter sound \u2014 check the audio file","error"),null}}let _abPauseUIInited=!1;function _abInitPauseUI(){const pEl=$("stu-pause-paragraph"),cEl=$("stu-pause-chapter"),sfxFile=$("stu-chapter-sfx-file"),sfxClear=$("stu-chapter-sfx-clear"),sfxCurrent=$("stu-chapter-sfx-current");if(!pEl||!cEl)return;const s=_abPauseSettings();pEl.value=s.paragraphMs/1e3,cEl.value=s.chapterMs/1e3,sfxCurrent&&(sfxCurrent.textContent=_abChapterSfxDataUrl()?"Custom sound set":"No custom sound"),!_abPauseUIInited&&(_abPauseUIInited=!0,pEl.addEventListener("change",()=>_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 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 fetchTtsPreviewBlob(voice,text,"wav",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;c<buckets.length;c++){const blobs=buckets[c].idx.map(i=>wavClips.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=`<div class="audiobook-box" style="max-width:520px;">
<div class="audiobook-title"><span class="mdi mdi-book-music-outline"></span> ${opts.browsing?"Saved audiobook exports":"Audiobook exported"}<span style="flex:1"></span><button type="button" class="btn-secondary btn-sm" id="ab-export-close">Close</button></div>
<p class="card-subtitle" style="margin:8px 0 14px;">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.</p>
<div style="display:flex; flex-direction:column; gap:8px; max-height:50vh; overflow-y:auto;">
${files.map(f=>`
<div style="display:flex; align-items:center; gap:10px; padding:8px 10px; border:1px solid var(--border); border-radius:8px;">
<span class="mdi mdi-file-music-outline" style="opacity:.7"></span>
<span style="flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:13px;" title="${escHtml(f.name)}">${escHtml(f.name)}</span>
<a class="btn-primary btn-sm" href="${f.url}" download="${escHtml(f.name)}"><span class="mdi mdi-download"></span> Download</a>
</div>`).join("")}
</div>
${files.length>1?`<div style="margin-top:14px; text-align:right;"><a class="btn-secondary btn-sm" href="${zipUrl}"><span class="mdi mdi-folder-zip-outline"></span> Download all (.zip)</a></div>`:""}
</div>`,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<emotions.length&&(emotions[eIdx]&&(l.emotion=emotions[eIdx]),eIdx++)})}const cast={};if(typeof detectCharacters=="function"){const detected=detectCharacters(lines);Object.entries(detected).forEach(([sp,def])=>{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)}const CS_CHUNK_CHARS=4e3,_cs={running:!1,cancel:!1,cache:{}},CS_DEFAULT_PROMPT=`You are an expert dramaturge, developmental editor, and tabletop RPG game master building rich character sheets passage by passage as a book is read. Extract playable, action-oriented sheets an actor can use to immediately know how to PLAY the character.
PROGRESSIVE FILLING: you may be given the sheets built so far. For returning characters, ADD any NEW detail this passage reveals and refine vague fields; do not contradict solid earlier facts or blank out a field you cannot improve. In this cast-character pass, ONLY refine the already-casted roster and do NOT invent new profiles, places, or institutions. The text may be a focused evidence window around a mention, so use the nearby paragraphs as context. Leave a field empty if the book genuinely hasn't shown it yet (a later passage can fill it). Extrapolate from dialogue and actions when reasonable, and mark any deduced value with a trailing ' *'.
For each character output these fields:
- name: canonical display name for this one character. Use the real personal name if known; otherwise use the most stable role/title.
- aliases: ONLY alternate names, roles, epithets, mistranscriptions, and titles proven to refer to the SAME character, comma-separated (max 6 items; e.g. 'the Executioner, Bloodfang, Marcus the Executioner'). Leave empty when uncertain.
- first_name, last_name: split the character identity when known. Leave unknown parts empty.
- title: nobility title only, if the text explicitly gives one (e.g. 'Graf', 'Baron', 'Ritter').
- profession: occupation / job / role in the story (e.g. 'Inquisitor', 'Soldier', 'Merchant', 'Priest').
- age_estimate: estimated age or age range, if inferable
- race_species: species, race, or kind (human, elf, ork, vampire, etc.) if relevant
- languages: spoken languages / dialects / tongues, comma-separated if multiple
- nationality_background: homeland, culture, origin, or social background if known
- social_class: rank or class if the text makes it clear (noble, soldier, slave, merchant, priesthood, etc.)
- archetype: a two-word role summary (e.g. 'Ruthless Scholar')
- gender: 'male', 'female', or 'nonbinary' \u2014 as apparent from the text (pronouns, roles, physical description). Leave empty if genuinely indeterminable.
- physical: height, weight, build, hair, eyes, skin, posture, gait, distinguishing features, physical disabilities, fantasy-specific extras, and any other bodily appearance details. Use metric units when size/weight is known.
- clothing: day-to-day wear, work attire, formal wear, sleepwear, undergarments, accessories, and visible weapons/gear if they define the look
- alignment: strict moral code + the one line they will never cross
- moral_alignment_score: integer 0\u2013100. 100 = purely good/heroic, 0 = purely evil/villainous, 50 = neutral/ambiguous
- arc_direction: one of: 'stable-good', 'stable-bad', 'neutral', 'good-to-bad', 'bad-to-good', 'complex'
- arc_note: one sentence explaining the arc or moral position visible so far
- attribute_high / attribute_low: highest and lowest natural attribute (Charisma, Intelligence, Wisdom, Agility\u2026)
- skills: what they are demonstrably good at in the story
- capabilities: combat, magic, social, technical, or other demonstrated abilities
- backstory: origin, formative background and history revealed in the text
- relationships: key allies, family, rivals and enemies \u2014 name them and how they relate
- motivation: the inner drive \u2014 WHY they pursue what they pursue (distinct from the win condition)
- fears: their deepest fears, phobias or dread
- mannerisms: habitual gestures, tics, body language, habits and quirks
- communication_style: how they communicate socially \u2014 blunt, formal, warm, guarded, sarcastic, etc.
- voice_pattern: speech style \u2014 accent, pacing, vocabulary, register and verbal tics (for voice casting)
- voice_design_prompt: concise English Qwen voice-design prompt (15-45 words). Include age impression, gender/androgyny if inferable, pitch, timbre, pace, accent/register, emotional baseline and suitability for audiobook dialogue. Do NOT mention plot spoilers.
- image_prompt: detailed English image-generation prompt for this character. Include face, age impression, build, hair/eyes/skin if known, clothing, posture, props, mood, genre/style, and visible symbols. Mark inferred traits with '*'.
- inventory: 1-3 defining items/props/clothing (array of short strings)
- secret: dark secret or fatal flaw
- conflict_style: fight, flight, or manipulate \u2014 how they act when cornered
- win_condition: the specific event that would make them feel they have won
- reputation: how other characters or society see them
- religious_beliefs: faith, religion, worship, or lack of belief if the text shows it
- notes: brief catch-all notes for useful details that do not fit elsewhere
- tier: 'main' or 'supporting'
- sources: array of {page, quote, line_hint} \u2014 the page number from the nearest [p.N] marker, a short verbatim quote that supports the sheet (1-12 entries), and a brief label (e.g. 'physical', 'clothing', 'relationships', 'motivation'). Use null page if unknown. line_hint MUST name the supported field when possible: physical, clothing, relationships, motivation, fears, mannerisms, voice_pattern, backstory, alignment, skills, capabilities, secret, conflict_style, or win_condition.
IDENTITY MERGING: A character may appear under multiple names in the book (first name, last name, title, role, alias, nickname). Examples: 'Marcus', 'the Executioner', and 'Bloodfang' may all refer to ONE profile if context shows they are the same person. Do NOT create separate sheets for aliases/titles of the same person; put the alternate forms in aliases/title and keep one canonical name.
Reuse the EXACT names from the known-characters list for returning characters when they are the canonical name or an alias of this character. Do not merge characters merely because their names appear near each other, in the known-character list, or in relationships.
Respond with STRICT JSON only:
{"sheets":[{"name":"","aliases":"","first_name":"","last_name":"","title":"","profession":"","age_estimate":"","race_species":"","languages":"","nationality_background":"","social_class":"","archetype":"","gender":"","physical":"","clothing":"","alignment":"","moral_alignment_score":50,"arc_direction":"neutral","arc_note":"","attribute_high":"","attribute_low":"","skills":"","capabilities":"","backstory":"","relationships":"","motivation":"","fears":"","mannerisms":"","communication_style":"","voice_pattern":"","voice_design_prompt":"","image_prompt":"","inventory":[],"secret":"","conflict_style":"","win_condition":"","reputation":"","religious_beliefs":"","notes":"","tier":"main","sources":[{"page":1,"quote":"","line_hint":""}]}]}
/no-think`;function csLlmUrl(){var _a2;const explicit=(_a2=$("reh-llm-url"))==null?void 0:_a2.value.trim();if(explicit)return explicit;const fromSettings=typeof _appSettings!="undefined"&&_appSettings&&_appSettings.llm_url?_appSettings.llm_url:"";return fromSettings&&!/localhost:11434|127\.0\.0\.1:11434/.test(fromSettings)?fromSettings:""}function csLlmModel(){var _a2;return((_a2=$("reh-llm-model"))==null?void 0:_a2.value)||""}function csLang(){var _a2;return((_a2=$("reh-design-lang"))==null?void 0:_a2.value)||""}function csReaderPageHost(){return $("reader-charsheets-panel")}function csReaderText(){var _a2,_b2,_c2;if(typeof readerScopeIndices!="function"||!((_a2=readerState==null?void 0:readerState.sentences)!=null&&_a2.length))return"";let out="",lastPage=-1;for(const i of readerScopeIndices()){const u=readerState.sentences[i],pg=(_c2=(_b2=u.words)==null?void 0:_b2[0])==null?void 0:_c2.page;readerState.mode==="pdf"&&pg!=null&&pg!==lastPage&&(out+=`
[p.${pg+1}] `,lastPage=pg),out+=u.text+" "}return out.trim()}function csRehearserText(){if(!window.rehState||!(rehState.lines||[]).length)return"";let out="",page=1,started=!1;for(const l of rehState.lines){if(l.type==="pagebreak"){page++,out+=`
[p.${page}] `;continue}const t=(typeof stripMarkdown=="function"?stripMarkdown(l.text||""):l.text||"").trim();t&&(started||(out+="[p.1] ",started=!0),out+=(l.type==="dialog"&&l.speaker?l.speaker+": ":"")+t+`
`)}return out.trim()}const CS_SCALAR_FIELDS=["aliases","first_name","last_name","full_name","title","profession","age_estimate","race_species","languages","nationality_background","social_class","archetype","physical","clothing","alignment","arc_note","attribute_high","attribute_low","skills","capabilities","backstory","relationships","motivation","fears","mannerisms","communication_style","voice_pattern","secret","conflict_style","win_condition","reputation","religious_beliefs","notes","voice_design_prompt","image_prompt","silly_tavern_prompt","concept_art_prompt"],CS_DETAIL_FIELDS=CS_SCALAR_FIELDS.filter(f=>!["aliases","first_name","last_name","full_name","title"].includes(f)),CS_IDENTITY_FIELDS=["name","aliases","first_name","last_name","full_name"],CS_ALIAS_MAX_TOKENS=12,CS_ALIAS_MAX_CHARS=500,CS_SOURCE_FIELDS={physical:["physical","appearance","body","look"],clothing:["clothing","appearance","armour","armor","item"],age_estimate:["age","older","young","teen","adult"],race_species:["race","species","kind","orc","elf","human","vampire"],languages:["language","languages","tongue","dialect","speech"],nationality_background:["nationality","background","origin","homeland","culture"],social_class:["class","social","status","rank"],relationships:["relationship","ally","rival","enemy","family"],motivation:["motivation","intention","goal","desire"],fears:["fear","dread"],mannerisms:["mannerism","habit","gesture","voice"],communication_style:["communication","communicate","demeanor","style"],voice_pattern:["voice","speech","dialogue"],backstory:["backstory","origin","history"],alignment:["alignment","ethos","morality"],profession:["profession","occupation","job","role"],skills:["skill","capability","ability"],capabilities:["capability","ability","combat","magic"],reputation:["reputation","known as","status","perceived"],religious_beliefs:["religion","faith","belief","priest","god","gods","temple"],notes:["note","misc","miscellaneous"],secret:["secret","flaw"],conflict_style:["conflict","fight","flight","manipulate"],win_condition:["win","goal"]};function csExistingSummary(map){if(!map.size)return"";const KEY_FIELDS=["age_estimate","race_species","languages","nationality_background","social_class","profession","physical","clothing","communication_style","voice_pattern","backstory","motivation","relationships","reputation","religious_beliefs","notes"];return[...map.values()].slice(0,40).map(s=>{const missing=KEY_FIELDS.filter(f=>!(s[f]||"").trim()),suffix=missing.length<KEY_FIELDS.length?` | needs: ${missing.join(", ")}`:" | complete",aliases=[s.title,s.profession,s.aliases].filter(Boolean).join(", ");return`- ${s.name}${aliases?` aka ${aliases}`:""}${s.archetype?` (${s.archetype})`:""}${suffix}`}).join(`
`)}function _csStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function csSplitIdentityTokens(v,opts={}){const raw=_csStr(v),parts=raw.split(/[,;/|]|\baka\b|\baka\.\b|\balias(?:es)?\b|\bgenannt\b|\bnamens\b|\bcalled\b|\bknown as\b/i).map(s=>s.trim()).filter(Boolean).filter(s=>s.length<=80&&!/^needs?:/i.test(s)&&!/^complete$/i.test(s));return opts.aliases&&(raw.length>CS_ALIAS_MAX_CHARS||parts.length>CS_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CS_ALIAS_MAX_TOKENS:void 0)}function csCleanRoster(names){const out=[],seen=new Set;return(names||[]).forEach(n=>{const name=_csStr(n).trim(),key=name.toLowerCase();!name||seen.has(key)||key==="narrator"||/^unknown|unbekannt$/i.test(name)||(seen.add(key),out.push(name))}),out}function csRosterKey(v){return String(v||"").trim().toLowerCase()}function csKnownReaderRoster(){const names=[],add=name=>{name=_csStr(name).trim();const key=name.toLowerCase();!name||key==="narrator"||/^unknown|unbekannt$/i.test(name)||names.some(n=>n.toLowerCase()===key)||names.push(name)},ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;return ab&&((ab.roster||[]).forEach(add),[ab.segments,ab.liveSegments].forEach(segs=>{(segs||[]).forEach(s=>{(s==null?void 0:s.type)==="dialogue"&&add(s.speaker)})})),document.querySelectorAll("#ab-cv-chars .ab-char-item[data-name], #audiobook-roster option[value]").forEach(el=>{add(el.dataset.name||el.value||el.textContent)}),csCleanRoster(names)}function csBlankSheet(name){return{name,aliases:"",first_name:"",last_name:"",full_name:"",title:"",profession:"",age_estimate:"",race_species:"",languages:"",nationality_background:"",social_class:"",archetype:"",gender:"",physical:"",clothing:"",alignment:"",arc_note:"",attribute_high:"",attribute_low:"",skills:"",capabilities:"",backstory:"",relationships:"",motivation:"",fears:"",mannerisms:"",communication_style:"",voice_pattern:"",secret:"",conflict_style:"",win_condition:"",reputation:"",religious_beliefs:"",notes:"",voice_design_prompt:"",image_prompt:"",inventory:[],sources:[],moral_alignment_score:50,arc_direction:"neutral",tier:"supporting"}}function csSeedSheet(raw){const name=_csStr(raw==null?void 0:raw.name).trim(),seed=csBlankSheet(name);if(!name)return seed;raw!=null&&raw.image&&(seed._image=raw.image);const src=raw!=null&&raw.sheet&&typeof raw.sheet=="object"?raw.sheet:raw;if(CS_SCALAR_FIELDS.forEach(f=>{src&&src[f]!=null&&(seed[f]=_csStr(src[f]))}),Array.isArray(src==null?void 0:src.inventory)?seed.inventory=[...src.inventory]:typeof(src==null?void 0:src.inventory)=="string"&&(seed.inventory=src.inventory.split(",").map(x=>x.trim()).filter(Boolean)),Array.isArray(src==null?void 0:src.sources)&&(seed.sources=src.sources.map(item=>item&&typeof item=="object"?{page:item.page!=null?item.page:null,quote:_csStr(item.quote).slice(0,240),line_hint:_csStr(item.line_hint).slice(0,60)}:null).filter(Boolean)),src!=null&&src.tier&&(seed.tier=String(src.tier).toLowerCase().startsWith("main")?"main":"supporting"),(src==null?void 0:src.moral_alignment_score)!=null){const mas=parseInt(src.moral_alignment_score,10);Number.isNaN(mas)||(seed.moral_alignment_score=Math.max(0,Math.min(100,mas)))}return src!=null&&src.arc_direction&&(seed.arc_direction=src.arc_direction),src!=null&&src.gender&&(seed.gender=String(src.gender).trim().toLowerCase()),(src==null?void 0:src.line_count)!=null&&(seed.line_count=src.line_count),seed}function csNameTokens(sheet){const out=new Set;for(const f of CS_IDENTITY_FIELDS){const raw=f==="name"?sheet==null?void 0:sheet.name:sheet==null?void 0:sheet[f];csSplitIdentityTokens(raw,{aliases:f==="aliases"}).forEach(s=>out.add(s.toLowerCase()))}return out}function csSheetMatchesRoster(sheet,roster=[]){const allowed=new Set((roster||[]).map(csRosterKey).filter(Boolean));if(!allowed.size)return!0;for(const token of csNameTokens(sheet))for(const name of allowed)if(token===name||token.includes(name)||name.includes(token))return!0;return!1}function csIdentityNeedles(value){return csSplitIdentityTokens(value,{aliases:!0}).filter(v=>{const n=String(v||"").trim();return n&&n.length>1&&!/^(die|der|das|den|dem|des|ein|eine|einer|er|sie|es|ich|du|wir|ihr)$/i.test(n)})}function csRecordNeedles(rec){var _a2,_b2,_c2;const seen=new Set,out=[],add=value=>{csIdentityNeedles(value).forEach(v=>{const key=v.toLowerCase();seen.has(key)||(seen.add(key),out.push(v))})};return add(rec==null?void 0:rec.name),add((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.aliases),add([(_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name,(_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name].filter(Boolean).join(" ")),out}function csParagraphBlocks(text){return String(text||"").replace(/\r\n/g,`
`).split(/\n\s*\n+/).map(s=>s.trim()).filter(Boolean)}function csReaderParagraphBlocks(){var _a2,_b2,_c2;if(readerState.mode==="text"&&readerState.docText)return csParagraphBlocks(readerState.docText);const base=Array.isArray(readerState.baseSentences)&&readerState.baseSentences.length?readerState.baseSentences:Array.isArray(readerState.sentences)?readerState.sentences:[];if(!base.length)return csParagraphBlocks(csReaderText());const paras=[];let cur="",curPage=null;for(const s of base){const page=(_c2=(_b2=(_a2=s==null?void 0:s.words)==null?void 0:_a2[0])==null?void 0:_b2.page)!=null?_c2:null,pageChanged=readerState.mode==="pdf"&&page!=null&&curPage!=null&&page!==curPage;(s!=null&&s.paraStart||pageChanged)&&cur.trim()&&(paras.push(cur.trim()),cur=""),!cur&&readerState.mode==="pdf"&&page!=null&&(cur+=`[p.${page+1}] `),cur+=(cur&&!/\s$/.test(cur)?" ":"")+String((s==null?void 0:s.text)||"").trim(),curPage=page!=null?page:curPage}return cur.trim()&&paras.push(cur.trim()),paras}function csParagraphHasNeedle(paragraph,needle){const p=String(paragraph||""),n=String(needle||"").trim();if(!n||n.length<2)return!1;const lowerP=p.toLowerCase(),lowerN=n.toLowerCase();if(lowerN.includes(" "))return lowerP.includes(lowerN);const esc=lowerN.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return new RegExp(`(^|[^\\p{L}\\p{N}_])${esc}(?=$|[^\\p{L}\\p{N}_])`,"iu").test(p)}function csEvidenceWindowText(text,needles,before=2,after=2){const paras=Array.isArray(text)?text.slice():csParagraphBlocks(text);if(!paras.length)return String(text||"").trim();const hits=new Set;return paras.forEach((para,idx)=>{if((needles||[]).some(n=>csParagraphHasNeedle(para,n)))for(let i=idx-before;i<=idx+after;i++)i>=0&&i<paras.length&&hits.add(i)}),hits.size?[...hits].sort((a,b)=>a-b).map(i=>paras[i]).join(`
`).trim():""}function csFindMergeKey(map,sheet){const incoming=csNameTokens(sheet);if(!incoming.size)return(sheet.name||"").trim().toLowerCase();for(const[key,existing]of map.entries()){const existingNames=csNameTokens(existing);for(const n of incoming)if(existingNames.has(n))return key}return(sheet.name||"").trim().toLowerCase()}function csMergeAliases(existing,incoming){const names=new Map,add=v=>csSplitIdentityTokens(v,{aliases:!0}).forEach(s=>names.set(s.toLowerCase(),s));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function csMerge(map,sheets){const changed=[];for(const s of sheets){const name=(s.name||"").trim();if(!name)continue;s.aliases=csMergeAliases({name,aliases:""},s);const key=csFindMergeKey(map,s);if(!map.has(key)){const copy={...s,name,inventory:[...s.inventory||[]],sources:[...s.sources||[]]};CS_SCALAR_FIELDS.forEach(f=>{copy[f]=_csStr(copy[f])}),map.set(key,copy),changed.push({key,name:copy.name,status:"new"});continue}const e=map.get(key),before=JSON.stringify(e),oldAliases=e.aliases;CS_SCALAR_FIELDS.forEach(f=>{const sv=_csStr(s[f]);sv.length>(e[f]||"").length&&(e[f]=sv)}),e.aliases=csMergeAliases({...e,aliases:oldAliases},s),s.tier==="main"&&(e.tier="main"),s.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+s.moral_alignment_score)/2):s.moral_alignment_score),s.arc_direction&&s.arc_direction!=="neutral"&&(e.arc_direction=s.arc_direction),s.gender&&!e.gender&&(e.gender=s.gender),(s.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),(s.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),JSON.stringify(e)!==before&&changed.push({key,name:e.name,status:"updated"})}return changed}function csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated){let changed=!0;for(;changed;){changed=!1;for(const[key,s]of map){const dupKey=csSplitIdentityTokens(s.aliases,{aliases:!0}).map(a=>a.toLowerCase()).find(a=>a!==key&&map.has(a));if(!dupKey)continue;const other=map.get(dupKey),sEst=establishedKeys.has(key),oEst=establishedKeys.has(dupKey);let keepKey=key,dropKey=dupKey;if(oEst&&!sEst)keepKey=dupKey,dropKey=key;else if(sEst===oEst){const filled=sh=>CS_DETAIL_FIELDS.filter(f=>_csStr(sh[f]).trim()).length,sFilled=filled(s),oFilled=filled(other);if(oFilled>sFilled)keepKey=dupKey,dropKey=key;else if(oFilled===sFilled&&establishedCreated){const sT=establishedCreated.get(key),oT=establishedCreated.get(dupKey);sT!=null&&oT!=null&&oT<sT&&(keepKey=dupKey,dropKey=key)}}const keep=map.get(keepKey),drop=map.get(dropKey),oldAliases=keep.aliases;CS_SCALAR_FIELDS.forEach(f=>{const dv=_csStr(drop[f]);dv.length>(keep[f]||"").length&&(keep[f]=dv)}),keep.aliases=csMergeAliases({...keep,aliases:oldAliases},drop),drop.tier==="main"&&(keep.tier="main"),drop.line_count!=null&&(keep.line_count=Math.max(keep.line_count||0,drop.line_count)),(drop.inventory||[]).forEach(it=>{it&&!keep.inventory.includes(it)&&keep.inventory.length<3&&keep.inventory.push(it)}),(drop.sources||[]).forEach(src=>{src&&src.quote&&keep.sources.length<12&&!keep.sources.some(x=>x.quote===src.quote)&&keep.sources.push(src)}),map.set(keepKey,keep),map.delete(dropKey),changed=!0;break}}}function csProgressSnapshot(map,changed=[]){const changedByKey=new Map(changed.map(x=>[x.key,x.status]));return[...map.entries()].map(([key,s])=>{const filled=CS_DETAIL_FIELDS.filter(f=>_csStr(s[f]).trim()).length;return{key,name:s.name||key,alias:[s.title,s.profession].filter(Boolean).join(" \xB7 "),status:changedByKey.get(key)||"",filled,main:s.tier==="main",gender:s.gender||"",lineCount:Number(s.line_count)||0,image:s._image||""}}).sort((a,b)=>(b.status?1:0)-(a.status?1:0)||b.filled-a.filled||a.name.localeCompare(b.name))}async function csGenerateStream(body,onDelta,outerSignal,idleTimeoutMs=6e4){const ctl=new AbortController,onAbort=()=>ctl.abort();outerSignal&&(outerSignal.aborted?ctl.abort():outerSignal.addEventListener("abort",onAbort,{once:!0}));let idleTimer=null;const armIdle=()=>{clearTimeout(idleTimer),idleTimer=setTimeout(()=>ctl.abort(),idleTimeoutMs)};try{armIdle();const r=await fetch("/api/character-sheets/stream",{method:"POST",headers:{"Content-Type":"application/json"},signal:ctl.signal,body:JSON.stringify(body)});if(!r.ok||!r.body)throw new Error("stream HTTP "+r.status);const reader=r.body.getReader(),dec=new TextDecoder;let buf="",result=null;for(;;){const{done,value}=await reader.read();if(done)break;armIdle(),buf+=dec.decode(value,{stream:!0});let at;for(;(at=buf.indexOf(`
`))>=0;){const line=buf.slice(0,at).trim();if(buf=buf.slice(at+2),!line.startsWith("data:"))continue;let d;try{d=JSON.parse(line.slice(5))}catch{continue}if(d.t&&onDelta&&onDelta(d.t),d.error)throw new Error(d.error);d.done&&(result=d.result||null)}}if(!result)throw new Error("stream ended without result");return result}finally{clearTimeout(idleTimer),outerSignal&&outerSignal.removeEventListener("abort",onAbort)}}async function csGenerate(text,cacheKey,initialRoster,opts={}){if(_cs.running)return null;if(!text)return toast("Nothing to analyse","error"),null;const chunks=typeof splitTextIntoChunks=="function"?splitTextIntoChunks(text,CS_CHUNK_CHARS):[text];_cs.running=!0,_cs.cancel=!1;try{const prog=csProgress(chunks.length,opts.pageHost||null),llm_url=csLlmUrl(),model=csLlmModel();let language=csLang();if(typeof detectLang=="function"){const detected=detectLang(text);if(detected&&(!language||/^auto$/i.test(language)||language.toLowerCase()==="english"&&detected.toLowerCase()!=="english")){language=detected;const sel=$("reh-design-lang");if(sel){const o=[...sel.options].find(x=>x.value.toLowerCase()===detected.toLowerCase()||x.textContent.toLowerCase()===detected.toLowerCase());o&&(sel.value=o.value)}}}const map=new Map,lineCounts=opts.lineCounts instanceof Map?opts.lineCounts:new Map,seedSheets=Array.isArray(opts.seedSheets)?opts.seedSheets:[],establishedKeys=new Set,establishedCreated=new Map;seedSheets.forEach(seedRaw=>{const seed=csSeedSheet(seedRaw),name=String(seed.name||"").trim();if(!name)return;const lc=lineCounts.get(name.toLowerCase());lc!=null&&(seed.line_count=lc),map.set(name.toLowerCase(),seed),establishedKeys.add(name.toLowerCase());const created=seedRaw!=null&&seedRaw.created?new Date(seedRaw.created).getTime():NaN;isNaN(created)||establishedCreated.set(name.toLowerCase(),created)});const roster=csCleanRoster(initialRoster),targetMode=roster.length>0,knownTotal=targetMode?roster.length:null;roster.forEach(name=>{const key=name.toLowerCase();if(!map.has(key)){const blank=csBlankSheet(name),lc=lineCounts.get(key);lc!=null&&(blank.line_count=lc),map.set(key,blank)}});try{for(let i=0;i<chunks.length&&!_cs.cancel;i++){const detailed=[...map.values()].filter(s=>CS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,charLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${detailed} profiles with details`:`${map.size} characters found`;prog.update(i,`Passage ${i+1} / ${chunks.length}\u2026`,charLabel,csProgressSnapshot(map),[...map.values()]),prog.startPassage(chunks[i]);const maxAttempts=4;for(let attempt=1;attempt<=maxAttempts;attempt++)try{const character_sheets_prompt=typeof _appSettings!="undefined"&&_appSettings.character_sheets_prompt||CS_DEFAULT_PROMPT,body={text:chunks[i],known_characters:roster.slice(0,120),target_mode:targetMode,existing:csExistingSummary(map),language,llm_url,model,character_sheets_prompt};let data=null,sawDelta=!1;try{data=await csGenerateStream(body,delta=>{sawDelta=!0,prog.thinking(delta)}),sawDelta||prog.noStream()}catch{sawDelta||prog.noStream();const r=await fetch("/api/character-sheets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){let detail=`HTTP ${r.status}`;try{const e=await r.json();detail=e.detail||e.error||detail}catch{}throw new Error(detail)}const raw=await r.text();try{data=JSON.parse(raw)}catch{throw new Error("Invalid JSON from server (passage may be too large)")}}const incomingSheets=targetMode?(data.sheets||[]).filter(s=>csSheetMatchesRoster(s,roster)):data.sheets||[];incomingSheets.forEach(s=>{const ownName=String(s.name||"").trim().toLowerCase();["first_name","last_name","full_name"].forEach(f=>{const v=String(s[f]||"").trim().toLowerCase();v&&v!==ownName&&roster.some(n=>n.toLowerCase()===v)&&(console.warn(`[character sheets] dropped ${f}="${s[f]}" from "${s.name}" \u2014 matches a different known character`),s[f]="")})});const changed=csMerge(map,incomingSheets);changed.length&&prog.focus(changed[0].name),targetMode||((data.characters||[]).forEach(n=>{roster.some(r=>r.toLowerCase()===String(n||"").toLowerCase())||roster.push(n)}),(data.sheets||[]).forEach(s=>{csNameTokens(s).forEach(n=>{const display=[s.name,s.title,s.profession,s.aliases].filter(Boolean).join(", ").split(/[,;/|]/).map(x=>x.trim()).find(x=>x.toLowerCase()===n)||n;display&&!roster.some(r=>r.toLowerCase()===display.toLowerCase())&&roster.push(display)})}));const newDetailed=[...map.values()].filter(s=>CS_DETAIL_FIELDS.some(f=>(s[f]||"").trim())).length,newCharLabel=knownTotal?`${knownTotal} cast characters queued \xB7 ${newDetailed} profiles with details`:`${map.size} characters found`;prog.update(i+1,`Passage ${i+1} / ${chunks.length}\u2026`,newCharLabel,csProgressSnapshot(map,changed),[...map.values()]);break}catch(e){const msg=e.message||String(e),isRateLimit=/429|too many requests|rate.?limit/i.test(msg),isNetworkError=/failed to fetch|network\s*error|load failed|timed?\s*out|timeout|\b50[234]\b|econnreset|econnrefused|socket hang up|connection (refused|reset|closed)/i.test(msg);if((isRateLimit||isNetworkError)&&attempt<maxAttempts){const waitMs=attempt*(isRateLimit?8e3:4e3);toast(`Passage ${i+1} \u2014 ${isRateLimit?"rate-limited":"network error"} \u2014 retrying in ${waitMs/1e3}s (attempt ${attempt+1}/${maxAttempts})\u2026`,"info"),await new Promise(res=>setTimeout(res,waitMs));continue}console.error("Character sheets passage",i+1,"failed:",e),toast("Passage "+(i+1)+" failed: "+msg,"error");break}}}finally{prog.done()}if(_cs.cancel)return toast("Cancelled","error"),null;csConsolidateAliasDuplicates(map,establishedKeys,establishedCreated);const sheets=[...map.values()];return cacheKey&&(_cs.cache[cacheKey]=sheets),sheets}catch(e){return console.error("[character sheets] generation crashed:",e),toast("Character sheet generation crashed: "+(e&&e.message?e.message:String(e)),"error"),null}finally{_cs.running=!1}}function csProgress(total,hostEl=null){var _a2,_b2;const usingHost=!!hostEl;let root=usingHost?hostEl:document.getElementById("cs-progress");if(!root){if(usingHost)return null;root=document.createElement("div"),root.id="cs-progress",root.className="audiobook-overlay",document.body.appendChild(root)}if(!root.querySelector("#cs-progress-fill")){usingHost&&(root.innerHTML=""),root.innerHTML=`<div class="audiobook-box cs-progress-box cs-progress-box-big cs-page-shell">
<div class="cs-progress-head">
<div class="audiobook-title">
<span class="mdi mdi-account-details-outline"></span> Character sheets
<button class="ab-castpanel-btn" id="cs-prompt-btn" title="Edit character-sheet extraction prompt" style="margin-left:auto; display:flex; align-items:center; gap:4px; padding:4px 8px; font-weight:600; font-size:12px; width:auto; height:auto; min-height:28px;"><span class="mdi mdi-text-box-edit-outline"></span> Prompt <span class="mdi mdi-chevron-down" id="cs-prompt-chevron"></span></button>
</div>
<div class="audiobook-msg" id="cs-progress-msg">Analysing\u2026</div>
<div class="cs-progress-prompt" id="cs-prompt-panel" hidden>
<textarea id="cs-prompt-text" rows="8" placeholder="Character-sheet extraction instructions..."></textarea>
<div class="cs-progress-prompt-actions">
<select id="cs-prompt-lib" class="btn-secondary btn-sm" aria-label="Load a saved prompt">
<option value="">\u2014 Load saved prompt \u2014</option>
</select>
<button class="btn-secondary btn-sm" id="cs-prompt-del" title="Delete selected prompt" style="display:none; color:var(--error);"><span class="mdi mdi-delete-outline"></span></button>
<input type="text" id="cs-prompt-name" placeholder="Preset name...">
<button class="btn-primary btn-sm" id="cs-prompt-save" title="Save current prompt as a preset"><span class="mdi mdi-content-save"></span> Save preset</button>
</div>
</div>
</div>
<div class="cs-progress-layout" id="cs-progress-layout">
<div class="cs-progress-left">
<div class="cs-progress-preview" id="cs-progress-preview">
<div class="cs-progress-preview-empty">Select a character to preview the sheet as it fills.</div>
</div>
<div class="cs-progress-output" id="cs-progress-output">
<button class="cs-progress-resize-handle" id="cs-progress-output-resize" type="button" title="Drag to resize passage and live output" aria-label="Resize passage and live output"></button>
<div class="cs-progress-output-head">
<span class="cs-progress-output-title"><span class="mdi mdi-text-long"></span> Passage / Live output <span class="cs-progress-live-status" id="cs-progress-live-status">watching\u2026</span></span>
<button class="ab-cv-side-collapse" id="cs-progress-output-collapse" type="button" title="Collapse passage and live output"><span class="mdi mdi-chevron-down"></span></button>
</div>
<div class="cs-progress-body">
<div class="cs-progress-main">
<div class="cs-progress-col">
<div class="cs-progress-col-label">Passage</div>
<pre class="cs-progress-passage-pre" id="cs-progress-passage-pre"></pre>
</div>
<div class="cs-progress-col">
<div class="cs-progress-col-label">Live output</div>
<pre class="cs-progress-live-pre" id="cs-progress-live-pre"></pre>
</div>
</div>
</div>
</div>
</div>
<div class="ab-cv-side cs-progress-side" id="cs-progress-side">
<div class="ab-cv-side-head">
<span class="ab-cv-side-title" id="cs-progress-side-title">Characters found</span>
<button class="ab-cv-side-collapse" id="cs-progress-side-collapse" type="button" title="Collapse character list"><span class="mdi mdi-chevron-right"></span></button>
</div>
<div class="ab-cv-side-tools">
<input class="ab-cv-side-search" id="cs-progress-search" type="search" placeholder="Search characters...">
<select class="ab-cv-side-sort" id="cs-progress-sort" title="Sort">
<option value="progress">Progress</option>
<option value="alpha">A\u2013Z</option>
<option value="lines">Lines</option>
<option value="gender">Gender</option>
</select>
</div>
<div class="cs-progress-chars" id="cs-progress-chars"></div>
<div class="ab-cv-side-done" id="cs-progress-side-done"></div>
</div>
</div>
<div class="cs-progress-footer">
<div class="reader-synth-track cs-progress-footer-track"><div class="reader-synth-fill" id="cs-progress-fill"></div></div>
<div class="audiobook-actions cs-progress-footer-actions"><button class="btn-secondary btn-sm" id="cs-progress-cancel">Cancel</button></div>
</div>
</div>`,(_a2=root.querySelector("#cs-progress-cancel"))==null||_a2.addEventListener("click",()=>{_cs.cancel=!0});const promptBtn=root.querySelector("#cs-prompt-btn"),promptPanel=root.querySelector("#cs-prompt-panel"),promptText=root.querySelector("#cs-prompt-text");promptText&&(promptText.value=typeof _appSettings!="undefined"&&_appSettings.character_sheets_prompt||CS_DEFAULT_PROMPT),promptBtn==null||promptBtn.addEventListener("click",()=>{if(!promptPanel)return;promptPanel.hidden=!promptPanel.hidden;const chevron=root.querySelector("#cs-prompt-chevron");chevron&&(chevron.className=promptPanel.hidden?"mdi mdi-chevron-down":"mdi mdi-chevron-up")});let savedCsPrompts=[];try{savedCsPrompts=JSON.parse(localStorage.getItem("ttsvc_cs_prompts")||"[]")}catch{savedCsPrompts=[]}const libSelect=root.querySelector("#cs-prompt-lib"),delBtn=root.querySelector("#cs-prompt-del"),nameInput=root.querySelector("#cs-prompt-name"),renderCsPromptLib=(selectedIdx=-1)=>{!libSelect||!delBtn||(libSelect.innerHTML='<option value="">\u2014 Load saved prompt \u2014</option>'+savedCsPrompts.map((p,i)=>`<option value="${i}">${escHtml(p.name)}</option>`).join(""),selectedIdx>=0?(libSelect.value=selectedIdx,delBtn.style.display="inline-flex"):(libSelect.value="",delBtn.style.display="none"))};renderCsPromptLib(),libSelect==null||libSelect.addEventListener("change",()=>{const idx=parseInt(libSelect.value,10);!isNaN(idx)&&savedCsPrompts[idx]?(promptText&&(promptText.value=savedCsPrompts[idx].prompt),nameInput&&(nameInput.value=savedCsPrompts[idx].name),delBtn.style.display="inline-flex"):(nameInput&&(nameInput.value=""),delBtn.style.display="none")}),delBtn==null||delBtn.addEventListener("click",()=>{const idx=parseInt(libSelect.value,10);isNaN(idx)||confirm("Delete this saved prompt preset?")&&(savedCsPrompts.splice(idx,1),localStorage.setItem("ttsvc_cs_prompts",JSON.stringify(savedCsPrompts)),renderCsPromptLib(),toast("Prompt deleted","success"))}),(_b2=root.querySelector("#cs-prompt-save"))==null||_b2.addEventListener("click",async()=>{const val=(promptText==null?void 0:promptText.value.trim())||"";if(!val){toast("Prompt is empty","error");return}const name=(nameInput==null?void 0:nameInput.value.trim())||"Custom Prompt "+(savedCsPrompts.length+1);let targetIdx=parseInt((libSelect==null?void 0:libSelect.value)||"",10);!isNaN(targetIdx)&&savedCsPrompts[targetIdx]&&savedCsPrompts[targetIdx].name===name?savedCsPrompts[targetIdx].prompt=val:(savedCsPrompts.push({name,prompt:val}),targetIdx=savedCsPrompts.length-1),localStorage.setItem("ttsvc_cs_prompts",JSON.stringify(savedCsPrompts)),renderCsPromptLib(targetIdx),toast("Prompt preset saved","success")});let promptSaveTimer=null;promptText==null||promptText.addEventListener("input",()=>{clearTimeout(promptSaveTimer),promptSaveTimer=setTimeout(()=>{const val=promptText.value;typeof _appSettings!="undefined"&&(_appSettings.character_sheets_prompt=val),fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({character_sheets_prompt:val})}).catch(()=>{})},500)})}usingHost||(root.style.display="flex");const fill=root.querySelector("#cs-progress-fill"),msg=root.querySelector("#cs-progress-msg"),chars=root.querySelector("#cs-progress-chars"),sideTitle=root.querySelector("#cs-progress-side-title"),sideDone=root.querySelector("#cs-progress-side-done"),sideCollapse=root.querySelector("#cs-progress-side-collapse"),leftPane=root.querySelector(".cs-progress-left"),outputPanel=root.querySelector("#cs-progress-output"),outputCollapse=root.querySelector("#cs-progress-output-collapse"),outputResize=root.querySelector("#cs-progress-output-resize"),preview=root.querySelector("#cs-progress-preview"),search=root.querySelector("#cs-progress-search"),passagePre=root.querySelector("#cs-progress-passage-pre"),livePre=root.querySelector("#cs-progress-live-pre"),liveStatus=root.querySelector("#cs-progress-live-status");let currentRoster=[],currentSheets=[],selectedName="",searchQuery="",rosterSort=(()=>{try{return localStorage.getItem("ttsvc_cs_side_sort")||"lines"}catch{return"lines"}})();const keyFor=v=>String(v||"").trim().toLowerCase(),findSheet=(name,sheets=currentSheets)=>(Array.isArray(sheets)?sheets:[]).find(s=>keyFor(s==null?void 0:s.name)===keyFor(name))||null;let outputCollapsed=!1,outputExpandedHeight=0;const clampOutputHeight=h=>{const max=Math.max(140,Math.round(window.innerHeight*.82));return Math.max(140,Math.min(max,Math.round(h)))},setOutputHeight=(h,persist=!0)=>{const next=clampOutputHeight(h);if(outputExpandedHeight=next,!outputCollapsed&&outputPanel&&(outputPanel.style.height=`${next}px`,outputPanel.style.maxHeight=`${next}px`,outputPanel.dataset.csHeight=String(next)),persist)try{localStorage.setItem("ttsvc_cs_output_height",String(next))}catch{}},applyOutputCollapse=collapsed=>{if(outputCollapsed=!!collapsed,outputPanel==null||outputPanel.classList.toggle("is-collapsed",outputCollapsed),leftPane==null||leftPane.classList.toggle("is-output-collapsed",outputCollapsed),outputCollapse){const icon=outputCollapse.querySelector(".mdi");icon&&(icon.className="mdi "+(outputCollapsed?"mdi-chevron-up":"mdi-chevron-down")),outputCollapse.title=outputCollapsed?"Expand passage and live output":"Collapse passage and live output"}if(outputPanel)if(outputCollapsed){if(!outputExpandedHeight){const cur=parseInt(outputPanel.dataset.csHeight||"",10)||parseInt(outputPanel.style.height||"",10)||outputPanel.getBoundingClientRect().height;cur&&(outputExpandedHeight=clampOutputHeight(cur))}const collapsedH=138;outputPanel.style.height=`${collapsedH}px`,outputPanel.style.maxHeight=`${collapsedH}px`}else{const h=outputExpandedHeight||parseInt(outputPanel.dataset.csHeight||"",10)||260;outputPanel.style.height=`${clampOutputHeight(h)}px`,outputPanel.style.maxHeight=`${clampOutputHeight(h)}px`,outputPanel.dataset.csHeight=String(clampOutputHeight(h))}try{localStorage.setItem("ttsvc_cs_output_collapsed",outputCollapsed?"1":"0")}catch{}};let sideCollapsed=!1;const applySideCollapse=collapsed=>{var _a3,_b3;if(sideCollapsed=!!collapsed,(_a3=root.querySelector("#cs-progress-layout"))==null||_a3.classList.toggle("side-collapsed",sideCollapsed),(_b3=root.querySelector("#cs-progress-side"))==null||_b3.classList.toggle("is-collapsed",sideCollapsed),sideCollapse){const icon=sideCollapse.querySelector(".mdi");icon&&(icon.className="mdi "+(sideCollapsed?"mdi-chevron-left":"mdi-chevron-right")),sideCollapse.title=sideCollapsed?"Expand character list":"Collapse character list"}try{localStorage.setItem("ttsvc_cs_side_collapsed",sideCollapsed?"1":"0")}catch{}};try{sideCollapsed=localStorage.getItem("ttsvc_cs_side_collapsed")==="1"}catch{}try{const savedOutputCollapsed=localStorage.getItem("ttsvc_cs_output_collapsed");savedOutputCollapsed!==null&&(outputCollapsed=savedOutputCollapsed==="1");const savedOutputHeight=parseInt(localStorage.getItem("ttsvc_cs_output_height")||"",10);!Number.isNaN(savedOutputHeight)&&savedOutputHeight>0&&(outputExpandedHeight=clampOutputHeight(savedOutputHeight))}catch{}if(applySideCollapse(sideCollapsed),applyOutputCollapse(outputCollapsed),sideCollapse==null||sideCollapse.addEventListener("click",()=>applySideCollapse(!sideCollapsed)),outputCollapse==null||outputCollapse.addEventListener("click",()=>applyOutputCollapse(!outputCollapsed)),outputResize){let dragging=!1,startY=0,startH=0;const onMove=e=>{dragging&&(outputCollapsed&&applyOutputCollapse(!1),setOutputHeight(startH-(e.clientY-startY)),e.preventDefault())},stopDrag=()=>{dragging=!1,document.body.classList.remove("cs-output-resizing"),document.removeEventListener("mousemove",onMove),document.removeEventListener("mouseup",stopDrag)};outputResize.addEventListener("mousedown",e=>{e.preventDefault(),outputPanel&&(outputCollapsed&&applyOutputCollapse(!1),dragging=!0,startY=e.clientY,startH=parseInt(outputPanel.dataset.csHeight||"",10)||outputPanel.getBoundingClientRect().height||260,document.body.classList.add("cs-output-resizing"),document.addEventListener("mousemove",onMove),document.addEventListener("mouseup",stopDrag))})}const renderPreview=()=>{var _a3,_b3,_c2,_d2,_e2,_f2;if(!preview)return;const sheet=findSheet(selectedName)||currentSheets[0]||null;if(!sheet){preview.innerHTML='<div class="cs-progress-preview-empty">Select a character to preview the sheet as it fills.</div>';return}preview.innerHTML=csCardHtml(sheet,{voice:sheet.voice,line_count:sheet.line_count}),(_a3=preview.querySelector(".cs-card"))==null||_a3.classList.add("cs-progress-preview-card"),preview.querySelectorAll(".cs-head-btns, .cs-header-actions, .cs-sources").forEach(el=>el.remove()),preview.querySelectorAll(".cs-avatar").forEach(el=>{el.style.pointerEvents="none",el.style.cursor="default"});const book=typeof readerState!="undefined"&&readerState.title||"",ensureRecord=async()=>typeof clUpsert=="function"?await clUpsert(book,sheet):null;(_b3=preview.querySelector(".lcd-pick-voice"))==null||_b3.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _openVoicePicker=="function"&&_openVoicePicker(preview.querySelector(".lcd-voice-top"),rec,()=>renderPreview())}),(_c2=preview.querySelector(".lcd-auto-voice"))==null||_c2.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _autoAssignVoice=="function"&&(await _autoAssignVoice(rec),renderPreview())}),(_d2=preview.querySelector(".lcd-online-voice"))==null||_d2.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _charSearchOnline=="function"&&_charSearchOnline(rec)}),(_e2=preview.querySelector(".lcd-gen-voice"))==null||_e2.addEventListener("click",async()=>{const rec=await ensureRecord();rec&&typeof _charDesignVoice=="function"&&_charDesignVoice(rec)}),(_f2=preview.querySelector(".cs-header-color input"))==null||_f2.addEventListener("input",async e=>{const rec=await ensureRecord();rec&&(sheet.color=e.target.value,rec.color=e.target.value,typeof clPut=="function"&&await clPut(rec))});const sourceBox=preview.querySelector(".cs-source-preview");preview.querySelectorAll(".cs-source-mark:not(.cs-source-mark-sample)").forEach(btn=>{btn.addEventListener("click",()=>{if(!sourceBox)return;preview.querySelectorAll(".cs-source-mark.is-active").forEach(b=>b.classList.remove("is-active")),btn.classList.add("is-active");const quote=btn.dataset.quote||"",hint=btn.dataset.lineHint||"",page=btn.dataset.page||"";sourceBox.innerHTML=`<div class="cs-source-preview-head"><span class="mdi mdi-book-open-page-variant-outline"></span> ${hint?escHtml(hint):"Source sentence"}${page?` <em>\xB7 p.${escHtml(page)}</em>`:""}</div>
<div class="cs-source-preview-quote">${quote?escHtml(quote):'<span style="opacity:.6">No exact sentence was saved for this field.</span>'}</div>`,sourceBox.scrollIntoView({behavior:"smooth",block:"nearest"})})})},rosterSorters={progress:(a,b)=>(b.status?1:0)-(a.status?1:0)||b.filled-a.filled||a.name.localeCompare(b.name),alpha:(a,b)=>a.name.localeCompare(b.name),lines:(a,b)=>(b.lineCount||0)-(a.lineCount||0)||a.name.localeCompare(b.name),gender:(a,b)=>(a.gender||"\uFFFF").localeCompare(b.gender||"\uFFFF")||a.name.localeCompare(b.name)},renderChars=()=>{if(!chars)return;const list=currentRoster.filter(c=>{if(!searchQuery)return!0;const name=keyFor(c.name),alias=keyFor(c.alias);return name.includes(searchQuery)||alias.includes(searchQuery)}).slice().sort(rosterSorters[rosterSort]||rosterSorters.progress);chars.innerHTML=list.length?list.map(c=>`
<div class="ab-char-item ${c.status?"is-active":""}" data-name="${escHtml(c.name||"")}">
<div class="cs-progress-fillmeta" aria-hidden="true" title="${escHtml(String(c.filled||0))} profile fields filled">
<span style="width:${Math.min(100,Math.round((c.filled||0)/CS_DETAIL_FIELDS.length*100))}%"></span>
</div>
${c.image?`<span class="ab-char-dot ab-char-dot-img"><img src="${escHtml(c.image)}" alt=""></span>`:`<span class="ab-char-dot"${typeof clNormalizeColor=="function"?` style="background:${escHtml(clNormalizeColor(null,c.name||""))}"`:""}>${escHtml((c.name||"?")[0].toUpperCase())}</span>`}
<span class="ab-char-name" title="${escHtml(c.name||"Unknown")}">${escHtml(c.name||"Unknown")}</span>
<span class="ab-char-meta" title="${escHtml(String(c.lineCount||0))} lines in the book \xB7 ${escHtml(String(c.filled||0))} profile fields filled">
<span class="ab-char-lines">${c.lineCount?escHtml(String(c.lineCount))+" lines":"\u2014"}</span>
<span class="ab-char-meta-sep" aria-hidden="true">|</span>
<span class="ab-char-count">${escHtml(String(c.filled||0))}/${CS_DETAIL_FIELDS.length}</span>
</span>
</div>
`).join(""):'<div class="cs-progress-empty">reading\u2026</div>',chars.querySelectorAll(".ab-char-item").forEach(el=>{el.addEventListener("click",()=>{selectedName=keyFor(el.dataset.name),renderPreview()})})};search==null||search.addEventListener("input",()=>{searchQuery=search.value.trim().toLowerCase(),renderChars()});const sortSel=root.querySelector("#cs-progress-sort");return sortSel&&(sortSel.value=rosterSort,sortSel.addEventListener("change",()=>{rosterSort=sortSel.value;try{localStorage.setItem("ttsvc_cs_side_sort",rosterSort)}catch{}renderChars()})),renderPreview(),{startPassage(text){passagePre&&(passagePre.textContent=text||""),livePre&&(livePre.textContent="Waiting for streamed JSON output\u2026"),liveStatus&&(liveStatus.textContent="watching\u2026",liveStatus.className="cs-progress-live-status"),outputPanel==null||outputPanel.classList.add("is-working")},thinking(delta){!livePre||!delta||(liveStatus&&(liveStatus.textContent="streaming",liveStatus.className="cs-progress-live-status is-live"),outputPanel==null||outputPanel.classList.add("is-working"),livePre.textContent=(livePre.textContent+delta).slice(-2e4),livePre.scrollTop=livePre.scrollHeight)},noStream(){liveStatus&&(liveStatus.textContent="no live output for this model",liveStatus.className="cs-progress-live-status"),livePre&&livePre.textContent==="Waiting for streamed JSON output\u2026"&&(livePre.textContent="This model doesn't stream incremental output \u2014 the full result arrives at once when each passage finishes.")},focus(name){name&&(selectedName=keyFor(name),renderPreview())},update(d,label,charLabel,roster=[],sheets=[]){if(fill&&(fill.style.width=d/total*100+"%"),msg&&label&&(msg.textContent=label),sideTitle&&(sideTitle.textContent="Characters found"),sideDone&&(sideDone.innerHTML=charLabel?`<span class="ab-cv-done"><span class="mdi mdi-check-circle-outline"></span> ${escHtml(charLabel)}</span>`:""),outputPanel==null||outputPanel.classList.add("is-working"),currentRoster=Array.isArray(roster)?roster:[],currentSheets=Array.isArray(sheets)?sheets:[],!selectedName&&currentRoster.length&&(selectedName=keyFor((currentRoster.find(c=>c.status)||currentRoster[0]||{}).name)),currentSheets.length&&!findSheet(selectedName)){const changedSheet=currentRoster.find(c=>c.status&&findSheet(c.name,currentSheets));changedSheet&&(selectedName=keyFor(changedSheet.name))}renderChars(),renderPreview()},done(){outputPanel==null||outputPanel.classList.remove("is-working"),usingHost||(root.style.display="none")}}}function csAlignmentBar(score,arcDirection,arcNote){if(score==null)return"";const pct=Math.max(0,Math.min(100,score)),arcMap={"stable-good":{arrow:"\u2192",label:"Stable Good",color:"#66bb6a"},"stable-bad":{arrow:"\u2192",label:"Stable Evil",color:"#aaa"},neutral:{arrow:"\u2192",label:"Neutral",color:"#aaa"},"good-to-bad":{arrow:"\u2198",label:"Descends toward evil",color:"#ff7043"},"bad-to-good":{arrow:"\u2197",label:"Redeems toward good",color:"#66bb6a"},complex:{arrow:"\u2195",label:"Complex arc",color:"#ab47bc"}},arc=arcMap[arcDirection]||arcMap.neutral,label=pct>=70?"Good":pct<=30?"Evil":"Neutral/Ambiguous",showArrow=arcDirection&&arcDirection!=="neutral";return`<div class="cs-alignment-wrap">
<div class="cs-alignment-labels"><span>Evil</span><span>Good</span></div>
<div class="cs-alignment-gradient" title="${label} (${pct}/100)${arcNote?" \u2014 "+arcNote:""}">
<div class="cs-alignment-dot" style="left:${pct}%">
${showArrow?`<span class="cs-alignment-arc-arrow" style="color:${arc.color}" title="${escHtml(arc.label)}">${arc.arrow}</span>`:""}
</div>
</div>
${arcNote?`<div class="cs-arc-note">${escHtml(arc.label)} \xB7 <em>${escHtml(arcNote)}</em></div>`:`<div class="cs-arc-note" style="color:${arc.color}">${escHtml(arc.label)}</div>`}
</div>`}function csBuildImagePrompt(s,bookProfile){var _a2;if(_csStr(s.image_prompt).trim())return _csStr(s.image_prompt).trim();const parts=[];s.race_species&&parts.push(s.race_species),s.archetype&&parts.push(s.archetype),s.physical&&parts.push(s.physical),s.clothing&&parts.push(s.clothing),s.alignment&&parts.push(s.alignment);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;parts.push(pct>=70?"benevolent expression":pct<=30?"dark and menacing presence":"ambiguous expression"),s.arc_direction==="bad-to-good"&&parts.push("redemptive aura"),s.arc_direction==="good-to-bad"&&parts.push("ominous aura, turning to darkness");const bp=bookProfile||{},settingBits=[bp.genre,bp.setting,bp.era].filter(Boolean),settingClause=settingBits.length?`This character belongs to the following book/world: ${settingBits.join(", ")}. Every visual choice \u2014 architecture, clothing materials, weaponry, ethnicity mix, technology level \u2014 must fit THAT world, not a generic modern or real-world default. `:"";return`Create a complete character reference sheet for an original character named ${s.name}, ${parts.filter(Boolean).join(", ")}. `+settingClause+`Base the setting, era, and art style strictly on the character's own described archetype and clothing above \u2014 do not default to a modern or real-world 20th/21st-century look for occupation-sounding titles (e.g. an "Admiral" or "General" in a fantasy/period setting should NOT be drawn in a contemporary military uniform); every visual choice should fit the world implied by the description, not the real one, unless the description itself is explicitly modern/contemporary. Include a full-body front view as the anchor, a turnaround panel (side and back views), an expression sheet with 3-5 headshots matching their personality, a color palette swatch for hair/eyes/outfit, and labeled callouts for signature props or clothing details. Clean production concept-art layout, plain neutral background, original character not based on any copyrighted character.`}function csBuildVoicePrompt(s){var _a2;if(_csStr(s.voice_design_prompt).trim())return _csStr(s.voice_design_prompt).trim();const parts=[];s.voice_pattern&&parts.push(s.voice_pattern),s.mannerisms&&parts.push(s.mannerisms),s.archetype&&parts.push(`archetype: ${s.archetype}`);const pct=(_a2=s.moral_alignment_score)!=null?_a2:50;return pct>=70?parts.push("warm, trustworthy tone"):pct<=30?parts.push("cold, threatening or sinister tone"):parts.push("neutral, measured tone"),parts.filter(Boolean).join(". ")}async function csDeepAnalysis(sheet,sourceText){const llm_url=csLlmUrl(),model=csLlmModel(),language=csLang(),modal=document.createElement("div");modal.className="audiobook-overlay",modal.innerHTML=`<div class="audiobook-box cs-deep-box">
<div class="cs-titlebar">
<span class="audiobook-title"><span class="mdi mdi-brain"></span> Deep Analysis \u2014 ${escHtml(sheet.name)}</span>
<span style="flex:1"></span>
<button class="btn-secondary btn-sm" id="cs-deep-close">Close</button>
</div>
<div class="cs-deep-body" id="cs-deep-body">
<div style="text-align:center;padding:40px;color:var(--subtext)">
<span class="mdi mdi-loading mdi-spin" style="font-size:28px"></span><br>Running deep psychological analysis\u2026
</div>
</div>
</div>`,document.body.appendChild(modal),modal.querySelector("#cs-deep-close").addEventListener("click",()=>modal.remove()),modal.addEventListener("click",e=>{e.target===modal&&modal.remove()});try{const summary=[sheet.archetype,sheet.alignment,sheet.secret,sheet.win_condition,sheet.conflict_style].filter(Boolean).join("; "),r=await fetch("/api/character-deep-analysis",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:sheet.name,role:sheet.tier,goal:sheet.win_condition,summary,text_excerpt:(sourceText||"").slice(0,8e3),language,llm_url,model})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const a=(await r.json()).analysis||{},section=(icon,title,content)=>content?`<div class="cs-deep-section">
<div class="cs-deep-section-head"><span class="mdi ${icon}"></span> ${title}</div>
<div class="cs-deep-section-body">${escHtml(String(content))}</div>
</div>`:"";modal.querySelector("#cs-deep-body").innerHTML=`
<div class="cs-deep-header">
<div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
<span class="cs-name" style="font-size:20px">${escHtml(sheet.name)}</span>
${sheet.archetype?`<span class="cs-archetype">${escHtml(sheet.archetype)}</span>`:""}
</div>
${csAlignmentBar(sheet.moral_alignment_score,sheet.arc_direction,sheet.arc_note)}
</div>
${section("mdi-heart-broken-outline","1 \u2014 Core Flaw & Desire",a.core_flaw)}
${section("mdi-run-fast","2 \u2014 Agency & Passivity",a.agency)}
${section("mdi-comment-quote-outline","3 \u2014 Dialogue & Voice",a.dialogue_voice)}
${section("mdi-timeline-outline","4 \u2014 Narrative Arc",a.narrative_arc)}
${section("mdi-infinity","5 \u2014 Paradox & Depth",a.paradox)}
`}catch(err){modal.querySelector("#cs-deep-body").innerHTML=`<div style="color:var(--error);padding:20px">Error: ${escHtml(String(err.message))}</div>`}}function csSourcesForField(s,field){const keys=(CS_SOURCE_FIELDS[field]||[field]).map(x=>x.toLowerCase());return(s.sources||[]).filter(src=>{const hint=_csStr(src==null?void 0:src.line_hint).toLowerCase();return hint&&keys.some(k=>hint.includes(k))})}function csSourceBadgeHtml(s,field){const all=s.sources||[],matches=csSourcesForField(s,field);return matches.length?matches.map(src=>{const idx=all.findIndex(x=>x===src),n=idx>=0?idx+1:1;return`<button class="cs-source-mark" type="button" data-page="${escHtml(String(src.page||""))}" data-quote="${escHtml(String(src.quote||""))}" data-line-hint="${escHtml(String(src.line_hint||""))}" title="Click to see the source sentence this was drawn from">${n}</button>`}).join(""):""}function csField(label,value,sheet,field){if(!value)return"";const badge=sheet&&field?csSourceBadgeHtml(sheet,field):"",key=field?` data-sheet-key="${escHtml(field)}"`:"";return`<div class="cs-row"><dt>${label}</dt><dd><span class="cs-field-editable"${key}>${escHtml(String(value))}</span>${badge?` ${badge}`:""}</dd></div>`}function csIdentityCell(label,value,sheet,field){if(!value)return"";const badge=sheet&&field?csSourceBadgeHtml(sheet,field):"",key=field?` data-sheet-key="${escHtml(field)}"`:"";return`<div class="cs-id-cell"><dt>${label}</dt><dd><span class="cs-field-editable"${key}>${escHtml(String(value))}</span>${badge?` ${badge}`:""}</dd></div>`}function csPromptBox(label,value,sheetKey,emptyNote,extraImage){const text=_csStr(value),has=!!text.trim(),actionBtn=has?sheetKey==="voice_design_prompt"?'<button type="button" class="btn-primary btn-sm cs-prompt-act" data-act="voice"><span class="mdi mdi-account-voice"></span> Design Voice</button>':sheetKey==="image_prompt"?'<button type="button" class="btn-primary btn-sm cs-prompt-act" data-act="image"><span class="mdi mdi-image-outline"></span> Generate Profile Image</button>':sheetKey==="concept_art_prompt"?`<button type="button" class="btn-primary btn-sm cs-prompt-act" data-act="concept_art"><span class="mdi mdi-image-multiple-outline"></span> ${extraImage?"Regenerate":"Generate"} Concept Art</button>`:"":"",imageHtml=extraImage?`<div class="cs-concept-art-preview"><img src="${escHtml(extraImage)}" alt="Concept art"></div>`:"";return`<details class="lcd-prompt-box cs-prompt-box">
<summary>${escHtml(label)}${has?"":' <span class="lcd-prompt-empty">\u2014 '+escHtml(emptyNote||"not generated yet")+"</span>"}</summary>
<div class="lcd-prompt-body">
${imageHtml}
<div class="lcd-prompt-text cs-prompt-text lcd-field-editable" contenteditable="true" spellcheck="false" data-sheet-key="${escHtml(sheetKey)}">${escHtml(text)}</div>
<div class="lcd-prompt-actions">
<button type="button" class="btn-secondary btn-sm cs-prompt-copy" data-sheet-key="${escHtml(sheetKey)}"><span class="mdi mdi-content-copy"></span> Copy</button>
<button type="button" class="btn-secondary btn-sm cs-gen-prompt" data-sheet-key="${escHtml(sheetKey)}"><span class="mdi mdi-creation"></span> ${has?"Regenerate":"Generate"}</button>
${actionBtn}
</div>
</div>
</details>`}function csAvatarHtml(s){const hue=Math.abs((s.name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0));return s._image?`<div class="cs-avatar cs-avatar-img" data-name="${escHtml(s.name)}" title="Click to change profile picture">
<img src="${s._image}" alt="${escHtml(s.name)}">
<div class="cs-avatar-overlay"><span class="mdi mdi-camera"></span></div>
</div>`:`<div class="cs-avatar cs-avatar-letter" style="background:hsl(${hue},50%,35%)" data-name="${escHtml(s.name)}" title="Click to upload profile picture">
<span>${escHtml((s.name||"?")[0].toUpperCase())}</span>
<div class="cs-avatar-overlay"><span class="mdi mdi-camera"></span></div>
</div>`}function csIdentityTagList(s){const tags=[],add=(label,value)=>{const text=String(value||"").trim();if(!text)return;const key=text.toLowerCase();tags.some(t=>t.key===key)||tags.push({label,text,key})},firstLast=[s.first_name,s.last_name].filter(Boolean).join(" ").trim();return firstLast&&firstLast!==String(s.name||"").trim()&&add("first / last",firstLast),tags}function csNameHue(name){return Math.abs((name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0))}function csHeaderPill(label,value,icon=""){const text=String(value||"").trim();return text?`<span class="cs-header-pill">${icon?`<span class="mdi mdi-${escHtml(icon)}"></span>`:""}<span class="cs-header-pill-label">${escHtml(label)}</span><span class="cs-header-pill-value">${escHtml(text)}</span></span>`:""}function csProfileHeaderHtml(s,meta={},actionsHtml=""){var _a2;const hue=csNameHue(s.name),accent2=`hsl(${(hue+38)%360}, 58%, 34%)`,primaryPills=[csHeaderPill("Occupation",s.profession,"briefcase-outline"),csHeaderPill("Archetype",s.archetype,"shape-outline"),csHeaderPill("Also known as",s.aliases,"tag-multiple-outline")].filter(Boolean).join(""),chips=[csHeaderPill("Lines",(_a2=meta.line_count)!=null?_a2:s.line_count,"format-list-numbered"),csHeaderPill("Voice",meta.voice,"account-voice"),csHeaderPill("Book",meta.book,"book-open-page-variant-outline"),csHeaderPill("Tags",meta.tags,"tag-outline"),csHeaderPill("Gender",s.gender,"human-male-female")].filter(Boolean).join(""),color=typeof clNormalizeColor=="function"?clNormalizeColor(s.color,s.name):`hsl(${hue},58%,42%)`;return`<div class="lcd-header cs-header" style="--lc1:hsl(${hue},58%,42%);--lc2:${accent2}; position:relative;">
${csAvatarHtml(s)}
<div class="lcd-header-body">
<div class="lcd-name">${escHtml(s.name||"Unnamed")}</div>
${primaryPills?`<div class="cs-header-pills cs-header-pills-primary">${primaryPills}</div>`:""}
<div class="lcd-tier-gender">${s.tier?`<span class="lcd-tier">${escHtml(String(s.tier).toLowerCase()==="main"?"Hauptcharakter":"Nebencharakter")}</span>`:""}${chips?`<div class="cs-header-pills">${chips}</div>`:""}</div>
</div>
<label class="ab-cd-color cs-header-color" title="Change this character's highlight color"><span class="mdi mdi-palette-outline"></span><input type="color" value="${escHtml(color)}" data-sheet-key="color" aria-label="Character highlight color"></label>
<button type="button" class="ab-cd-color cs-edit-toggle" title="Edit character sheet" aria-label="Edit character sheet"><span class="mdi mdi-pencil-outline"></span></button>
${actionsHtml?`<div class="lcd-header-btns cs-header-actions">${actionsHtml}</div>`:""}
</div>`}function csDetailActionsHtml(s){return`
<button class="btn-secondary btn-sm cs-auto-refine-btn" data-name="${escHtml(s.name)}" title="Refine this character only"><span class="mdi mdi-refresh"></span> Auto refine</button>
<button class="btn-secondary btn-sm cs-edit-btn" data-name="${escHtml(s.name)}" title="Open character editing"><span class="mdi mdi-pencil-outline"></span> Edit</button>
`}function csOverviewCardHtml(rec){const s=rec.sheet||{},score=s.moral_alignment_score!=null?Math.max(0,Math.min(100,parseInt(s.moral_alignment_score,10)||50)):50,scoreLabel=score>=70?"Good":score<=30?"Evil":"Neutral",voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",fullName=s.full_name||rec.name||"",tierLabel=String(s.tier||"").toLowerCase()==="main"?"Main character":"Side character",facts=[["Occupation",s.profession],["Archetype",s.archetype],["Gender",s.gender],["Lines",s.line_count],["Voice",voiceId||"Not assigned"]],hue=csNameHue(rec.name);return`<div class="cs-card cs-overview cs-${s.tier==="main"?"main":"supp"}" data-id="${escHtml(rec.id||"")}" data-name="${escHtml(rec.name||"")}" role="button" tabindex="0" title="Open ${escHtml(rec.name||"character")}'s full profile">
<div class="cs-overview-head" style="--lc1:hsl(${hue},58%,42%);--lc2:hsl(${(hue+38)%360},58%,32%)">
${csAvatarHtml(s.name?s:{...s,name:rec.name,_image:rec.image})}
<div class="cs-overview-head-body">
<div class="cs-overview-name">${escHtml(fullName)}</div>
<span class="cs-overview-tier">${escHtml(tierLabel)}</span>
</div>
</div>
<div class="cs-overview-meta">
<div class="cs-alignment-wrap cs-overview-alignment">
<div class="cs-alignment-labels"><span>Evil</span><span>${scoreLabel} \xB7 ${score}/100</span><span>Good</span></div>
<div class="cs-alignment-gradient"><div class="cs-alignment-dot" style="left:${score}%"></div></div>
</div>
<div class="cs-overview-facts">
${facts.filter(([,v])=>String(v||"").trim()).map(([label,value])=>`<div class="cs-overview-fact"><span>${escHtml(label)}</span><b>${escHtml(String(value))}</b></div>`).join("")}
</div>
</div>
</div>`}function csLcdField(label,value,sheet,field){if(!value)return"";const badge=sheet&&field?csSourceBadgeHtml(sheet,field):"";return label?`<div class="lcd-row"><dt>${escHtml(label)}</dt><dd>${escHtml(String(value))}${badge?` ${badge}`:""}</dd></div>`:`<div class="lcd-row lcd-row-full"><dd>${escHtml(String(value))}${badge?` ${badge}`:""}</dd></div>`}function csLcdSection(icon,label,fieldsHtml,full=!1){const inner=(fieldsHtml||[]).filter(Boolean).join("");return inner?`<div class="${full?"lcd-section-full":"lcd-section"}"><div class="lcd-section-label"><span class="mdi ${icon}"></span> ${escHtml(label)}</div>${inner}</div>`:""}function csCardHtml(s,meta={}){const inv=(s.inventory||[]).filter(Boolean),invHtml=inv.length?`<div class="lcd-field"><div class="lcd-field-label">Signature Items</div><div class="lcd-field-value">${inv.map(escHtml).join(", ")}</div></div>`:"",attrs=s.attribute_high||s.attribute_low?`<div class="lcd-field"><div class="lcd-field-label">Core Attributes</div><div class="lcd-field-value">\u25B2 ${escHtml(s.attribute_high||"\u2014")} &nbsp;\xB7&nbsp; \u25BC ${escHtml(s.attribute_low||"\u2014")}</div></div>`:"",sources=(s.sources||[]).filter(x=>x&&(x.quote||x.page!=null||x.line_hint)),srcHtml=sources.length?`<div class="cs-sources"><div class="cs-sources-title"><span class="mdi mdi-book-open-page-variant-outline"></span> Sources / Evidence</div><div class="cs-source-list">${sources.map((x,i)=>`<button class="cs-source-link" type="button" data-page="${escHtml(String(x.page||""))}" title="${escHtml([x.line_hint,x.quote].filter(Boolean).join(" \u2014 ")||"Source")}">
<b>${i+1}</b>${x.line_hint?`<span class="cs-source-hint">${escHtml(x.line_hint)}</span>`:""}${x.quote?`<span class="cs-source-quote">${escHtml(String(x.quote).slice(0,140))}${String(x.quote).length>140?"\u2026":""}</span>`:""}${x.page!=null?`<em>p.${escHtml(String(x.page))}</em>`:""}
</button>`).join("")}</div></div>`:"",laterNote=_cs.running?"generates after casting finishes":"not generated yet",promptParts=[csPromptBox("Voice Design Prompt",s.voice_design_prompt,"voice_design_prompt"),csPromptBox("Character Image Prompt",s.image_prompt,"image_prompt")].filter((_,i)=>[s.voice_design_prompt,s.image_prompt][i]);promptParts.push(csPromptBox("SillyTavern Character Prompt",s.silly_tavern_prompt,"silly_tavern_prompt",laterNote),csPromptBox("Character Concept Art Prompt",s.concept_art_prompt,"concept_art_prompt",laterNote,s.concept_art_image));const promptHtml=promptParts.length?`<div class="cs-prompts">${promptParts.join("")}</div>`:"",voiceId=meta.voice||s.voice||"";return`<div class="cs-card cs-${s.tier==="main"?"main":"supp"}" data-name="${escHtml(s.name)}">
${csProfileHeaderHtml(s,meta,csDetailActionsHtml(s))}
<div class="lcd-body ab-cd-body">
<div class="lcd-voice-top">
<div class="lcd-section-label"><span class="mdi mdi-account-voice"></span> Stimme</div>
<span class="lcd-voice-label">${voiceId?escHtml(voiceId):'<span style="opacity:.5">Noch keine Stimme</span>'}</span>
<button class="lcd-pick-voice">Ausw\xE4hlen</button>
<button class="lcd-auto-voice">Automatisch</button>
<button class="lcd-online-voice">Online suchen</button>
<button class="lcd-gen-voice">Generieren</button>
</div>
<div class="cs-detail-hero-note">${csAlignmentBar(s.moral_alignment_score,s.arc_direction,s.arc_note)}</div>
<div class="lcd-sections">
${(()=>{const sections=[csLcdSection("mdi-card-account-details-outline","Identit\xE4t",[csLcdField("Voller Name",s.full_name,s,"full_name"),csLcdField("Vorname",s.first_name,s,"first_name"),csLcdField("Nachname",s.last_name,s,"last_name"),csLcdField("Alter",s.age_estimate,s,"age_estimate"),csLcdField("Geschlecht",s.gender,s,"gender"),csLcdField("Rasse / Spezies",s.race_species,s,"race_species"),csLcdField("Sprachen",s.languages,s,"languages"),csLcdField("Titel",s.title,s,"title"),csLcdField("Beruf / Rolle",s.profession,s,"profession"),csLcdField("Herkunft",s.nationality_background,s,"nationality_background"),csLcdField("Sozialer Stand",s.social_class,s,"social_class"),csLcdField("Religion",s.religious_beliefs,s,"religious_beliefs"),csLcdField("Ruf",s.reputation,s,"reputation")]),csLcdSection("mdi-account-outline","Erscheinung",[csLcdField("K\xF6rperlich",s.physical,s,"physical"),csLcdField("Kleidung",s.clothing,s,"clothing")]),csLcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[csLcdField("Eigenheiten",s.mannerisms,s,"mannerisms"),csLcdField("Kommunikationsstil",s.communication_style,s,"communication_style"),csLcdField("Stimme & Sprache",s.voice_pattern,s,"voice_pattern")]),csLcdSection("mdi-book-open-outline","Geschichte",[csLcdField("Hintergrund",s.backstory,s,"backstory"),csLcdField("Motivation",s.motivation,s,"motivation"),csLcdField("\xC4ngste",s.fears,s,"fears")]),csLcdSection("mdi-sword","F\xE4higkeiten",[csLcdField("Fertigkeiten",s.skills,s,"skills"),csLcdField("Besondere F\xE4higkeiten",s.capabilities,s,"capabilities"),attrs,invHtml]),csLcdSection("mdi-shield-sword-outline","Konflikt",[csLcdField("Geheimnis / Fataler Fehler",s.secret,s,"secret"),csLcdField("Konfliktstil",s.conflict_style,s,"conflict_style"),csLcdField("Siegbedingung",s.win_condition,s,"win_condition")]),csLcdSection("mdi-account-group-outline","Beziehungen",[csLcdField("",s.relationships,s,"relationships")],!0),csLcdSection("mdi-note-text-outline","Notizen",[csLcdField("",s.notes,s,"notes")],!0)].filter(Boolean);return sections.length?sections.join(""):`<div class="lcd-section-full lcd-empty-note"><span class="mdi mdi-progress-clock"></span> This character's profile hasn't been generated yet \u2014 it fills in as the passage containing their scenes is processed.</div>`})()}
</div>
</div>
${srcHtml}
${promptHtml}
<div class="cs-source-preview">
<div class="cs-source-preview-empty">Click a <span class="cs-source-mark cs-source-mark-sample">i</span> mark next to a field above to see the exact sentence it was drawn from.</div>
</div>
</div>`}function csToMarkdown(sheets){var _a2;const sec=t=>`
## ${t}
`;let md=`# Character Sheets
`;for(const tier of["main","supp"]){const list=sheets.filter(s=>s.tier==="main"==(tier==="main"));if(list.length){md+=sec(tier==="main"?"Main characters":"Supporting characters");for(const s of list){md+=`
### ${s.name}${s.archetype?" \u2014 "+s.archetype:""}
`,s.aliases&&(md+=`*also known as ${s.aliases}*
`),s.gender&&(md+=`- **Gender:** ${s.gender}
`),s.title&&(md+=`- **Title:** ${s.title}
`),s.profession&&(md+=`- **Occupation:** ${s.profession}
`),(s.first_name||s.last_name)&&(md+=`- **Name parts:** ${[s.first_name,s.last_name].filter(Boolean).join(" ")}
`);const mas=(_a2=s.moral_alignment_score)!=null?_a2:50,masLabel=mas>=70?"Good":mas<=30?"Evil":"Neutral";md+=`- **Moral alignment:** ${mas}/100 (${masLabel}) \u2014 Arc: ${s.arc_direction||"neutral"}
`,s.arc_note&&(md+=` *${s.arc_note}*
`);const f=(l,v)=>v?`- **${l}:** ${v}
`:"";md+=f("Physical",s.physical)+f("Clothing",s.clothing)+f("Alignment & Ethos",s.alignment)+f("Core Attributes",[s.attribute_high&&"\u25B2 "+s.attribute_high,s.attribute_low&&"\u25BC "+s.attribute_low].filter(Boolean).join(" \xB7 "))+f("Trained Skills",s.skills)+f("Capabilities",s.capabilities)+f("Backstory & Origin",s.backstory)+f("Relationships",s.relationships)+f("Motivation",s.motivation)+f("Fears",s.fears)+f("Mannerisms & Habits",s.mannerisms)+f("Voice & Speech",s.voice_pattern)+f("Voice Design Prompt",s.voice_design_prompt)+f("Image Generation Prompt",s.image_prompt)+f("SillyTavern Character Prompt",s.silly_tavern_prompt)+f("Character Concept Art Prompt",s.concept_art_prompt)+f("Signature Items",(s.inventory||[]).join(", "))+f("Dark Secret / Fatal Flaw",s.secret)+f("Conflict Style",s.conflict_style)+f("Win Condition",s.win_condition);const src=(s.sources||[]).filter(x=>x&&x.quote).map(x=>`${x.line_hint?"["+x.line_hint+"] ":""}${x.page!=null?"p."+x.page+" ":""}"${x.quote}"`).join("; ");src&&(md+=`- *Sources:* ${src}
`)}}}return md.trim()}function csWireResultInteractions(root,sheets,sourceText,book,inline=!1){var _a2,_b2;const copyBtn=root.querySelector("#cs-copy");copyBtn==null||copyBtn.addEventListener("click",()=>{var _a3;(_a3=navigator.clipboard)==null||_a3.writeText(csToMarkdown(sheets)).then(()=>toast("Copied as Markdown","success"),()=>toast("Copy failed","error"))});const closeBtn=root.querySelector("#cs-close");closeBtn==null||closeBtn.addEventListener("click",()=>{var _a3;inline?typeof navLibraryView=="function"&&navLibraryView("characters"):(_a3=document.getElementById("cs-overlay"))==null||_a3.remove()}),(_a2=root.querySelector("#cs-open-cast"))==null||_a2.addEventListener("click",()=>csGoToLibrary()),(_b2=root.querySelector("#cs-back-cast-audio"))==null||_b2.addEventListener("click",()=>{typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("cast")});const bkCtx=book||"";root.querySelectorAll(".cs-avatar").forEach(av=>{av.addEventListener("click",async e=>{e.stopPropagation();const name=av.dataset.name,s=sheets.find(x=>x.name===name);if(!s)return;const id=`${bkCtx}::${name}`.toLowerCase();let rec=typeof clGet=="function"?await clGet(id).catch(()=>null):null;if(!rec&&typeof clUpsert=="function"&&(rec=await clUpsert(bkCtx,s).catch(()=>null)),!rec||typeof _openAvatarLightbox!="function"){toast("Avatar editor is unavailable right now","error");return}_openAvatarLightbox(rec,async updatedRec=>{s._image=(updatedRec==null?void 0:updatedRec.image)||rec.image||"";const card=root.querySelector(`.cs-card[data-name="${CSS.escape(name)}"]`);if(card){const oldAv=card.querySelector(".cs-avatar");oldAv&&(oldAv.outerHTML=csAvatarHtml(s))}})})}),root.querySelectorAll(".cs-deep-btn").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation();const sheet=sheets.find(s=>s.name===btn.dataset.name);sheet&&csDeepAnalysis(sheet,sourceText)})}),root.querySelectorAll(".cs-auto-refine-btn").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const name=String(btn.dataset.name||"").trim();if(name){if(typeof window.csForReaderSelective=="function"){await window.csForReaderSelective([name]);return}toast("Character refinement is unavailable right now","error")}})}),root.querySelectorAll(".cs-edit-btn").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const name=String(btn.dataset.name||"").trim();if(!(!name||typeof clEdit!="function"))try{const all=typeof clGetAll=="function"?await clGetAll():[],wantBook=String(book||"").trim().toLowerCase(),rec=all.find(r=>{var _a3;const recName=String((r==null?void 0:r.name)||((_a3=r==null?void 0:r.sheet)==null?void 0:_a3.name)||"").trim().toLowerCase(),recBook=String((r==null?void 0:r.book)||"").trim().toLowerCase();return recName===name.toLowerCase()&&(!wantBook||recBook===wantBook)})||all.find(r=>{var _a3;return String((r==null?void 0:r.name)||((_a3=r==null?void 0:r.sheet)==null?void 0:_a3.name)||"").trim().toLowerCase()===name.toLowerCase()});if(!rec){toast("Character record not found in library","error");return}clEdit(rec.id)}catch{toast("Could not open editor","error")}})}),root.querySelectorAll(".cs-img-btn").forEach(btn=>{btn.addEventListener("click",e=>{var _a3;e.stopPropagation();const sheet=sheets.find(s=>s.name===btn.dataset.name);sheet&&((_a3=navigator.clipboard)==null||_a3.writeText(csBuildImagePrompt(sheet)).then(()=>toast("Image prompt copied","success"),()=>toast("Copy failed","error")))})}),root.querySelectorAll(".cs-voice-btn").forEach(btn=>{btn.addEventListener("click",e=>{var _a3;e.stopPropagation();const sheet=sheets.find(s=>s.name===btn.dataset.name);sheet&&((_a3=navigator.clipboard)==null||_a3.writeText(csBuildVoicePrompt(sheet)).then(()=>toast("Voice prompt copied \u2014 paste into Design a Voice","success"),()=>toast("Copy failed","error")))})}),root.querySelectorAll(".cs-prompt-text[data-sheet-key]").forEach(el=>{el.addEventListener("input",()=>{const key=el.dataset.sheetKey,card=el.closest(".cs-card"),sheet=sheets.find(s=>s.name===(card==null?void 0:card.dataset.name));sheet&&key&&(sheet[key]=el.textContent||"")})}),root.querySelectorAll(".cs-prompt-copy").forEach(btn=>{btn.addEventListener("click",async e=>{var _a3,_b3,_c2;e.stopPropagation();const text=((_b3=(_a3=btn.closest(".lcd-prompt-body"))==null?void 0:_a3.querySelector(".cs-prompt-text"))==null?void 0:_b3.textContent.trim())||"";if(!text){toast("Nothing to copy yet \u2014 generate first","error");return}typeof copyText=="function"?await copyText(text):(_c2=navigator.clipboard)==null||_c2.writeText(text),toast("Prompt copied","success")})}),root.querySelectorAll(".cs-gen-prompt").forEach(btn=>{btn.addEventListener("click",async e=>{var _a3;e.stopPropagation();const key=btn.dataset.sheetKey,card=btn.closest(".cs-card"),name=card==null?void 0:card.dataset.name,sheet=sheets.find(s=>s.name===name);if(!sheet||!key)return;const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span> Generating\u2026';try{const sample=[sheet.physical,sheet.backstory,sheet.motivation,sheet.relationships].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""},r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:sheet.name,book:book||"",sheet,language,llm_url:target.url,model:target.model,fields:[key]})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();if(!d[key])throw new Error("Empty response \u2014 try again");sheet[key]=d[key];const textEl=(_a3=btn.closest(".lcd-prompt-body"))==null?void 0:_a3.querySelector(".cs-prompt-text");textEl&&(textEl.textContent=d[key]),typeof csSaveToLibrary=="function"&&await csSaveToLibrary(book,sheets),toast("Prompt generated","success")}catch(err){toast("Prompt generation failed: "+(err.message||err),"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}),root.querySelectorAll(".cs-prompt-act").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const card=btn.closest(".cs-card"),name=card==null?void 0:card.dataset.name,sheet=sheets.find(s=>s.name===name);if(!sheet)return;const act=btn.dataset.act;let rec=typeof clUpsert=="function"?await clUpsert(book||"",sheet).catch(()=>null):null;if(!rec){toast("Could not save this character yet \u2014 try again","error");return}if(act==="voice"){typeof _charDesignVoice=="function"&&_charDesignVoice(rec);return}if(act==="image"){const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span> Generating\u2026';try{await _charAutoGenerateImage(rec),sheet._image=rec.image;const av=card==null?void 0:card.querySelector(".cs-avatar");av&&(av.outerHTML=csAvatarHtml(sheet)),toast("Profile image generated","success")}catch(err){toast("Image generation failed: "+(err.message||err),"error")}finally{btn.disabled=!1,btn.innerHTML=orig}}if(act==="concept_art"){const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span> Generating\u2026';try{const img=await _charAutoGenerateConceptArt(rec);sheet.concept_art_image=img;const box=btn.closest(".cs-prompt-box"),body=box==null?void 0:box.querySelector(".lcd-prompt-body"),existingPreview=body==null?void 0:body.querySelector(".cs-concept-art-preview");existingPreview?existingPreview.querySelector("img").src=img:body&&body.insertAdjacentHTML("afterbegin",`<div class="cs-concept-art-preview"><img src="${escHtml(img)}" alt="Concept art"></div>`),btn.innerHTML='<span class="mdi mdi-image-multiple-outline"></span> Regenerate Concept Art',toast("Concept art generated","success")}catch(err){toast("Concept art generation failed: "+(err.message||err),"error"),btn.innerHTML=orig}finally{btn.disabled=!1}}})}),root.querySelectorAll(".cs-edit-toggle").forEach(btn=>{const card=btn.closest(".cs-card");if(!card)return;const saveTimers=new Map;btn.addEventListener("click",e=>{e.stopPropagation();const editing=card.classList.toggle("cs-editing");btn.innerHTML=editing?'<span class="mdi mdi-check"></span>':'<span class="mdi mdi-pencil-outline"></span>',btn.title=editing?"Done editing":"Edit character sheet",card.querySelectorAll(".cs-field-editable[data-sheet-key]").forEach(el=>{el.contentEditable=editing?"true":"false",editing&&!el.dataset.wired&&(el.dataset.wired="1",el.addEventListener("input",()=>{const key=el.dataset.sheetKey,name=card.dataset.name,sheet=sheets.find(s=>s.name===name);!sheet||!key||(clearTimeout(saveTimers.get(key)),saveTimers.set(key,setTimeout(async()=>{sheet[key]=el.textContent.trim(),typeof csSaveToLibrary=="function"&&await csSaveToLibrary(book,[sheet])},900)))}))})})}),root.querySelectorAll(".cs-source-link, .cs-source-mark").forEach(btn=>{btn.addEventListener("click",async e=>{e.stopPropagation();const pg=parseInt(btn.dataset.page,10),quote=(btn.dataset.quote||"").trim();if(!pg){toast("No page reference saved for this source","error");return}if(typeof window.readerJumpToPage!="function"){toast(`Source: page ${pg}`,"info");return}if(await window.readerJumpToPage(pg),quote&&typeof readerSearchApply=="function"){const term=quote.length>60?quote.slice(0,60):quote;readerSearchApply(term,{jump:!0});const inp=document.getElementById("reader-search");inp&&(inp.value=term)}})})}async function csShow(sheets,title,sourceText,book,hostEl=null){var _a2;const inline=!!hostEl;if((_a2=document.getElementById("cs-overlay"))==null||_a2.remove(),typeof clGetAll=="function")try{const bk=book||title||"",all=await clGetAll(),imgMap=new Map(all.filter(r=>r.image).map(r=>[r.id,r.image]));sheets.forEach(s=>{const id=`${bk}::${s.name}`.toLowerCase();imgMap.has(id)&&(s._image=imgMap.get(id))})}catch{}const bkCtx=book||title||"";let allRecs=[];try{allRecs=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(bkCtx):[]}catch{allRecs=[]}const recByName=new Map(allRecs.map(r=>[String(r.name||"").toLowerCase(),r])),recFor=s=>recByName.get(String(s.name||"").toLowerCase())||{id:"",name:s.name,sheet:s,voice:s.voice,book:bkCtx,tags:""},recsById=new Map,cardsFor=list=>list.map(s=>{const rec=recFor(s);return rec.id&&recsById.set(rec.id,rec),typeof _charCardHtml=="function"?_charCardHtml(rec,allRecs):csCardHtml(s)}).join(""),main=sheets.filter(s=>s.tier==="main"),supp=sheets.filter(s=>s.tier!=="main"),group=(label,list)=>list.length?`<div class="cs-group-label">${label}</div>`+cardsFor(list):"",ov=inline?hostEl:document.createElement("div");if(inline?ov.innerHTML=`<div class="audiobook-box cs-box cs-page-shell" style="width:100%; max-width:none; max-height:none; height:auto;">
<div class="cs-titlebar">
<span class="audiobook-title"><span class="mdi mdi-account-details-outline"></span>
${escHtml(title||"Character sheets")}
<span class="audiobook-count">${sheets.length} character${sheets.length!==1?"s":""}</span>
</span>
<span style="flex:1"></span>
<button class="btn-secondary btn-sm" id="cs-back-cast-audio"><span class="mdi mdi-arrow-left"></span> Cast Audiobook</button>
<button class="btn-secondary btn-sm" id="cs-open-cast"><span class="mdi mdi-folder-account-outline"></span> Open Cast</button>
<button class="btn-secondary btn-sm" id="cs-copy"><span class="mdi mdi-content-copy"></span> Copy</button>
</div>
<div class="card-subtitle" style="margin:0 0 12px 0;">Generated sheets are saved into the cast library. Use the buttons above to jump back to the audiobook cast or continue to the roster stage.</div>
<div class="cs-list cs-list-compact">${group("Main characters",main)}${group("Supporting characters",supp)}</div>
</div>`:(ov.id="cs-overlay",ov.className="audiobook-overlay",ov.innerHTML=`<div class="audiobook-box cs-box">
<div class="cs-titlebar">
<span class="audiobook-title"><span class="mdi mdi-account-details-outline"></span>
${escHtml(title||"Character sheets")}
<span class="audiobook-count">${sheets.length} character${sheets.length!==1?"s":""}</span>
</span>
<span style="flex:1"></span>
<button class="btn-secondary btn-sm" id="cs-copy"><span class="mdi mdi-content-copy"></span> Copy</button>
<button class="btn-secondary btn-sm" id="cs-close">Close</button>
</div>
<div class="cs-list cs-list-compact">${group("Main characters",main)}${group("Supporting characters",supp)}</div>
</div>`,document.body.appendChild(ov),ov.addEventListener("click",e=>{e.target===ov&&ov.remove()})),csWireResultInteractions(ov,sheets,sourceText,book,inline),typeof _wireCharCards=="function"){const listEl=ov.querySelector(".cs-list")||ov;_wireCharCards(ov,recsById,allRecs,null,{container:listEl,onBack:()=>csShow(sheets,title,sourceText,book,hostEl)})}}async function csSaveToLibrary(book,sheets){if(typeof clUpsertMany=="function")try{const n=await clUpsertMany(book,sheets);n&&toast(`${n} character${n!==1?"s":""} saved to library`,"success")}catch{}}async function csAutoGenerateExternalPrompts(book,sheets){if(!Array.isArray(sheets)||!sheets.length)return;const target=typeof statusLlmTarget=="function"?statusLlmTarget():{url:"",model:""};let done=0,failed=0,repeatMsg="",repeatCount=0;toast(`Generating SillyTavern + Concept Art prompts for ${sheets.length} character${sheets.length!==1?"s":""}\u2026`,"info");for(const s of sheets)if(!(!s||!s.name))try{const sample=[s.physical,s.backstory,s.motivation].filter(Boolean).join(" "),language=typeof detectLang=="function"&&sample&&detectLang(sample)||"",r=await fetch("/api/character-generate-prompts",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:s.name,book,sheet:s,language,llm_url:target.url,model:target.model,fields:["silly_tavern_prompt","concept_art_prompt"]})});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);const d=await r.json();d.silly_tavern_prompt&&(s.silly_tavern_prompt=d.silly_tavern_prompt),d.concept_art_prompt&&(s.concept_art_prompt=d.concept_art_prompt),done++,repeatCount=0}catch(e){failed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto external prompts]",s.name,e),msg===repeatMsg?repeatCount++:(repeatMsg=msg,repeatCount=1),repeatCount>=3)break}try{await clUpsertMany(book,sheets)}catch{}const suffix=failed?` (${failed} failed${repeatMsg?": "+repeatMsg.slice(0,160):""})`:"";if(toast(`External prompts generated for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),typeof _charAutoGenerateConceptArt!="function")return;const withPrompt=sheets.filter(s=>s&&s.name&&_libStr(s.concept_art_prompt).trim());if(!withPrompt.length)return;let imgDone=0,imgFailed=0,imgRepeatMsg="",imgRepeatCount=0;toast(`Generating concept art for ${withPrompt.length} character${withPrompt.length!==1?"s":""}\u2026`,"info");for(const s of withPrompt)try{await _charAutoGenerateConceptArt({id:clKey(book,s.name),sheet:s}),imgDone++,imgRepeatCount=0}catch(e){imgFailed++;const msg=e&&e.message?e.message:String(e);if(console.error("[auto concept art]",s.name,e),msg===imgRepeatMsg?imgRepeatCount++:(imgRepeatMsg=msg,imgRepeatCount=1),imgRepeatCount>=3)break}const imgSuffix=imgFailed?` (${imgFailed} failed${imgRepeatMsg?": "+imgRepeatMsg.slice(0,160):""})`:"";toast(`Concept art generated for ${imgDone} character${imgDone!==1?"s":""}${imgSuffix}`,imgFailed&&!imgDone?"error":"success")}function csGoToLibrary(){typeof navTo=="function"&&navTo("s-library"),typeof navLibraryView=="function"?navLibraryView("characters"):typeof libraryRender=="function"&&libraryRender("characters"),typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib")}function csAttachLineCounts(sheets,counts){!counts||!counts.size||sheets.forEach(function(s){const n=counts.get(String(s.name||"").trim().toLowerCase());n!=null&&(s.line_count=n)})}async function csForReader(opts={}){var _a2;const fresh=!!opts.fresh,text=csReaderText(),book=readerState.title||"Untitled book",key="reader:"+(readerState.title||"")+":"+(typeof readerScopeIndices=="function"?readerScopeIndices().length:0);typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(!fresh&&_cs.cache[key]){await csSaveToLibrary(book,_cs.cache[key]),pageHost&&await csShow(_cs.cache[key],book,text,book,pageHost);return}const knownRoster=csKnownReaderRoster();let seedSheets=[];if(!fresh)try{if(typeof clGetAllByTagOrBook=="function"){const existing=await clGetAllByTagOrBook(book),rosterSet=new Set(knownRoster.map(n=>n.toLowerCase())),stillCast=rec=>!rosterSet.size||rosterSet.has(String((rec==null?void 0:rec.name)||"").trim().toLowerCase());seedSheets=(existing||[]).filter(stillCast).map(rec=>csSeedSheet(rec)).filter(s=>String(s.name||"").trim())}}catch{seedSheets=[]}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook,lineCounts=new Map;if((_a2=ab==null?void 0:ab.segments)!=null&&_a2.length)for(const s of ab.segments){if((s==null?void 0:s.type)!=="dialogue"||!s.speaker)continue;const k=String(s.speaker).trim().toLowerCase();lineCounts.set(k,(lineCounts.get(k)||0)+1)}const sheets=await csGenerate(text,key,knownRoster,{pageHost,seedSheets,lineCounts});if(sheets){if(!sheets.length){toast("No characters found","error");return}lineCounts.size&&csAttachLineCounts(sheets,lineCounts),await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,book,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success"),csAutoGenerateExternalPrompts(book,sheets)}}async function csForReaderSelective(selectedNames){var _a2;const wanted=new Set((selectedNames||[]).map(n=>String(n).trim().toLowerCase()));if(!wanted.size){toast("No characters selected","error");return}const text=csReaderText(),book=readerState.title||"Untitled book";typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();let records=[];try{records=typeof clGetAllByTagOrBook=="function"?await clGetAllByTagOrBook(book):[]}catch{records=[]}const picked=[],seen=new Set,matchesWanted=rec=>{var _a3,_b2,_c2,_d2;const tokens=csNameTokens({name:(rec==null?void 0:rec.name)||"",aliases:((_a3=rec==null?void 0:rec.sheet)==null?void 0:_a3.aliases)||"",first_name:((_b2=rec==null?void 0:rec.sheet)==null?void 0:_b2.first_name)||"",last_name:((_c2=rec==null?void 0:rec.sheet)==null?void 0:_c2.last_name)||"",full_name:((_d2=rec==null?void 0:rec.sheet)==null?void 0:_d2.full_name)||""});for(const t of tokens){const lower=String(t||"").toLowerCase();for(const w of wanted)if(lower===w||lower.includes(w)||w.includes(lower))return!0}return!1};if((records||[]).forEach(rec=>{if(!rec||!rec.name||!matchesWanted(rec))return;const key=String(rec.name||"").trim().toLowerCase();seen.has(key)||(seen.add(key),picked.push(rec))}),(selectedNames||[]).forEach(name=>{const key=String(name||"").trim().toLowerCase();if(!key||seen.has(key))return;const fallback={name:String(name||"").trim(),sheet:csBlankSheet(String(name||"").trim())};seen.add(key),picked.push(fallback)}),picked.sort((a,b)=>{var _a3,_b2;return(((_a3=b==null?void 0:b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a==null?void 0:a.sheet)==null?void 0:_b2.line_count)||0)||String((a==null?void 0:a.name)||"").localeCompare(String((b==null?void 0:b.name)||""))}),!picked.length){toast("No matching cast characters found","error");return}const finalMap=new Map;for(const rec of picked){const targetName=String(rec.name||"").trim();if(!targetName)continue;const needles=csRecordNeedles(rec),scanText=csEvidenceWindowText(csReaderParagraphBlocks(),needles,2,2)||text,seedSheet=csSeedSheet(rec),sheets=await csGenerate(scanText,null,[targetName],{pageHost,seedSheets:[seedSheet]});if(!sheets)return;const filtered2=sheets.filter(s=>wanted.has(String(s.name||"").trim().toLowerCase()));filtered2.length&&(((_a2=rec==null?void 0:rec.sheet)==null?void 0:_a2.line_count)!=null&&filtered2.forEach(s=>{s.line_count=rec.sheet.line_count}),csMerge(finalMap,filtered2))}const filtered=[...finalMap.values()];if(!filtered.length){toast("None of the selected characters turned up in this pass \u2014 try again or pick different ones","error");return}const ab=typeof _audiobook!="undefined"?_audiobook:window._audiobook;if(ab!=null&&ab.roster){const counts=new Map;ab.roster.forEach(function(info,name){counts.set(String(name).trim().toLowerCase(),info.count||0)}),csAttachLineCounts(filtered,counts)}await csSaveToLibrary(book,filtered),pageHost&&await csShow(filtered,book,text,book,pageHost),toast(filtered.length+" character"+(filtered.length!==1?"s":"")+" defined","success")}async function csForRehearser(){var _a2,_b2,_c2,_d2;const title=((_a2=$("reh-script-title"))==null?void 0:_a2.value.trim())||"Character sheets",book=((_b2=$("reh-script-title"))==null?void 0:_b2.value.trim())||"Untitled script",text=csRehearserText(),key="reh:"+title+":"+(rehState.lines||[]).length;typeof navTo=="function"&&navTo("s-reader"),typeof showReaderView=="function"&&showReaderView("chars"),typeof showReaderView=="function"&&setTimeout(()=>showReaderView("chars"),120);const pageHost=csReaderPageHost();if(_cs.cache[key]){await csSaveToLibrary(book,_cs.cache[key]),pageHost&&await csShow(_cs.cache[key],title,text,book,pageHost);return}let seedSheets=[];try{typeof clGetAllByTagOrBook=="function"&&(seedSheets=(await clGetAllByTagOrBook(book)||[]).map(rec=>csSeedSheet(rec)).filter(s=>String(s.name||"").trim()))}catch{seedSheets=[]}const sheets=await csGenerate(text,key,null,{pageHost,seedSheets});if(sheets){if(!sheets.length){toast("No characters found","error");return}if((_d2=(_c2=window.rehState)==null?void 0:_c2.lines)!=null&&_d2.length){const counts=new Map;rehState.lines.forEach(function(l){if(l.type!=="dialog"||!l.speaker)return;const k=String(l.speaker).trim().toLowerCase();counts.set(k,(counts.get(k)||0)+1)}),csAttachLineCounts(sheets,counts)}await csSaveToLibrary(book,sheets),pageHost&&await csShow(sheets,title,text,book,pageHost),toast(sheets.length+" character sheets saved \u2014 Library \u2192 Cast","success")}}window.csForReader=csForReader,window.csForReaderSelective=csForReaderSelective,window.csForRehearser=csForRehearser,(_rc=$("reader-charsheets-btn"))==null||_rc.addEventListener("click",csForReader),(_sc=$("reh-charsheets-btn"))==null||_sc.addEventListener("click",csForRehearser);const CL_EDIT_FIELDS=[["name","Name"],["aliases","Aliases / also known as"],["first_name","First name"],["last_name","Last name"],["title","Title / role"],["age_estimate","Estimated age"],["race_species","Race / species"],["languages","Languages"],["nationality_background","Nationality / background"],["social_class","Social class"],["archetype","Archetype"],["physical","Physical"],["clothing","Clothing & Appearance"],["alignment","Alignment & Ethos"],["arc_note","Arc note"],["skills","Trained Skills"],["capabilities","Capabilities"],["backstory","Backstory & Origin"],["relationships","Relationships"],["motivation","Motivation"],["fears","Fears"],["mannerisms","Mannerisms & Habits"],["communication_style","Communication style"],["reputation","Reputation"],["religious_beliefs","Religious beliefs"],["notes","Notes"],["voice_pattern","Voice & Speech"],["voice_design_prompt","Voice Design Prompt"],["image_prompt","Image Generation Prompt"],["silly_tavern_prompt","SillyTavern Character Prompt"],["concept_art_prompt","Concept Art Prompt"],["secret","Dark Secret / Fatal Flaw"],["conflict_style","Conflict Style"],["win_condition","Win Condition"]];async function clGetAll(){const r=await fetch("/api/characters");if(!r.ok)throw new Error("clGetAll failed: "+r.status);return(await r.json()).characters||[]}async function clGet(id){const r=await fetch("/api/characters/"+encodeURIComponent(id));if(r.status!==404){if(!r.ok)throw new Error("clGet failed: "+r.status);return r.json()}}async function clPut(rec){rec!=null&&rec.color&&(rec.sheet=rec.sheet||{},rec.sheet.color=clNormalizeColor(rec.color,rec.name));const r=await fetch("/api/characters/"+encodeURIComponent(rec.id),{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify(rec)});if(!r.ok)throw new Error("clPut failed: "+r.status);const saved=await r.json();return typeof window._rehSyncCastVoiceFromLibrary=="function"&&window._rehSyncCastVoiceFromLibrary(saved),saved}async function clDelete(id){const r=await fetch("/api/characters/"+encodeURIComponent(id),{method:"DELETE"});if(!r.ok)throw new Error("clDelete failed: "+r.status)}function clKey(book,name){return`${String(book||"").trim()}::${String(name||"").trim()}`.toLowerCase()}function clHslToHex(h,s,l){s/=100,l/=100;const k=n=>(n+h/30)%12,a=s*Math.min(l,1-l),f=n=>l-a*Math.max(-1,Math.min(k(n)-3,Math.min(9-k(n),1)));return"#"+[f(0),f(8),f(4)].map(x=>Math.round(255*x).toString(16).padStart(2,"0")).join("")}function clNameHue(name){return Math.abs((name||"?").split("").reduce((h,c)=>(h*31+c.charCodeAt(0))%360,0))}function clNormalizeColor(color,name){const c=String(color||"").trim();return/^#[0-9a-f]{6}$/i.test(c)?c:/^#[0-9a-f]{3}$/i.test(c)?"#"+c.slice(1).split("").map(ch=>ch+ch).join(""):clHslToHex(clNameHue(name),58,43)}const CL_IDENTITY_FIELDS=["name","aliases","first_name","last_name","full_name"],CL_ALIAS_MAX_TOKENS=12,CL_ALIAS_MAX_CHARS=500,CL_ALIAS_STOPWORDS=new Set(["die","der","das","den","dem","des","ein","eine","einer","er","sie","es","ich","du","wir","ihr","the","a","an","he","she","it","they","who"]);function clSplitIdentityTokens(v,opts={}){const raw=_clStr(v),parts=raw.split(/[,;/|]|\baka\b|\baka\.\b|\balias(?:es)?\b|\bgenannt\b|\bnamens\b|\bcalled\b|\bknown as\b/i).map(x=>x.trim()).filter(Boolean).filter(x=>x.length<=80&&!/^needs?:/i.test(x)&&!/^complete$/i.test(x)).filter(x=>!CL_ALIAS_STOPWORDS.has(x.toLowerCase()));return opts.aliases&&(raw.length>CL_ALIAS_MAX_CHARS||parts.length>CL_ALIAS_MAX_TOKENS)?[]:parts.slice(0,opts.aliases?CL_ALIAS_MAX_TOKENS:void 0)}function clIdentityNames(recOrSheet){const s=(recOrSheet==null?void 0:recOrSheet.sheet)||recOrSheet||{},out=new Set,add=(v,opts={})=>clSplitIdentityTokens(v,opts).forEach(x=>out.add(x.toLowerCase()));return add((recOrSheet==null?void 0:recOrSheet.name)||s.name),CL_IDENTITY_FIELDS.filter(k=>k!=="name").forEach(k=>add(s[k],{aliases:k==="aliases"})),out}function clMergeAliases(existing,incoming){const names=new Map,add=v=>clSplitIdentityTokens(v,{aliases:!0}).forEach(x=>names.set(x.toLowerCase(),x));return add(existing.aliases),add(incoming.aliases),incoming.name&&incoming.name!==existing.name&&add(incoming.name),[...names.values()].filter(n=>n.toLowerCase()!==String(existing.name||"").toLowerCase()).join(", ")}function clSameIdentity(rec,book,sheet){if(String((rec==null?void 0:rec.book)||"").trim().toLowerCase()!==String(book||"").trim().toLowerCase())return!1;const a=clIdentityNames(rec),b=clIdentityNames(sheet);for(const n of b)if(a.has(n))return!0;return!1}function _clStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _clSanitize(sheet){const out={...sheet};return(typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name")).forEach(f=>{out[f]!=null&&(out[f]=_clStr(out[f]))}),out}function clMergeSheet(existing,incoming){const e={...existing},inc=_clSanitize(incoming),scalars=typeof CS_SCALAR_FIELDS!="undefined"?CS_SCALAR_FIELDS:CL_EDIT_FIELDS.map(f=>f[0]).filter(k=>k!=="name"),oldAliases=e.aliases;return scalars.forEach(f=>{(inc[f]||"").length>(e[f]||"").length&&(e[f]=inc[f])}),e.aliases=clMergeAliases({...e,aliases:oldAliases},incoming),incoming.tier==="main"&&(e.tier="main"),incoming.moral_alignment_score!=null&&(e.moral_alignment_score=e.moral_alignment_score!=null?Math.round((e.moral_alignment_score+incoming.moral_alignment_score)/2):incoming.moral_alignment_score),incoming.arc_direction&&incoming.arc_direction!=="neutral"&&(e.arc_direction=incoming.arc_direction),incoming.gender&&!e.gender&&(e.gender=incoming.gender),incoming.line_count!=null&&(e.line_count=incoming.line_count),e.inventory=[...existing.inventory||[]],(incoming.inventory||[]).forEach(it=>{it&&!e.inventory.includes(it)&&e.inventory.length<3&&e.inventory.push(it)}),e.sources=[...existing.sources||[]],(incoming.sources||[]).forEach(src=>{src&&src.quote&&e.sources.length<12&&!e.sources.some(x=>x.quote===src.quote)&&e.sources.push(src)}),e}function clMergeTags(...parts){const set=new Set;return parts.forEach(p=>String(p||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))),[...set].join(", ")}async function clUpsert(book,sheet,knownId){var _a2;const name=(sheet.name||"").trim();if(!name)return null;const bk=(book||"").trim()||"Unsorted",aliasPrev=knownId?await clGet(knownId).catch(()=>null):(await clGetAll().catch(()=>[])).find(r=>clSameIdentity(r,bk,sheet)),id=(aliasPrev==null?void 0:aliasPrev.id)||knownId||clKey(bk,name),now=new Date,prev=aliasPrev||await clGet(id).catch(()=>null),merged=prev?clMergeSheet(prev.sheet||{},sheet):{..._clSanitize(sheet),name},canonicalName=(prev==null?void 0:prev.name)||name;merged.name=canonicalName;const tags=clMergeTags(prev==null?void 0:prev.tags,sheet.tags,bk),color=clNormalizeColor((prev==null?void 0:prev.color)||((_a2=prev==null?void 0:prev.sheet)==null?void 0:_a2.color)||sheet.color,canonicalName);merged.color=color;const rec={id,book:bk,name:canonicalName,tags,sheet:merged,color,analysis:(prev==null?void 0:prev.analysis)||null,voice:sheet.voice||(prev==null?void 0:prev.voice)||null,image:sheet.image||(prev==null?void 0:prev.image)||null,created:(prev==null?void 0:prev.created)||now,updated:now};return await clPut(rec),rec}async function clSetImage(id,dataUrl){const rec=await clGet(id).catch(()=>null);if(rec)return rec.image=dataUrl||null,rec.updated=new Date,await clPut(rec),rec}window.clSetImage=clSetImage;async function clGetAllByTagOrBook(title){const key=String(title||"").trim().toLowerCase();return key?(await clGetAll().catch(()=>[])).filter(r=>String(r.book||"").trim().toLowerCase()===key?!0:String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===key)):[]}async function clUpsertMany(book,sheets){let n=0;for(const s of sheets||[])await clUpsert(book,s)&&n++;return n}(async function(){try{if((await clGetAll()).length>0)return;const idbRecs=await _clIdbGetAll().catch(()=>[]);if(!idbRecs.length)return;const r=await fetch("/api/characters/migrate",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(idbRecs)});if(r.ok){const d=await r.json();console.log(`[characters-library] migrated ${d.imported} records from IndexedDB \u2192 SQLite`)}}catch(e){console.warn("[characters-library] migration skipped:",e)}})();function _clIdbGetAll(){return new Promise((resolve,reject)=>{const req=indexedDB.open("character-library",1);req.onerror=()=>resolve([]),req.onsuccess=e=>{const db=e.target.result;if(!db.objectStoreNames.contains("characters")){db.close(),resolve([]);return}const all=db.transaction("characters","readonly").objectStore("characters").getAll();all.onsuccess=ev=>{db.close(),resolve(ev.target.result||[])},all.onerror=()=>{db.close(),resolve([])}}})}let _clRecords=[];async function clRender(){if(!document.getElementById("cl-grid"))return;const filterEl=document.getElementById("cl-book-filter"),searchEl=document.getElementById("cl-search");filterEl&&!filterEl.dataset.bound&&(filterEl.dataset.bound="1",filterEl.addEventListener("change",clApplyFilter)),searchEl&&!searchEl.dataset.bound&&(searchEl.dataset.bound="1",searchEl.addEventListener("input",clApplyFilter));try{_clRecords=await clGetAll()}catch{_clRecords=[]}const filter=document.getElementById("cl-book-filter"),prods=clAllProductions();if(filter){const cur=filter.value;filter.innerHTML=`<option value="">All productions (${prods.length})</option>`+prods.map(b=>`<option value="${escHtml(b)}">${escHtml(b)}</option>`).join(""),cur&&prods.includes(cur)&&(filter.value=cur)}clApplyFilter()}function clAllProductions(){const set=new Set;return _clRecords.forEach(r=>{r.book&&set.add(r.book),String(r.tags||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>set.add(t))}),[...set].sort((a,b)=>a.localeCompare(b))}function clApplyFilter(){var _a2,_b2;const grid=document.getElementById("cl-grid");if(!grid)return;const book=((_a2=document.getElementById("cl-book-filter"))==null?void 0:_a2.value)||"",q=(((_b2=document.getElementById("cl-search"))==null?void 0:_b2.value)||"").trim().toLowerCase();let recs=_clRecords.slice();if(book){const bk=book.toLowerCase();recs=recs.filter(r=>(r.book||"").toLowerCase()===bk||String(r.tags||"").split(",").some(t=>t.trim().toLowerCase()===bk))}if(q&&(recs=recs.filter(r=>{var _a3,_b3,_c2,_d2,_e2;return(r.name||"").toLowerCase().includes(q)||(((_a3=r.sheet)==null?void 0:_a3.aliases)||"").toLowerCase().includes(q)||(((_b3=r.sheet)==null?void 0:_b3.first_name)||"").toLowerCase().includes(q)||(((_c2=r.sheet)==null?void 0:_c2.last_name)||"").toLowerCase().includes(q)||(((_d2=r.sheet)==null?void 0:_d2.title)||"").toLowerCase().includes(q)||(((_e2=r.sheet)==null?void 0:_e2.archetype)||"").toLowerCase().includes(q)||(r.tags||"").toLowerCase().includes(q)||(r.book||"").toLowerCase().includes(q)})),!recs.length){grid.innerHTML=`<div class="cl-empty">
<span class="mdi mdi-account-box-multiple-outline" style="font-size:48px;opacity:0.4"></span>
<p>${_clRecords.length?"No characters match your filter.":"No characters yet."}</p>
<p class="cl-empty-hint">Run <b>Character sheets</b> from Read Aloud or the Script Rehearser to populate your library.</p>
</div>`;return}const byBook=new Map;for(const r of recs)byBook.has(r.book)||byBook.set(r.book,[]),byBook.get(r.book).push(r);const groups=[...byBook.keys()].sort((a,b)=>a.localeCompare(b));grid.innerHTML=groups.map(bk=>{const list=byBook.get(bk).sort((a,b)=>{var _a3,_b3;return(((_a3=a.sheet)==null?void 0:_a3.tier)==="main"?0:1)-(((_b3=b.sheet)==null?void 0:_b3.tier)==="main"?0:1)||(a.name||"").localeCompare(b.name||"")});return`<div class="cl-book-group">
<div class="cl-book-head"><span class="mdi mdi-book-open-page-variant-outline"></span> ${escHtml(bk)}
<span class="cl-book-count">${list.length}</span></div>
<div class="cl-book-cards">${list.map(clCardHtml).join("")}</div>
</div>`}).join("")}function clCardHtml(rec){return`<div class="cl-card-wrap" data-id="${escHtml(rec.id)}">
<div class="cl-card-tools">
<button class="btn-secondary btn-sm cl-edit" data-id="${escHtml(rec.id)}" title="Edit this character"><span class="mdi mdi-pencil-outline"></span> Edit</button>
<button class="btn-secondary btn-sm cl-delete" data-id="${escHtml(rec.id)}" title="Delete from library"><span class="mdi mdi-trash-can-outline"></span></button>
</div>
${typeof csOverviewCardHtml=="function"?csOverviewCardHtml(rec):csCardHtml(rec.sheet)}
</div>`}function clEdit(id){var _a2;const rec=_clRecords.find(r=>r.id===id);if(!rec)return;const s=rec.sheet||{},modal=document.createElement("div");modal.className="audiobook-overlay";const rows=CL_EDIT_FIELDS.map(([k,label])=>{const multiline=!["name","aliases","archetype"].includes(k),val=escHtml(String(s[k]||""));return`<label class="cl-edit-row"><span>${label}</span>${multiline?`<textarea data-field="${k}" rows="2">${val}</textarea>`:`<input type="text" data-field="${k}" value="${val}">`}</label>`}).join("");modal.innerHTML=`<div class="audiobook-box cl-edit-box">
<div class="cs-titlebar">
<span class="audiobook-title"><span class="mdi mdi-pencil-outline"></span> Edit \u2014 ${escHtml(rec.name)}</span>
<span style="flex:1"></span>
<button class="btn-secondary btn-sm" id="cl-edit-cancel">Cancel</button>
<button class="btn-primary btn-sm" id="cl-edit-save"><span class="mdi mdi-content-save-outline"></span> Save</button>
</div>
<div class="cl-edit-body">
<label class="cl-edit-row"><span>Tags / productions (comma-separated \u2014 reuse across books &amp; scripts)</span>
<input type="text" data-rec-field="tags" value="${escHtml(String(rec.tags||rec.book||""))}" placeholder="e.g. Das Jahr des Greifen, Star-Trek"></label>
<label class="cl-edit-row cl-edit-color"><span>Character color</span>
<input type="color" data-rec-field="color" value="${escHtml(clNormalizeColor(rec.color||s.color,rec.name))}"></label>
<label class="cl-edit-row"><span>Moral alignment (0 evil \u2013 100 good)</span>
<input type="number" min="0" max="100" data-field="moral_alignment_score" value="${(_a2=s.moral_alignment_score)!=null?_a2:50}"></label>
<label class="cl-edit-row"><span>Arc direction</span>
<select data-field="arc_direction">${["stable-good","stable-bad","neutral","good-to-bad","bad-to-good","complex"].map(o=>`<option value="${o}"${(s.arc_direction||"neutral")===o?" selected":""}>${o}</option>`).join("")}</select></label>
${rows}
</div>
</div>`,document.body.appendChild(modal);const close=()=>modal.remove();modal.querySelector("#cl-edit-cancel").addEventListener("click",close),modal.addEventListener("click",e=>{e.target===modal&&close()}),modal.querySelector("#cl-edit-save").addEventListener("click",async()=>{const patch={};modal.querySelectorAll("[data-field]").forEach(el=>{patch[el.dataset.field]=el.value});const newName=(patch.name||"").trim()||rec.name;let mas=parseInt(patch.moral_alignment_score,10);mas=isNaN(mas)?50:Math.max(0,Math.min(100,mas));const newSheet={...s,...patch,name:newName,moral_alignment_score:mas},tagsEl=modal.querySelector('[data-rec-field="tags"]'),newTags=tagsEl?clMergeTags(tagsEl.value,rec.book):rec.tags||rec.book,colorEl=modal.querySelector('[data-rec-field="color"]'),color=clNormalizeColor((colorEl==null?void 0:colorEl.value)||rec.color||s.color,newName),newId=clKey(rec.book,newName);newSheet.color=color,rec.sheet=newSheet,rec.name=newName,rec.tags=newTags,rec.color=color,rec.updated=new Date;try{newId!==rec.id&&(await clDelete(rec.id),rec.id=newId),await clPut(rec),toast("Character saved","success"),close(),clRender()}catch(e){toast("Save failed: "+(e.message||e),"error")}})}async function clDeleteRec(id){const rec=_clRecords.find(r=>r.id===id);if(rec&&confirm(`Delete "${rec.name}" from ${rec.book}?`))try{await clDelete(id),toast("Deleted","success"),clRender()}catch(e){toast("Delete failed: "+(e.message||e),"error")}}document.addEventListener("click",e=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l2,_m2,_n2;const edit=(_b2=(_a2=e.target).closest)==null?void 0:_b2.call(_a2,".cl-edit");if(edit){e.stopPropagation(),clEdit(edit.dataset.id);return}const del=(_d2=(_c2=e.target).closest)==null?void 0:_d2.call(_c2,".cl-delete");if(del){e.stopPropagation(),clDeleteRec(del.dataset.id);return}const deep=(_f2=(_e2=e.target).closest)==null?void 0:_f2.call(_e2,".cl-card-wrap .cs-deep-btn");if(deep){e.stopPropagation();const rec=_clRecords.find(r=>{var _a3;return r.id===((_a3=deep.closest(".cl-card-wrap"))==null?void 0:_a3.dataset.id)});rec&&typeof csDeepAnalysis=="function"&&csDeepAnalysis(rec.sheet,"")}const img=(_h2=(_g2=e.target).closest)==null?void 0:_h2.call(_g2,".cl-card-wrap .cs-img-btn");if(img){e.stopPropagation();const rec=_clRecords.find(r=>{var _a3;return r.id===((_a3=img.closest(".cl-card-wrap"))==null?void 0:_a3.dataset.id)});rec&&typeof csBuildImagePrompt=="function"&&((_i2=navigator.clipboard)==null||_i2.writeText(csBuildImagePrompt(rec.sheet)).then(()=>toast("Image prompt copied","success"),()=>toast("Copy failed","error")));return}const voice=(_k2=(_j2=e.target).closest)==null?void 0:_k2.call(_j2,".cl-card-wrap .cs-voice-btn");if(voice){e.stopPropagation();const rec=_clRecords.find(r=>{var _a3;return r.id===((_a3=voice.closest(".cl-card-wrap"))==null?void 0:_a3.dataset.id)});rec&&typeof csBuildVoicePrompt=="function"&&((_l2=navigator.clipboard)==null||_l2.writeText(csBuildVoicePrompt(rec.sheet)).then(()=>toast("Voice prompt copied","success"),()=>toast("Copy failed","error")));return}const src=(_n2=(_m2=e.target).closest)==null?void 0:_n2.call(_m2,".cl-card-wrap .cs-source-link, .cl-card-wrap .cs-source-mark");if(src){e.stopPropagation();const pg=parseInt(src.dataset.page,10);pg&&typeof window.readerJumpToPage=="function"?window.readerJumpToPage(pg):pg&&toast(`Source: page ${pg}`,"info")}}),window.clRender=clRender,window.clUpsertMany=clUpsertMany,window.clUpsert=clUpsert,window.clGetAll=clGetAll,window.clPut=clPut,window.clGetAllByTagOrBook=clGetAllByTagOrBook;function stReadPngText(buf){const dv=new DataView(buf),sig=[137,80,78,71,13,10,26,10];for(let i=0;i<8;i++)if(dv.getUint8(i)!==sig[i])throw new Error("Not a PNG file");let off=8;const out={};for(;off+8<=dv.byteLength;){const len=dv.getUint32(off);off+=4;let type="";for(let i=0;i<4;i++)type+=String.fromCharCode(dv.getUint8(off+i));if(off+=4,type==="tEXt"){const bytes=new Uint8Array(buf,off,len);let sep=bytes.indexOf(0);sep===-1&&(sep=len);let keyword="";for(let i=0;i<sep;i++)keyword+=String.fromCharCode(bytes[i]);const text=new TextDecoder("latin1").decode(bytes.subarray(sep+1));out[keyword]=text}if(off+=len+4,type==="IEND")break}return out}function stB64ToJson(b64){const bin=atob(b64.trim()),bytes=new Uint8Array(bin.length);for(let i=0;i<bin.length;i++)bytes[i]=bin.charCodeAt(i);return JSON.parse(new TextDecoder("utf-8").decode(bytes))}function stNormalize(card){if(!card||typeof card!="object")throw new Error("Invalid card JSON");if(card.data&&typeof card.data=="object"&&card.data.name!=null)return card.data;if(card.name!=null)return card;throw new Error("Unrecognised character-card structure")}async function stParseFile(file){if((file.name||"").toLowerCase().endsWith(".png")){const buf=await file.arrayBuffer(),chunks=stReadPngText(buf),raw=chunks.ccv3||chunks.chara;if(!raw)throw new Error("PNG has no embedded character card");return stNormalize(stB64ToJson(raw))}const txt=await file.text();return stNormalize(JSON.parse(txt))}function stGuessGender(text){const t=String(text||"").toLowerCase(),f=(t.match(/\b(she|her|female|woman|girl|mother|daughter|sister|queen|lady|mrs|miss)\b/g)||[]).length,m=(t.match(/\b(he|him|his|male|man|boy|father|son|brother|king|lord|mr|sir)\b/g)||[]).length;return f>m*1.3?"female":m>f*1.3?"male":""}function stToSheet(data){const tags=(Array.isArray(data.tags)?data.tags:String(data.tags||"").split(",")).map(t=>String(t||"").trim()).filter(Boolean).join(", "),ext=data.extensions||{};return{name:(data.name||data.char_name||"Unnamed").trim(),archetype:(data.creator_notes||"").split(`
`)[0].slice(0,60)||"",backstory:data.description||data.char_persona||"",mannerisms:data.personality||"",arc_note:data.scenario||"",voice_pattern:data.mes_example||data.example_dialogue||data.first_mes||data.char_greeting||"",gender:ext.gender||stGuessGender((data.description||"")+" "+(data.personality||"")),note:data.creator_notes||"",tags,voice:ext.tts_voice||null,tier:"supporting",st_card:data}}function stFromRecord(rec){const sh=rec.sheet||{},prev=sh.st_card||{},descParts=[sh.physical,sh.clothing,sh.backstory,sh.alignment].map(x=>String(x||"").trim()).filter(Boolean),persona=[sh.archetype,sh.mannerisms,sh.motivation,sh.fears].map(x=>String(x||"").trim()).filter(Boolean).join(`
`);return{spec:"chara_card_v2",spec_version:"2.0",data:{name:rec.name,description:prev.description||descParts.join(`
`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'<div class="reh-book lib-book-sk" style="pointer-events:none"><div class="sk" style="height:14px;width:70%;margin-bottom:8px;border-radius:4px"></div><div class="sk" style="height:10px;width:45%;border-radius:4px;margin-bottom:6px"></div><div class="sk" style="height:6px;width:90%;border-radius:3px"></div></div>').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='<div class="lib-empty"><span class="mdi mdi-book-open-outline"></span><p>No books yet.</p><p class="lib-empty-hint">Open <b>Read Aloud</b>, import a PDF or text, and save it to your library.</p></div>';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'<div class="reh-book lib-book" data-id="'+rec.id+'" data-title="'+escHtml(rec.title||"")+'" '+bg+' title="'+escHtml(rec.title||"")+'"><div class="reh-book-actions"><button class="reh-book-act lib-act-rehearse" title="Rehearse this title"><span class="mdi mdi-theater"></span></button><button class="reh-book-act lib-act-del-book" title="Delete book"><span class="mdi mdi-delete-outline"></span></button></div><div class="reh-book-title">'+escHtml(rec.title||"Untitled")+'</div><div class="reh-book-meta">'+total+" sentences"+(rec.pageCount?" \xB7 "+rec.pageCount+" pg":"")+'<div class="reh-book-progress" title="'+synthPct+'% audio"><div style="width:'+synthPct+'%"></div></div><div style="margin-top:4px;opacity:.8"><span class="mdi '+(rec.kind==="pdf"?"mdi-file-pdf-box":"mdi-text-box-outline")+'"></span> '+readPct+"% read \xB7 "+date+"</div></div></div>"}).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='<div class="lib-empty"><span class="mdi mdi-theater"></span><p>No theater plays yet.</p><p class="lib-empty-hint">Open <b>Script Rehearsal</b> \u2192 Import / Export to add a script, or cast a book as an audiobook.</p></div>';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'<span style="background:'+((rec.cast[sp]||{}).color||"#888")+'">'+(sp[0]||"?").toUpperCase()+"</span>"}).join("");return'<div class="reh-book lib-play" data-id="'+rec.id+'" data-title="'+escHtml(rec.title||"")+'" style="--bk1:'+cov.c1+";--bk2:"+cov.c2+'" title="'+escHtml(rec.title||"")+'"><div class="reh-book-actions"><button class="reh-book-act lib-act-readaloud" title="Read aloud"><span class="mdi mdi-book-open-page-variant-outline"></span></button><button class="reh-book-act lib-act-del-play" title="Delete rehearsal"><span class="mdi mdi-delete-outline"></span></button></div><div class="reh-book-title">'+escHtml(rec.title||"Untitled")+'</div><div class="reh-book-meta"><div class="reh-book-avatars">'+avatars+"</div>"+total+" lines \xB7 "+speakers.length+' cast<div class="reh-book-progress"><div style="width:'+pct+'%"></div></div><div style="margin-top:4px;opacity:.8">'+pct+"% \xB7 "+date+"</div></div></div>"}).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<s.length;i++)h=(h*31+s.charCodeAt(i))%360;return{c1:"hsl("+h+",55%,42%)",c2:"hsl("+(h+40)%360+",58%,30%)"}}function libConfirmDelete(cardEl,heading,sub,onConfirm){if(cardEl.querySelector(".reh-book-del-confirm"))return;const o=document.createElement("div");o.className="reh-book-del-confirm",o.style.cssText="position:absolute;inset:0;background:rgba(0,0,0,.85);color:#fff;display:flex;flex-direction:column;justify-content:center;align-items:center;border-radius:inherit;z-index:10;padding:12px;text-align:center;box-sizing:border-box",o.innerHTML='<div style="font-weight:600;margin-bottom:8px;font-size:14px">'+heading+'</div><div style="font-size:11px;opacity:.8;margin-bottom:12px;line-height:1.4">'+sub+'</div><div style="display:flex;gap:8px"><button class="btn-secondary btn-sm" data-lib-cancel style="background:rgba(255,255,255,.15);border:none;color:#fff;padding:6px 12px">Cancel</button><button class="btn-primary btn-sm" data-lib-ok style="background:var(--red,#ef4444);border:none;color:#fff;padding:6px 12px">Delete</button></div>',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='<span class="mdi mdi-loading mdi-spin" style="font-size:24px"></span>',await onConfirm()}),cardEl.appendChild(o)}window.libraryRenderBooks=libraryRenderBooks,window.libraryRenderPlays=libraryRenderPlays,window.productionOpenInRehearser=productionOpenInRehearser,window.productionOpenInReader=productionOpenInReader,window.prodKey=prodKey;async function libraryRenderCharacters(){var _a2;const container=document.getElementById("lib-chars-list");if(!container)return;let all=[];try{all=typeof clGetAll=="function"?await clGetAll():[]}catch(e){console.warn("[characters] load failed",e),typeof toast=="function"&&toast("Failed to load characters \u2014 keeping the current view","error");return}const mainEl=document.getElementById("main-content"),savedScrollTop=!window._libCharsScrollToBook&&mainEl?mainEl.scrollTop:null;container.innerHTML='<div class="lib-chars-loading"><span class="mdi mdi-loading mdi-spin"></span> Loading characters\u2026</div>';const byId=new Map(all.map(function(rec){return[rec.id,rec]}));if(!all.length){container.innerHTML='<div class="lib-chars-toolbar"><button class="btn-secondary btn-sm" id="lib-chars-import" title="Import character cards from SillyTavern (.json or .png)"><span class="mdi mdi-import"></span> Import from SillyTavern</button></div><div class="lib-empty"><span class="mdi mdi-account-box-multiple-outline"></span><p>No characters yet.</p><p class="lib-empty-hint">Open a book in <b>Read Aloud</b>, cast it as an audiobook, then click <b>Cast Characters</b> to generate character sheets \u2014 or import an existing cast from SillyTavern.</p></div>',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"]],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?'<button class="btn-secondary btn-sm" id="lib-chars-back-reader" title="Return to Read Aloud"><span class="mdi mdi-arrow-left"></span> Back to reader</button>':"")+'<button class="btn-secondary btn-sm" id="lib-chars-import" title="Import character cards from SillyTavern (.json or .png)"><span class="mdi mdi-import"></span> Import from SillyTavern</button><label class="lib-chars-sort"><span class="mdi mdi-sort"></span> Sort<select id="lib-chars-sort-sel">'+SORT_OPTIONS.map(function(o){return'<option value="'+o[0]+'"'+(o[0]===sortMode?" selected":"")+">"+o[1]+"</option>"}).join("")+'</select></label><div class="lib-chars-view-toggle"><button class="btn-secondary btn-sm'+(viewMode==="cards"?" is-active":"")+'" data-view="cards"><span class="mdi mdi-view-grid-outline"></span> Cards</button><button class="btn-secondary btn-sm'+(viewMode==="table"?" is-active":"")+'" data-view="table"><span class="mdi mdi-table"></span> Table</button></div>',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||"")},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='<div class="lib-chars-prod-head" style="--pk1:'+cov.c1+";--pk2:"+cov.c2+'"><button type="button" class="lib-chars-prod-collapse-btn" title="Collapse/expand this production"><span class="mdi mdi-chevron-down"></span></button><div class="lib-chars-prod-title"><span class="mdi mdi-bookshelf"></span> '+escHtml(book)+'</div><div class="lib-chars-prod-actions"><button class="btn-secondary btn-sm lib-chars-bookctx-btn" data-book="'+escHtml(book)+'" title="Set genre, setting, era and language for this book \u2014 gives every voice/image prompt real context instead of guessing per character"><span class="mdi mdi-book-cog-outline"></span> Context</button><button class="btn-secondary btn-sm lib-chars-casting-btn" data-book="'+escHtml(book)+'" title="Back to the casting script (speaker attribution)"><span class="mdi mdi-drama-masks"></span> Casting</button><button class="btn-secondary btn-sm lib-chars-cast-btn" data-book="'+escHtml(book)+'" title="Re-run character sheet generation"><span class="mdi mdi-account-details-outline"></span> Cast Characters</button><button class="btn-secondary btn-sm lib-chars-reh-btn" data-book="'+escHtml(book)+'" title="Open in Script Rehearsal"><span class="mdi mdi-theater"></span> Rehearse</button><button class="btn-secondary btn-sm lib-chars-read-btn" data-book="'+escHtml(book)+'" title="Open in Read Aloud"><span class="mdi mdi-book-open-page-variant-outline"></span> Read Aloud</button><button class="btn-secondary btn-sm lib-chars-imp-btn" data-book="'+escHtml(book)+'" title="Import SillyTavern cards into this production"><span class="mdi mdi-import"></span></button><button class="btn-secondary btn-sm lib-chars-select-all-btn" data-book="'+escHtml(book)+'" title="Select or deselect every character in this production"><span class="mdi mdi-checkbox-multiple-marked-outline"></span> Select all</button><button class="btn-secondary btn-sm lib-chars-bulk-voice-btn" data-book="'+escHtml(book)+'" title="Auto-assign an existing library voice to every checked character" disabled><span class="mdi mdi-account-voice"></span> Auto-assign selected (<span class="lib-chars-bulk-count">0</span>)</button><button class="btn-secondary btn-sm lib-chars-bulk-design-btn" data-book="'+escHtml(book)+'" title="Design and save a brand-new voice for every checked character, from their Voice Design Prompt" disabled><span class="mdi mdi-creation"></span> Auto-design voices (<span class="lib-chars-bulk-count-design">0</span>)</button><select class="lib-chars-image-provider" title="Image generation engine for this run \u2014 overrides the Active Provider in Settings just for this run"><option value="">(Active Provider)</option><option value="openai">OpenAI</option><option value="google">Google</option><option value="openrouter">OpenRouter</option><option value="pollinations">Pollinations.ai (free)</option><option value="comfyui">Local ComfyUI</option></select><button class="btn-secondary btn-sm lib-chars-bulk-image-btn" data-book="'+escHtml(book)+'" title="Generate a profile picture for every checked character, from their Character Image Prompt" disabled><span class="mdi mdi-image-outline"></span> Auto-generate images (<span class="lib-chars-bulk-count-image">0</span>)</button><button class="btn-secondary btn-sm lib-chars-bulk-delete-btn" data-book="'+escHtml(book)+'" title="Delete every checked character from the library \u2014 use this to clear out stale/corrupted entries before a fresh recast" disabled><span class="mdi mdi-trash-can-outline"></span> Delete selected (<span class="lib-chars-bulk-count-delete">0</span>)</button><button class="btn-secondary btn-sm lib-chars-fix-lang-btn" data-book="'+escHtml(book)+`" title="Find every character whose current voice does not match this book's language, and design a properly-matching one \u2014 no selection needed, scans the whole production"><span class="mdi mdi-earth-arrow-right"></span> Fix wrong-language voices</button></div></div><div class="lib-chars-prod-body">`+(viewMode==="table"?_charsTableHtml(chars,sortMode,sortDir):'<div class="lib-chars-grid" id="lib-chars-grid-'+encodeURIComponent(book).replace(/%/g,"_")+'">'+chars.map(function(rec){return _charCardHtml(rec,chars)}).join("")+"</div>")+"</div>",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 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"),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&&checkedN<boxes.length},refreshBulkBtn=function(){const n=prod.querySelectorAll(".lib-char-select-cb:checked").length;bulkCount.textContent=n,bulkBtn.disabled=n===0,designCount.textContent=n,designBtn.disabled=n===0,imageCount.textContent=n,imageBtn.disabled=n===0,deleteCount.textContent=n,deleteBtn.disabled=n===0,syncTblSelectAllCb()};prod.querySelectorAll(".lib-char-select-cb").forEach(function(cb){cb.addEventListener("change",refreshBulkBtn)}),selectAllBtn.addEventListener("click",function(){const boxes=[...prod.querySelectorAll(".lib-char-select-cb")],allChecked=boxes.length>0&&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='<span class="mdi mdi-loading mdi-spin"></span> '+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)})});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),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)}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'<div class="lib-char-align"><span class="lib-char-align-evil" title="Evil">\u25CF</span><div class="lib-char-align-bar" title="'+(pct>=70?"Good":pct<=30?"Evil":"Morally ambiguous")+" ("+pct+'/100)"><div class="lib-char-align-dot" style="left:'+pct+'%"></div></div><span class="lib-char-align-good" title="Good">\u25CF</span><span class="lib-char-align-arrow" style="color:'+a.color+'" title="'+a.tip+'">'+a.ch+"</span></div>"}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?'<div class="lib-char-rels">'+hits.map(function(x){return'<span class="lib-char-rel-dot" style="background:hsl('+_charHue(x.c.name)+',55%,38%)" title="'+escHtml(x.c.name)+" ("+x.n+'\xD7)">'+escHtml((x.c.name||"?")[0].toUpperCase())+"</span>"}).join("")+"</div>":""}let _avatarHoverEl=null;function _showAvatarHoverPreview(rec,anchorEl){if(!rec.image)return;_avatarHoverEl||(_avatarHoverEl=document.createElement("div"),_avatarHoverEl.className="lib-avatar-hover-preview",_avatarHoverEl.innerHTML="<img>",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 _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"?'<span class="lib-char-tier main">Haupt</span>':tier==="supporting"?'<span class="lib-char-tier support">Neben</span>':"",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?'<div class="lib-char-tags">'+tagList.map(function(t){return'<span class="cl-tag-chip"><span class="mdi mdi-tag-outline"></span>'+escHtml(t)+"</span>"}).join("")+"</div>":"",hasPhoto=!!rec.image,bannerStyle=hasPhoto?'style="background-image:linear-gradient(180deg, rgba(0,0,0,.05) 0%, rgba(0,0,0,.72) 100%), url(&quot;'+rec.image+'&quot;); background-size:cover; background-position:center;"':'style="--ch1:hsl('+hue+",52%,35%);--ch2:hsl("+hue2+',56%,26%)"',avatarInner=hasPhoto?'<span class="mdi mdi-camera-outline"></span>':escHtml((rec.name||"?")[0].toUpperCase()),stat=function(label,value,icon){return value?'<div class="lib-char-stat-row"><span class="lib-char-stat-icon mdi '+(icon||"")+'"></span><span class="lib-char-stat-label">'+escHtml(label)+'</span><span class="lib-char-stat-value">'+escHtml(value)+"</span></div>":""},metaChips=[];return bookLabel&&metaChips.push('<span class="lib-char-meta-chip"><span class="mdi mdi-book-open-page-variant-outline"></span> '+escHtml(bookLabel)+"</span>"),'<div class="lib-char-card" data-char-id="'+escHtml(rec.id)+'"><label class="lib-char-select" title="Select for bulk actions" onclick="event.stopPropagation()"><input type="checkbox" class="lib-char-select-cb" data-char-id="'+escHtml(rec.id)+'"></label><div class="lib-char-card-banner'+(hasPhoto?" has-photo":"")+'" '+bannerStyle+'><div class="lib-char-avatar'+(hasPhoto?" is-photo-edit":"")+'" data-char-id="'+escHtml(rec.id)+'" title="Bild hochladen">'+avatarInner+'</div><div class="lib-char-voice-top-row">'+(voiceId?'<button type="button" class="lib-char-voice-play" data-char-id="'+escHtml(rec.id)+'" title="Hear a sample line in this voice" aria-label="Hear a sample line in this voice"><span class="mdi mdi-play"></span></button>':"")+'<button type="button" class="lib-char-voice-pill'+(voiceId?" has-voice":"")+(voiceMissing?" voice-missing":"")+'" title="'+(voiceMissing?'Stimme "'+escHtml(voiceId)+'" wurde aus der Voice Library gel\xF6scht \u2014 bitte neu zuweisen':voiceId?"Stimme: "+escHtml(voiceId)+" \u2014 klicken zum \xC4ndern":"Stimme ausw\xE4hlen oder generieren")+'"><span class="mdi '+(voiceMissing?"mdi-alert-circle-outline":voiceId?"mdi-volume-high":"mdi-volume-off")+'"></span><span class="lib-char-voice-pill-text">'+(voiceId?escHtml(voiceId):"Keine Stimme zugewiesen")+'</span></button></div><div class="lib-char-banner-copy"><div class="lib-char-name">'+escHtml(rec.name)+'<span class="mdi '+genderIcon+'" style="font-size:12px;opacity:.55"></span>'+tierBadge+"</div>"+(roleLine?'<div class="lib-char-roleline">'+escHtml(roleLine)+"</div>":"")+(_libStr(sh.title)?'<div class="lib-char-subline">Titel: '+escHtml(_libStr(sh.title))+"</div>":"")+(_libStr(sh.aliases)?'<div class="lib-char-subline">aka '+escHtml(_libStr(sh.aliases))+"</div>":"")+'<div class="lib-char-meta-chips">'+metaChips.join("")+'</div></div></div><div class="lib-char-body"><div class="lib-char-facts">'+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")+"</div>"+_charAlignHtml(sh)+"</div></div>"}function _charsTableHtml(chars,sortMode,sortDir){const arrow=function(key){return sortMode===key?' <span class="mdi mdi-arrow-'+(sortDir==="desc"?"down":"up")+'" style="font-size:11px"></span>':""},th=function(key,label,title){return'<th class="lib-chars-tbl-sortable" data-sort-key="'+key+'"'+(title?' title="'+title+'"':"")+">"+label+arrow(key)+"</th>"},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):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),voiceLang=_charLangLabel(rec),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'<span class="lib-char-tier main">Haupt</span>':tier==="supporting"?'<span class="lib-char-tier support">Neben</span>':"",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?'<img src="'+rec.image+'" alt="'+escHtml(rec.name)+'" style="width:100%;height:100%;object-fit:cover;border-radius:50%">':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 ");return'<tr class="lib-char-card lib-chars-tbl-row" data-char-id="'+escHtml(rec.id)+'"><td class="lib-chars-tbl-cb" onclick="event.stopPropagation()"><input type="checkbox" class="lib-char-select-cb" data-char-id="'+escHtml(rec.id)+'"></td><td class="lib-chars-tbl-avatar-cell"><div class="lib-char-avatar lib-chars-tbl-avatar" data-char-id="'+escHtml(rec.id)+'" style="background:hsl('+hue+',55%,38%)" title="Bild hochladen">'+avatarInner+'</div></td><td class="lib-chars-tbl-name"><div class="lib-chars-tbl-name-main">'+tierBadge+escHtml(rec.name)+"</div>"+(tableMeta?'<div class="lib-chars-tbl-sub">'+escHtml(tableMeta)+"</div>":"")+(bookLabel?'<div class="lib-chars-tbl-book">'+escHtml(bookLabel)+"</div>":"")+"</td><td>"+(genderLabel?escHtml(genderLabel):'<span class="lib-chars-tbl-dash">\u2014</span>')+"</td><td>"+(ageLabel?escHtml(ageLabel):'<span class="lib-chars-tbl-dash">\u2014</span>')+"</td><td>"+(sh.line_count!=null?sh.line_count:'<span class="lib-chars-tbl-dash">\u2014</span>')+"</td><td>"+(voiceLang?escHtml(voiceLang):'<span class="lib-chars-tbl-dash">\u2014</span>')+"</td><td>"+(pct!=null?'<div class="lib-char-align-bar" title="'+pct+'/100"><div class="lib-char-align-dot" style="left:'+pct+'%"></div></div>':'<span class="lib-chars-tbl-dash">\u2014</span>')+'</td><td class="lib-chars-tbl-voice"><div class="lib-chars-tbl-voice-wrap"><div class="lib-chars-tbl-voice-row">'+(voiceId?'<span class="'+(voiceMissing?"lib-chars-tbl-voice-missing":"")+'"'+(voiceMissing?' title="Stimme wurde aus der Voice Library gel\xF6scht \u2014 bitte neu zuweisen"':"")+">"+(voiceMissing?'<span class="mdi mdi-alert-circle-outline"></span> ':"")+escHtml(voiceId)+"</span>":'<span class="lib-chars-tbl-dash">Keine Stimme</span>')+'</div><div class="lib-chars-tbl-voice-row">'+(voiceId?'<button type="button" class="lib-char-voice-play" data-char-id="'+escHtml(rec.id)+'" title="Hear a sample line in this voice" aria-label="Hear a sample line in this voice"><span class="mdi mdi-play"></span></button>':"")+'<button class="lib-char-pick-voice btn-sm">Auswahl</button><button class="lib-char-auto-voice btn-sm">Auto</button><button class="lib-char-redesign-voice btn-sm" title="Design a brand-new voice for this character, ignoring any existing match" aria-label="Design a new voice"><span class="mdi mdi-creation-outline"></span></button><button class="lib-char-gen-voice btn-sm" title="Show and edit the Voice Design Prompt before generating \u2014 opens the Design a Voice page pre-filled" aria-label="Edit voice design prompt"><span class="mdi mdi-text-box-edit-outline"></span></button>'+(voiceId?'<button class="lib-char-remove-voice btn-sm" title="Remove this voice \u2014 leaves the character unassigned" aria-label="Remove voice"><span class="mdi mdi-close"></span></button>':"")+'</div></div></td><td class="lib-chars-tbl-tags"><div class="lib-chars-tbl-tags-wrap">'+tagList.map(function(t){return'<span class="cl-tag-chip"><span class="mdi mdi-tag-outline"></span>'+escHtml(t)+"</span>"}).join("")+'</div></td><td><button class="lib-char-export btn-sm" title="Als SillyTavern-Karte exportieren"><span class="mdi mdi-export-variant"></span></button></td></tr>'}).join("");return'<div class="lib-chars-tbl-wrap"><table class="lib-chars-tbl"><colgroup><col style="width:28px"><col style="width:60px"><col style="width:300px"><col style="width:88px"><col style="width:80px"><col style="width:72px"><col style="width:100px"><col style="width:140px"><col style="width:230px"><col style="width:220px"><col style="width:200px"><col style="width:36px"></colgroup><thead><tr><th class="lib-chars-tbl-cb"><input type="checkbox" class="lib-chars-tbl-select-all-cb" title="Select all"></th><th></th>'+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("voice","Stimme")+"<th>Tags</th><th>Book / Script</th><th></th></tr></thead><tbody>"+rows+"</tbody></table></div>"}function _lcdSourcesHtml(sources){const list=Array.isArray(sources)?sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[];return list.length?'<div class="lcd-sources"><div class="lcd-section-label"><span class="mdi mdi-text-box-search-outline"></span> Quellen im Text</div><div class="lcd-source-list">'+list.map(function(s){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'<div class="lcd-source-item">'+(page||hint?'<div class="lcd-source-page">'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"</div>":"")+(s.quote?'<div class="lcd-source-quote">\u201E'+escHtml(_libStr(s.quote))+'"</div>':"")+"</div>"}).join("")+"</div></div>":""}function _lcdField(label,value,multiline){const v=_libStr(value);return v?'<div class="lcd-field"><div class="lcd-field-label">'+label+'</div><div class="lcd-field-value">'+escHtml(v)+"</div></div>":""}function _lcdSection(icon,label,fields){const body=fields.join("");return body?'<div class="lcd-section"><div class="lcd-section-label"><span class="mdi '+icon+'"></span> '+label+"</div>"+body+"</div>":""}function _lcdSectionFull(icon,label,fields){const body=fields.join("");return body?'<div class="lcd-section-full"><div class="lcd-section-label"><span class="mdi '+icon+'"></span> '+label+"</div>"+body+"</div>":""}function _lcdPromptBox(label,value,sheetKey){const has=!!(value&&String(value).trim());return'<details class="lcd-prompt-box"><summary>'+escHtml(label)+(has?"":' <span class="lcd-prompt-empty">\u2014 not generated yet</span>')+'</summary><div class="lcd-prompt-body"><div class="lcd-prompt-text lcd-field-editable" contenteditable="true" spellcheck="false" data-sheet-key="'+escHtml(sheetKey)+'">'+escHtml(value||"")+'</div><div class="lcd-prompt-actions"><button type="button" class="btn-secondary btn-sm lcd-prompt-copy" data-sheet-key="'+escHtml(sheetKey)+'"><span class="mdi mdi-content-copy"></span> Copy</button><button type="button" class="btn-secondary btn-sm lcd-gen-prompt" data-sheet-key="'+escHtml(sheetKey)+'"><span class="mdi mdi-creation"></span> '+(has?"Regenerate":"Generate")+"</button></div></div></details>"}function _lcdFieldEdit(label,value,sheetKey,sourceIdxs){const v=_libStr(value),links=(sourceIdxs||[]).map(function(idx){return'<a href="#" class="lcd-field-src-link" data-jump-src="'+idx+'" title="Quelle im Text anzeigen">'+(sourceIdxs.indexOf(idx)+1)+"</a>"}).join("");return'<div class="lcd-field">'+(label||links?'<div class="lcd-field-label">'+escHtml(label)+(links?' <span class="lcd-field-src-links">'+links+"</span>":"")+"</div>":"")+'<div class="lcd-field-value lcd-field-editable" contenteditable="true" data-sheet-key="'+escHtml(sheetKey)+'">'+escHtml(v)+"</div></div>"}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;opts=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||{},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='<div class="lcd-avatar-wrap">'+(rec.image?'<div class="lcd-avatar lcd-avatar-upload" title="Bild hochladen"><img src="'+rec.image+'" alt="'+escHtml(rec.name)+'"></div>':'<div class="lcd-avatar lcd-avatar-upload" style="background:hsl('+hue+',55%,38%)" title="Bild hochladen">'+escHtml((rec.name||"?")[0].toUpperCase())+"</div>")+'<div class="lcd-avatar-actions"><button class="lcd-avatar-upload-btn" type="button" title="Von Festplatte hochladen"><span class="mdi mdi-upload"></span></button><button class="lcd-avatar-online-btn" type="button" title="Im Internet suchen"><span class="mdi mdi-web"></span></button><button class="lcd-avatar-gen-btn" type="button" title="Mit KI generieren"><span class="mdi mdi-creation"></span></button></div></div>',conceptArtHtml='<div class="lcd-section-full lcd-conceptart"><div class="lcd-conceptart-headrow"><div class="lcd-section-label"><span class="mdi mdi-image-frame"></span> Konzeptbild</div><button type="button" class="lcd-conceptart-gen btn-secondary btn-sm" title="Konzeptbild generieren"><span class="mdi mdi-creation"></span> '+(sh.concept_art_image?"Neu generieren":"Generieren")+"</button></div>"+(sh.concept_art_image?'<div class="lcd-conceptart-img" title="Konzeptbild ansehen"><img src="'+sh.concept_art_image+'" alt="Concept art \u2014 '+escHtml(rec.name)+'"></div>':'<div class="lcd-conceptart-empty'+(_libStr(sh.concept_art_prompt).trim()?"":" is-noprompt")+'"><span class="mdi mdi-image-frame"></span><span>'+(_libStr(sh.concept_art_prompt).trim()?"Kein Konzeptbild":"Kein Konzeptbild-Prompt \u2014 erst unten bei Generation Prompts erzeugen")+"</span></div>")+"</div>",alignHtml=pct!=null?'<div class="lcd-align-section"><div class="lcd-section-label"><span class="mdi mdi-scale-balance"></span> Moralische Gesinnung</div><div class="lcd-align-bar-wrap"><span class="lcd-align-label">B\xF6se</span><input type="range" class="lcd-align-slider" min="0" max="100" value="'+pct+'"><span class="lcd-align-label">Gut</span><span class="lcd-align-slider-val">'+pct+'/100</span></div><div class="lcd-align-arc" style="color:'+arcInfo.color+'">'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"</div>"+(_libStr(sh.alignment)?'<div class="lcd-arc-note">'+escHtml(_libStr(sh.alignment))+"</div>":"")+"</div>":"",promptsHtml='<div class="lcd-section-full lcd-prompts-section"><div class="lcd-section-label"><span class="mdi mdi-script-text-outline"></span> Generation Prompts</div>'+_lcdPromptBox("Voice Design Prompt",sh.voice_design_prompt,"voice_design_prompt")+_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")+"</div>",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?'<div class="lcd-rels-dots">'+relHits.map(function(x){return'<span class="lcd-rel-dot" style="background:hsl('+_charHue(x.c.name)+',55%,38%)" title="'+escHtml(x.c.name)+" ("+x.n+'\xD7)">'+escHtml((x.c.name||"?")[0].toUpperCase())+"</span>"}).join("")+"</div>":"",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?'<div class="lcd-sources"><div class="lcd-section-label"><span class="mdi mdi-text-box-search-outline"></span> Quellen im Text<span class="lcd-sources-hint"> \xB7 Klicken zum Springen</span></div><div class="lcd-source-list">'+sourcesList.map(function(s,idx){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'<div class="lcd-source-item lcd-source-clickable" id="lcd-src-'+idx+'" data-page="'+(s.page!=null?s.page:"")+'">'+(page||hint?'<div class="lcd-source-page"><span class="mdi mdi-book-open-page-variant-outline"></span> '+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"</div>":"")+(s.quote?'<div class="lcd-source-quote">\u201E'+escHtml(_libStr(s.quote))+'"</div>':"")+"</div>"}).join("")+"</div></div>":"",sidebarHtml=(allChars||[]).slice().sort(function(a,b){return _charLineCount(b)-_charLineCount(a)}).map(function(c){const h=_charHue(c.name),count=_charLineCount(c);return'<div class="lib-cpg-sidebar-item'+(c.id===rec.id?" is-active":"")+'" data-char-id="'+escHtml(c.id)+'"><span class="lib-cpg-sidebar-dot" style="background:hsl('+h+',55%,38%)">'+escHtml((c.name||"?")[0].toUpperCase())+'</span><span class="lib-cpg-sidebar-name">'+escHtml(c.name)+"</span>"+(count?'<span class="lib-cpg-sidebar-count">'+count+"</span>":"")+"</div>"}).join("");container.innerHTML="";const pg=document.createElement("div");pg.className="lib-char-page",pg.style.gridColumn="1 / -1",pg.innerHTML='<div class="lib-cpg-main"><button class="lib-cpg-back"><span class="mdi mdi-arrow-left"></span> Alle Charaktere</button><div class="lcd-header" style="--lc1:hsl('+hue+",55%,35%);--lc2:hsl("+hue2+',58%,25%)">'+avatarHtml+'<div class="lcd-header-body"><div class="lcd-name" contenteditable="true" data-rec-key="name" spellcheck="false">'+escHtml(rec.name)+"</div>"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'<div class="lcd-aliases" contenteditable="true" data-sheet-key="full_name" spellcheck="false">'+escHtml(_libStr(sh.full_name))+"</div>":"")+(_libStr(sh.title)?'<div class="lcd-aliases" contenteditable="true" data-sheet-key="title" spellcheck="false">'+escHtml(_libStr(sh.title))+"</div>":"")+'<div class="lcd-aliases" contenteditable="true" data-sheet-key="aliases" spellcheck="false">'+escHtml(_libStr(sh.aliases))+'</div><div class="lcd-archetype" contenteditable="true" data-sheet-key="archetype" spellcheck="false">'+escHtml(_libStr(sh.archetype))+'</div><div class="lcd-tier-gender">'+(tierLabel?'<span class="lcd-tier">'+tierLabel+"</span>":"")+(gender?'<span class="lcd-gender"><span class="mdi '+genderIcon+'"></span> '+escHtml(gender)+"</span>":"")+'</div></div></div><div class="lcd-body">'+conceptArtHtml+'<div class="lcd-voice-top"><div class="lcd-section-label"><span class="mdi mdi-account-voice"></span> Stimme</div><span class="lcd-voice-label">'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):'<span class="lcd-voice-missing" title="Stimme wurde aus der Voice Library gel\xF6scht \u2014 bitte neu zuweisen"><span class="mdi mdi-alert-circle-outline"></span> '+escHtml(voiceId)+"</span>":'<span style="opacity:.5">Noch keine Stimme zugewiesen</span>')+'</span><button class="lcd-pick-voice">Ausw\xE4hlen</button><button class="lcd-auto-voice">Automatisch</button><button class="lcd-online-voice">Online suchen</button><button class="lcd-gen-voice">Design</button><button class="lcd-clone-voice">Klonen</button></div>'+alignHtml+'<div class="lcd-sections">'+_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+"</div>"+sourcesHtml+(rec.analysis?'<div class="lcd-analysis" contenteditable="true" data-rec-key="analysis">'+escHtml(String(rec.analysis))+"</div>":"")+'</div></div><div class="lib-cpg-sidebar"><div class="lib-cpg-sidebar-title">Charaktere \xB7 '+escHtml(rec.book||"")+"</div>"+sidebarHtml+"</div>",container.appendChild(pg),pg.querySelector(".lib-cpg-back").addEventListener("click",function(){goBack()});const _lcdUploadAvatar=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("Profilbild gespeichert","success"),rec.image=ev.target.result,_syncVoicePictureFromChar(rec);const av=pg.querySelector(".lcd-avatar-upload");av&&(av.innerHTML='<img src="'+ev.target.result+'" alt="'+escHtml(rec.name)+'">')},fr.readAsDataURL(file)},inp.click()};pg.querySelector(".lcd-avatar-upload").addEventListener("click",_lcdUploadAvatar),(_a2=pg.querySelector(".lcd-avatar-upload-btn"))==null||_a2.addEventListener("click",function(e){e.stopPropagation(),_lcdUploadAvatar()}),(_b2=pg.querySelector(".lcd-avatar-online-btn"))==null||_b2.addEventListener("click",function(e){e.stopPropagation();const q=[rec.name,rec.book,sh.archetype,"character art"].filter(Boolean).join(" ");window.open("https://www.google.com/search?tbm=isch&q="+encodeURIComponent(q),"_blank","noopener")}),(_c2=pg.querySelector(".lcd-avatar-gen-btn"))==null||_c2.addEventListener("click",async function(e){e.stopPropagation();const btn=this,bookProfile=typeof _getBookProfile=="function"?await _getBookProfile(rec.book):{},prompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh,bookProfile):"");if(!prompt){toast("No image prompt to work from \u2014 generate the Character Image Prompt below first","error");return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span>';try{const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({prompt})});if(!r.ok)throw new Error((await r.json().catch(function(){return{}})).detail||r.statusText);const d=await r.json();typeof clSetImage=="function"&&await clSetImage(rec.id,d.image),rec.image=d.image,_syncVoicePictureFromChar(rec),toast("Profile picture generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Image generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}}),(_d2=pg.querySelector(".lcd-conceptart-gen"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation();const btn=this;if(!_libStr(sh.concept_art_prompt).trim()){toast("No Concept Art Prompt yet \u2014 generate that first in Generation Prompts below","error");return}const orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span>';try{await _charAutoGenerateConceptArt(rec),toast("Concept art generated","success"),_charDetailPage(rec,allChars,opts)}catch(err){toast("Concept art generation failed: "+(err.message||err),"error"),btn.disabled=!1,btn.innerHTML=orig}}),(_e2=pg.querySelector(".lcd-conceptart-img"))==null||_e2.addEventListener("click",function(){var _a3;(_a3=document.getElementById("conceptart-lightbox"))==null||_a3.remove();const ov=document.createElement("div");ov.id="conceptart-lightbox",ov.className="audiobook-overlay",ov.innerHTML='<div class="audiobook-box conceptart-lightbox-box"><div class="audiobook-title"><span class="mdi mdi-image-frame"></span> '+escHtml(rec.name)+' \u2014 Konzeptbild<span style="flex:1"></span><button type="button" class="btn-secondary btn-sm" id="calb-close">Close</button></div><img class="conceptart-lightbox-img" src="'+sh.concept_art_image+'" alt="Concept art \u2014 '+escHtml(rec.name)+'"></div>',document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector("#calb-close").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()})}),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)})}),(_f2=pg.querySelector(".lcd-pick-voice"))==null||_f2.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)})}),(_g2=pg.querySelector(".lcd-auto-voice"))==null||_g2.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)}),(_h2=pg.querySelector(".lcd-online-voice"))==null||_h2.addEventListener("click",function(){_charSearchOnline(rec)}),(_i2=pg.querySelector(".lcd-gen-voice"))==null||_i2.addEventListener("click",function(){_charDesignVoiceInline(rec)}),(_j2=pg.querySelector(".lcd-clone-voice"))==null||_j2.addEventListener("click",function(){_charCloneVoice(rec)}),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-gen-prompt").forEach(function(btn){btn.addEventListener("click",async function(e){e.preventDefault();const key=btn.dataset.sheetKey,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="mdi mdi-loading mdi-spin"></span> 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||"",sheet:sh2,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.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?'<div class="lcd-avatar"><img src="'+rec.image+'" alt="'+escHtml(rec.name)+'"></div>':'<div class="lcd-avatar" style="background:hsl('+hue+',55%,38%)">'+escHtml((rec.name||"?")[0].toUpperCase())+"</div>",alignHtml=pct!=null?'<div class="lcd-align-section"><div class="lcd-section-label"><span class="mdi mdi-scale-balance"></span> Moralische Gesinnung</div><div class="lcd-align-bar-wrap"><span class="lcd-align-label">B\xF6se</span><div class="lcd-align-bar"><div class="lcd-align-dot" style="left:'+pct+'%"></div></div><span class="lcd-align-label">Gut</span></div><div class="lcd-align-arc" style="color:'+arcInfo.color+'">'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"</div>"+(_libStr(sh.arc_note)?'<div class="lcd-arc-note">'+escHtml(_libStr(sh.arc_note))+"</div>":"")+(_libStr(sh.alignment)?'<div class="lcd-arc-note" style="margin-top:6px">'+escHtml(_libStr(sh.alignment))+"</div>":"")+"</div>":"",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?'<div class="lcd-rels-dots">'+relHits.map(function(x){return'<span class="lcd-rel-dot" style="background:hsl('+_charHue(x.c.name)+',55%,38%)" title="'+escHtml(x.c.name)+" ("+x.n+'\xD7)">'+escHtml((x.c.name||"?")[0].toUpperCase())+"</span>"}).join("")+"</div>":"",ov=document.createElement("div");ov.className="lib-char-detail-ov",ov.innerHTML='<div class="lib-char-detail-box"><div class="lcd-header" style="--lc1:hsl('+hue+",55%,35%);--lc2:hsl("+(hue+40)%360+',58%,25%)">'+avatarHtml+'<div class="lcd-header-body"><div class="lcd-name">'+escHtml(rec.name)+"</div>"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'<div class="lcd-aliases">'+escHtml(_libStr(sh.full_name))+"</div>":"")+(_libStr(sh.title)?'<div class="lcd-aliases">'+escHtml(_libStr(sh.title))+"</div>":"")+(_libStr(sh.aliases)?'<div class="lcd-aliases">auch bekannt als '+escHtml(_libStr(sh.aliases))+"</div>":"")+(_libStr(sh.archetype)?'<div class="lcd-archetype">'+escHtml(_libStr(sh.archetype))+"</div>":"")+'<div class="lcd-tier-gender">'+(tierLabel?'<span class="lcd-tier">'+tierLabel+"</span>":"")+(gender?'<span class="lcd-gender"><span class="mdi '+genderIcon+'"></span> '+escHtml(gender)+"</span>":"")+'</div></div><div class="lcd-header-btns"><button class="lcd-edit-btn"><span class="mdi mdi-pencil-outline"></span> Bearbeiten</button><button class="lcd-close-btn"><span class="mdi mdi-close"></span></button></div></div><div class="lcd-body"><div class="lcd-voice-top"><div class="lcd-section-label"><span class="mdi mdi-account-voice"></span> Stimme</div><span class="lcd-voice-label">'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):'<span class="lcd-voice-missing" title="Stimme wurde aus der Voice Library gel\xF6scht \u2014 bitte neu zuweisen"><span class="mdi mdi-alert-circle-outline"></span> '+escHtml(voiceId)+"</span>":'<span style="opacity:.5">Noch keine Stimme zugewiesen</span>')+'</span><button class="lcd-pick-voice">Ausw\xE4hlen</button><button class="lcd-auto-voice">Automatisch</button><button class="lcd-online-voice">Online suchen</button><button class="lcd-gen-voice">Generieren</button></div>'+alignHtml+'<div class="lcd-sections">'+_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)])+"</div>"+_lcdSourcesHtml(sh.sources)+(rec.analysis?'<div class="lcd-analysis">'+escHtml(String(rec.analysis))+"</div>":"")+"</div></div>",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='<div class="audiobook-box avatar-lightbox-box"><div class="audiobook-title"><span class="mdi mdi-image-outline"></span> '+escHtml(rec.name)+' \u2014 Profilbild<span style="flex:1"></span><button type="button" class="btn-secondary btn-sm" id="alb-close">Close</button></div><div class="alb-body"><div class="alb-preview">'+(rec.image?'<img id="alb-preview-img" src="'+rec.image+'" alt="'+escHtml(rec.name)+'">':'<div class="alb-preview-empty"><span class="mdi mdi-account-outline"></span></div>')+'</div><div class="alb-actions"><div class="alb-section"><div class="alb-section-label"><span class="mdi mdi-upload"></span> Von Festplatte hochladen</div><input type="file" id="alb-file-input" accept="image/*"></div><div class="alb-section"><div class="alb-section-label"><span class="mdi mdi-link-variant"></span> Bild-URL</div><div class="alb-url-row"><input type="text" id="alb-url-input" placeholder="https://\u2026"><button type="button" class="btn-secondary btn-sm" id="alb-url-btn">Herunterladen</button></div></div><div class="alb-section"><div class="alb-section-label"><span class="mdi mdi-creation"></span> Mit KI (neu) generieren</div><textarea id="alb-prompt" rows="4" spellcheck="false" placeholder="Character image prompt\u2026">'+escHtml(currentPrompt)+'</textarea><div class="alb-gen-row"><select id="alb-provider"><option value="">(Active Provider)</option><option value="openai">OpenAI</option><option value="google">Google</option><option value="openrouter">OpenRouter</option><option value="pollinations">Pollinations.ai (free)</option><option value="comfyui">Local ComfyUI</option></select><button type="button" class="btn-primary btn-sm" id="alb-gen-btn"><span class="mdi mdi-creation"></span> Generieren</button></div></div></div></div><div id="alb-status" class="llm-active-status"></div></div>',document.body.appendChild(ov),ov.addEventListener("click",function(e){e.target===ov&&ov.remove()}),ov.querySelector("#alb-close").addEventListener("click",function(){ov.remove()});const setPreview=function(src){ov.querySelector(".alb-preview").innerHTML='<img id="alb-preview-img" src="'+src+'" alt="'+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='<img src="'+dataUri+'" alt="'+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='<span class="mdi mdi-loading mdi-spin"></span> 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='<div class="lib-vp-search"><input type="search" placeholder="Search voices\u2026" class="lib-vp-input" autocomplete="off"></div><div class="lib-vp-quick"><button type="button" class="lib-vp-design-btn"><span class="mdi mdi-creation"></span> Neue Stimme designen</button></div><div class="lib-vp-list"></div>',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'<div class="lib-vp-item'+(v.id===rec.voice?" selected":"")+'" data-vid="'+escHtml(v.id)+'">'+escHtml(v.id||v.name||"")+(v.gender?' <span style="opacity:.5;font-size:10px">\xB7 '+escHtml(v.gender)+"</span>":"")+"</div>"}).join("")+(list.length===0?'<div style="padding:12px;opacity:.5;font-size:12px">No voices found</div>':""),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},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<str.length;i++)h=h*31+str.charCodeAt(i)>>>0;return pool[h%pool.length]}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){_getBookProfile(book).then(function(profile){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='<div class="audiobook-box" style="max-width:440px;"><div class="audiobook-title"><span class="mdi mdi-book-cog-outline"></span> Book context \u2014 '+escHtml(book)+`</div><p class="card-subtitle" style="margin:8px 0 16px;">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.</p><div style="display:flex; flex-direction:column; gap:10px; margin-bottom:16px;"><label>Genre<input type="text" id="bctx-genre" placeholder="e.g. High fantasy" value="`+escHtml(profile.genre||"")+'"></label><label>Setting<input type="text" id="bctx-setting" placeholder="e.g. Like Lord of the Rings" value="'+escHtml(profile.setting||"")+'"></label><label>Era<input type="text" id="bctx-era" placeholder="e.g. Medieval times" value="'+escHtml(profile.era||"")+'"></label><label>Language<input type="text" id="bctx-lang" placeholder="e.g. German" value="'+escHtml(profile.language||"")+'"></label></div><div style="display:flex; gap:8px; justify-content:flex-end;"><button type="button" class="btn-secondary btn-sm" id="bctx-cancel">Cancel</button><button type="button" class="btn-primary btn-sm" id="bctx-save">Save</button></div></div>',document.body.appendChild(ov);const close=function(){ov.remove()};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{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()}catch(e){toast("Failed to save: "+(e.message||e),"error"),btn.disabled=!1}})})}function _confirmVoiceReuse(rec,reuse){return new Promise(function(resolve){const ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='<div class="audiobook-box" style="max-width:440px;"><div class="audiobook-title"><span class="mdi mdi-account-voice"></span> Existing voice found for '+escHtml(rec.name)+'</div><p class="card-subtitle" style="margin:8px 0 16px;">"'+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?</p><div style="margin-bottom:16px;"><button type="button" class="btn-secondary btn-sm" id="cvr-play"><span class="mdi mdi-play"></span> Play sample</button></div><div style="display:flex; gap:8px; justify-content:flex-end;"><button type="button" class="btn-secondary btn-sm" id="cvr-cancel">Cancel</button><button type="button" class="btn-secondary btn-sm" id="cvr-use">Use this voice</button><button type="button" class="btn-primary btn-sm" id="cvr-new">Design a new one</button></div></div>',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=savedPrompt||_buildVoicePrompt(rec,null,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){const sh=rec.sheet||{},lang=_charLang(rec),instruct=_libStr(sh.voice_design_prompt).trim()||_buildVoicePrompt(rec,null,lang),ov=document.createElement("div");ov.className="audiobook-overlay",ov.innerHTML='<div class="audiobook-box" style="max-width:560px;"><div class="audiobook-title"><span class="mdi mdi-creation"></span> Voice design prompt \u2014 '+escHtml(rec.name)+'</div><p class="card-subtitle" style="margin:8px 0 12px;">Edit the description, then generate a new voice from it. This replaces '+(rec.voice?"the current voice":"this character\u2019s voice")+'.</p><textarea id="cdi-instruct" class="input" style="width:100%; min-height:160px; resize:vertical; font-family:inherit;">'+escHtml(instruct)+'</textarea><div style="display:flex; gap:8px; justify-content:flex-end; margin-top:14px;"><button type="button" class="btn-secondary btn-sm" id="cdi-cancel">Cancel</button><button type="button" class="btn-primary btn-sm" id="cdi-generate"><span class="mdi mdi-creation"></span> Generate voice</button></div></div>',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),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}btn.disabled=!0;const icon=btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-loading mdi-spin");try{await _charAutoDesignVoice(rec,!0,text),_schedulePendingTtsRestart(),close(),toast("New voice designed for "+rec.name,"success"),typeof libraryRenderCharacters=="function"&&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||_buildVoicePrompt(rec,await _getBookProfile(rec.book),langName);if(!instruct.trim())throw new Error("No character description to design a voice from yet");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,note:"Voice Design: "+instruct.slice(0,240),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};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='<img src="'+d.image+'" alt="'+escHtml(rec.name)+'">'}),_syncVoicePictureFromChar(rec),d.image}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};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}window._charSearchOnline=_charSearchOnline,window._charDesignVoice=_charDesignVoice,window._charAutoDesignVoice=_charAutoDesignVoice,window._charAutoGenerateImage=_charAutoGenerateImage,window._charAutoGenerateConceptArt=_charAutoGenerateConceptArt,window.libraryRenderCharacters=libraryRenderCharacters;let _stuActive=1;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='<div class="card" style="padding:24px; min-height:220px; display:flex; flex-direction:column; gap:12px; justify-content:center; align-items:flex-start;"><div class="audiobook-title"><span class="mdi mdi-account-details-outline"></span> Character sheets</div><p class="card-subtitle" style="margin:0;">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.</p><button class="btn-primary btn-sm" id="stu-goto-cast-menu"><span class="mdi mdi-creation"></span> Generate character sheets</button></div>';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)})});let _stuCastFootObserver=null;function _stuRelocateCastFoot(){if(_stuTryRelocateCastFoot(),_stuCastFootObserver)return;const slot=document.getElementById("stu-cast-slot");slot&&(_stuCastFootObserver=new MutationObserver(function(){_stuTryRelocateCastFoot()}),_stuCastFootObserver.observe(slot,{childList:!0,subtree:!0}))}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"):null,alreadyRelocated=tabs.querySelector("#ab-cv-foot");if(!freshFoot&&!alreadyRelocated){document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!1});return}if(!freshFoot||freshFoot.parentElement===tabs)return;tabs.querySelectorAll("#ab-cv-foot").forEach(function(stale){stale.remove()}),freshFoot.style.borderTop="none",freshFoot.style.padding="0",freshFoot.style.justifyContent="flex-start",tabs.appendChild(freshFoot),document.querySelectorAll("#stu-cast-inner-tabs > .stu-inner-tab").forEach(function(t){t.hidden=!0});const openReh=freshFoot.querySelector("#ab-cv-open-reh");openReh&&(openReh.hidden=!0)}function _stuRestoreCastFoot(){_stuCastFootObserver&&(_stuCastFootObserver.disconnect(),_stuCastFootObserver=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;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))})}),(_uc=document.getElementById("stu-phase-prev"))==null||_uc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_vc=document.getElementById("stu-phase-next"))==null||_vc.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){_stuInstallNavGuardOnce(),_stuIsActive=!0,showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow;