diff --git a/CHANGELOG.md b/CHANGELOG.md index 7eb4371..f198131 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.18.11] — 2026-07-29 + +### Fixed +- **Designed voices now store their full design prompt.** For a designed voice the prompt IS the voice's identity — the engine reproduces it from that text alone — but the only copy saved was the `note` display summary, which is deliberately clipped to 240 characters (43 of 73 voices were cut off mid-sentence). The complete prompt is now saved in its own field, so the TTS engine can register the voice from the whole description rather than a truncated one. Existing voices keep working from the clipped copy — it still carries gender, accent and timbre — and pick up the full text the next time they are redesigned. + +### Note +- This release pairs with an engine-side fix (in the `tts-dgx-spark-faster-qwen3-tts` repo) for two bugs that made custom voices unusable: stale speaker embeddings causing cloned voices to ignore the requested text entirely, and designed voices never being registered with the Voice Design engine, which silently substituted a bundled British preset. See that repo's history for details. + ## [1.18.10] — 2026-07-29 ### Fixed diff --git a/VERSION b/VERSION index 0150af1..6961fed 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.18.10 +1.18.11 diff --git a/routes/library.py b/routes/library.py index aad4e6c..2fac10a 100644 --- a/routes/library.py +++ b/routes/library.py @@ -382,7 +382,7 @@ async def update_voice_meta(request: Request): meta["enabled"] = enabled for field in ("note", "rating", "flag", "gender", "loudness", "persona", "origin", "group", - "name", "tag", "avatar"): + "name", "tag", "avatar", "voice_design_prompt"): if field in data: meta[field] = data[field] if "transcript" in data: diff --git a/static/dist/main.min.js b/static/dist/main.min.js index 3e0ca2f..d2405e2 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -422,7 +422,7 @@ Mia:Only if you promise not to spill coffee on my notes again... though I guess - `,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)}).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=' Transcribing\u2026',status2&&(status2.className="clone-tr-status working",status2.innerHTML=' 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=' Transcribed'),toast("Transcription complete","success"),(_b2=window._cloneScheduleAutoSave)==null||_b2.call(window)}catch(e){status2&&(status2.className="clone-tr-status error",status2.innerHTML=` 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='

Table View Mode
Click a row to exit table view and edit.

')}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=''+langs.map(x=>``).join(""),sexSel.innerHTML=''+sexes.map(x=>``).join(""),typeSel.innerHTML=''+types.map(x=>``).join(""),tagSel&&(tagSel.innerHTML=''+tags.map(x=>``).join("")),groupSel&&(groupSel.innerHTML=''+groups.map(x=>``).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&&rString(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;ch0?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])=>`
${value}${label}
`).join("");return}if(state==="error"){el.innerHTML='
FailedLibrary load
';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`
${escHtml(item.value)}${escHtml(item.label)}
`}).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='
Failed to load voices: '+(e.message||String(e))+"
"),$("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=>`${escHtml(x)}`).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;ix.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=`
Verifying voices
+ `,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=' Transcribing\u2026',status2&&(status2.className="clone-tr-status working",status2.innerHTML=' 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=' Transcribed'),toast("Transcription complete","success"),(_b2=window._cloneScheduleAutoSave)==null||_b2.call(window)}catch(e){status2&&(status2.className="clone-tr-status error",status2.innerHTML=` 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='

Table View Mode
Click a row to exit table view and edit.

')}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=''+langs.map(x=>``).join(""),sexSel.innerHTML=''+sexes.map(x=>``).join(""),typeSel.innerHTML=''+types.map(x=>``).join(""),tagSel&&(tagSel.innerHTML=''+tags.map(x=>``).join("")),groupSel&&(groupSel.innerHTML=''+groups.map(x=>``).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&&rString(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;ch0?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])=>`
${value}${label}
`).join("");return}if(state==="error"){el.innerHTML='
FailedLibrary load
';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`
${escHtml(item.value)}${escHtml(item.label)}
`}).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='
Failed to load voices: '+(e.message||String(e))+"
"),$("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=>`${escHtml(x)}`).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;ix.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=`
Verifying voices
0 / ${ids.length}
`,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=`
@@ -1781,4 +1781,4 @@ Respond with STRICT JSON only: `)[0].slice(0,60)||"",backstory:data.description||data.char_persona||"",mannerisms:data.personality||"",arc_note:data.scenario||"",voice_pattern:data.mes_example||data.example_dialogue||data.first_mes||data.char_greeting||"",gender:ext.gender||stGuessGender((data.description||"")+" "+(data.personality||"")),note:data.creator_notes||"",tags,voice:ext.tts_voice||null,tier:"supporting",st_card:data}}function stFromRecord(rec){const sh=rec.sheet||{},prev=sh.st_card||{},descParts=[sh.physical,sh.clothing,sh.backstory,sh.alignment].map(x=>String(x||"").trim()).filter(Boolean),persona=[sh.archetype,sh.mannerisms,sh.motivation,sh.fears].map(x=>String(x||"").trim()).filter(Boolean).join(` `);return{spec:"chara_card_v2",spec_version:"2.0",data:{name:rec.name,description:prev.description||descParts.join(` -`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'
').join("")}async function libraryRenderBooks(){const list=document.getElementById("lib-books-list");if(!list)return;list.innerHTML=_libSkeleton(4);let all=[];try{const r=await fetch(LIB_READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='

No books yet.

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

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

No theater plays yet.

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

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const speakers=Object.keys(rec.cast||{}),total=typeof parseScript=="function"?parseScript(rec.script||"").filter(function(l){return l.type==="dialog"}).length:0,pct=total?Math.round((rec.lineIndex||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"\u2014",cov=libBookCover(rec.title||"Untitled"),avatars=speakers.slice(0,5).map(function(sp){return''+(sp[0]||"?").toUpperCase()+""}).join("");return'
'+escHtml(rec.title||"Untitled")+'
'+avatars+"
"+total+" lines \xB7 "+speakers.length+' cast
'+pct+"% \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-play").forEach(function(el){const id=parseInt(el.dataset.id,10),title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||openPlayInRehearser(id)});const ra=el.querySelector(".lib-act-readaloud");ra&&ra.addEventListener("click",function(e){e.stopPropagation(),productionOpenInReader(title)});const del=el.querySelector(".lib-act-del-play");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete rehearsal?","This cannot be undone.",async function(){try{typeof rehDbDelete=="function"&&await rehDbDelete(id),toast("Rehearsal deleted","success"),libraryRenderPlays()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function openPlayInRehearser(id){try{const rec=typeof rehDbGetById=="function"?await rehDbGetById(id):null;rec&&typeof loadRecord=="function"?loadRecord(rec):toast("Rehearsal not found","error")}catch{toast("Could not open rehearsal","error")}}async function productionOpenInRehearser(title){const key=prodKey(title);try{const match=(typeof rehDbGetAll=="function"?await rehDbGetAll():[]).find(function(p){return prodKey(p.title)===key});if(match){openPlayInRehearser(match.id);return}}catch{}let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&book.kind!=="pdf")try{const sr=await fetch(LIB_READER_API+"/"+book.id+"/source"),text=sr.ok?await sr.text():"";if(text&&typeof audiobookOpenInRehearser=="function"){audiobookOpenInRehearser(text,title,[]);return}}catch{}if(book&&book.kind==="pdf"){typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(book.id),toast('Open this PDF book, then use "Cast as audiobook" to build a rehearsal',"info");return}toast("No source to rehearse for this title yet","error")}async function productionOpenInReader(title){const key=prodKey(title);let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&typeof readerOpenLibraryDoc=="function"){readerOpenLibraryDoc(book.id);return}typeof navTo=="function"&&navTo("s-reader"),toast("No audiobook for this title yet \u2014 import its source in Read Aloud","info")}async function castForProduction(title){const out={};if(typeof clGetAllByTagOrBook!="function")return out;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{recs=[]}return recs.forEach(function(r){const name=(r.name||"").trim();if(!name)return;const voice=r.voice&&r.voice.id?r.voice.id:typeof r.voice=="string"?r.voice:"";out[name.toLowerCase()]={name,voice:voice||"",gender:r.sheet&&r.sheet.gender||"",soul:r.sheet&&(r.sheet.voice_pattern||r.sheet.motivation)||"",tags:r.tags||""}}),out}window.castForProduction=castForProduction;async function castWriteBack(title,castMap){if(!title||!castMap||typeof clGetAllByTagOrBook!="function"||typeof clPut!="function")return;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{return}if(!recs.length)return;const byName={};recs.forEach(function(r){byName[(r.name||"").trim().toLowerCase()]=r});let n=0;for(const sp of Object.keys(castMap)){if(String(sp).includes("NARRATOR"))continue;const voice=(castMap[sp]||{}).voice;if(!voice||voice==="me")continue;const rec=byName[String(sp).trim().toLowerCase()];if(!(!rec||(rec.voice&&rec.voice.id?rec.voice.id:typeof rec.voice=="string"?rec.voice:"")===voice)){rec.voice={id:voice},rec.updated=new Date;try{await clPut(rec),n++}catch{}}}return n}window.castWriteBack=castWriteBack;function libBookCover(title){let h=0;const s=String(title||"Untitled");for(let i=0;i'+heading+'
'+sub+'
',o.addEventListener("click",function(e){e.stopPropagation()}),o.querySelector("[data-lib-cancel]").addEventListener("click",function(e){e.stopPropagation(),o.remove()}),o.querySelector("[data-lib-ok]").addEventListener("click",async function(e){e.stopPropagation(),o.innerHTML='',await onConfirm()}),cardEl.appendChild(o)}window.libraryRenderBooks=libraryRenderBooks,window.libraryRenderPlays=libraryRenderPlays,window.productionOpenInRehearser=productionOpenInRehearser,window.productionOpenInReader=productionOpenInReader,window.prodKey=prodKey;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='
Loading characters\u2026
';const byId=new Map(all.map(function(rec){return[rec.id,rec]}));if(!all.length){container.innerHTML='

No characters yet.

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

',container.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop);return}const byBook={};all.forEach(function(rec){const bk=rec.book||"Unsorted";byBook[bk]||(byBook[bk]=[]),byBook[bk].push(rec)}),Object.keys(byBook).forEach(function(bk){if(!byBook[bk].some(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"})){const narrRec={id:clKey(bk,"Narrator"),book:bk,name:"Narrator",tags:bk,voice:null,image:null,sheet:{}};byId.set(narrRec.id,narrRec),byBook[bk].unshift(narrRec)}}),container.innerHTML="";const viewMode=localStorage.getItem("ttsvc_libchars_view")==="table"?"table":"cards";let returnToReader=!1;try{returnToReader=sessionStorage.getItem("ttsvc_cast_return")==="reader"}catch{}const SORT_OPTIONS=[["tier","Rolle (Haupt zuerst)"],["alpha","Alphabet"],["lines","Anzahl Zeilen"],["gender","Geschlecht"],["voice","Stimme zugewiesen"]],sortMode=SORT_OPTIONS.some(function(o){return o[0]===localStorage.getItem("ttsvc_libchars_sort")})?localStorage.getItem("ttsvc_libchars_sort"):"tier",bar=document.createElement("div");bar.className="lib-chars-toolbar",bar.innerHTML=(returnToReader?'':"")+'
',bar.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),(_a2=bar.querySelector("#lib-chars-back-reader"))==null||_a2.addEventListener("click",function(){try{sessionStorage.removeItem("ttsvc_cast_return")}catch{}typeof navTo=="function"&&navTo("s-reader")}),bar.querySelector("#lib-chars-sort-sel").addEventListener("change",function(){localStorage.setItem("ttsvc_libchars_sort",this.value),libraryRenderCharacters()}),bar.querySelectorAll(".lib-chars-view-toggle button").forEach(function(btn){btn.addEventListener("click",function(){localStorage.setItem("ttsvc_libchars_view",btn.dataset.view),libraryRenderCharacters()})}),container.appendChild(bar);const _charSortCmp={tier:function(a,b){var _a3,_b2,_c2,_d2;const tierOrder={main:0,supporting:1,minor:2},ta=(_b2=tierOrder[String(((_a3=a.sheet)==null?void 0:_a3.tier)||"minor").toLowerCase()])!=null?_b2:2,tb=(_d2=tierOrder[String(((_c2=b.sheet)==null?void 0:_c2.tier)||"minor").toLowerCase()])!=null?_d2:2;return ta-tb||(a.name||"").localeCompare(b.name||"")},alpha:function(a,b){return(a.name||"").localeCompare(b.name||"")},lines:function(a,b){var _a3,_b2;return(((_a3=b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)||(a.name||"").localeCompare(b.name||"")},gender:function(a,b){var _a3,_b2;const ga=String(((_a3=a.sheet)==null?void 0:_a3.gender)||"zzz"),gb=String(((_b2=b.sheet)==null?void 0:_b2.gender)||"zzz");return ga.localeCompare(gb)||(a.name||"").localeCompare(b.name||"")},voice:function(a,b){return(b.voice?1:0)-(a.voice?1:0)||(a.name||"").localeCompare(b.name||"")},age:function(a,b){return _charAgeSortVal(a.sheet)-_charAgeSortVal(b.sheet)||(a.name||"").localeCompare(b.name||"")},language:function(a,b){return _charLangLabel(a).localeCompare(_charLangLabel(b))||(a.name||"").localeCompare(b.name||"")},align:function(a,b){var _a3,_b2,_c2,_d2;return((_b2=(_a3=b.sheet)==null?void 0:_a3.moral_alignment_score)!=null?_b2:-1)-((_d2=(_c2=a.sheet)==null?void 0:_c2.moral_alignment_score)!=null?_d2:-1)||(a.name||"").localeCompare(b.name||"")}},sortDir=localStorage.getItem("ttsvc_libchars_sort_dir")==="desc"?"desc":"asc",productions=document.createDocumentFragment();if(Object.keys(byBook).sort().forEach(function(book){const chars=byBook[book].sort(_charSortCmp[sortMode]||_charSortCmp.tier);sortDir==="desc"&&chars.reverse();const narrIdx=chars.findIndex(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"});narrIdx>0&&chars.unshift(chars.splice(narrIdx,1)[0]);const cov=libBookCover(book),prod=document.createElement("div");prod.className="lib-chars-production",prod.dataset.book=book;const collapseKey="ttsvc_libchars_collapsed::"+book;let isCollapsed=localStorage.getItem(collapseKey)==="1";window._libCharsScrollToBook&&(isCollapsed=book!==window._libCharsScrollToBook),isCollapsed&&prod.classList.add("lib-chars-production-collapsed"),prod.innerHTML='
'+escHtml(book)+'
`+(viewMode==="table"?_charsTableHtml(chars,sortMode,sortDir):'
'+chars.map(function(rec){return _charCardHtml(rec,chars)}).join("")+"
")+"
",prod.querySelector(".lib-chars-prod-collapse-btn").addEventListener("click",function(e){e.stopPropagation();const collapsed=prod.classList.toggle("lib-chars-production-collapsed");localStorage.setItem(collapseKey,collapsed?"1":"0")}),prod.querySelector(".lib-chars-prod-head").addEventListener("click",function(e){e.target.closest("button, select, input, a")||prod.querySelector(".lib-chars-prod-collapse-btn").click()}),prod.querySelector(".lib-chars-bookctx-btn").addEventListener("click",function(){_editBookProfile(book)}),prod.querySelector(".lib-chars-casting-btn").addEventListener("click",function(){typeof navTo=="function"&&navTo("s-reader")}),prod.querySelector(".lib-chars-cast-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book),toast("Open the book in Read Aloud then click Cast Characters","info")}),prod.querySelector(".lib-chars-reh-btn").addEventListener("click",function(){typeof productionOpenInRehearser=="function"&&productionOpenInRehearser(book)}),prod.querySelector(".lib-chars-read-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book)}),prod.querySelector(".lib-chars-imp-btn").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog(book,function(){libraryRenderCharacters()})}),prod.querySelectorAll("[data-sort-key]").forEach(function(th){th.addEventListener("click",function(){const key=th.dataset.sortKey,nextDir=sortMode===key&&sortDir==="asc"?"desc":"asc";localStorage.setItem("ttsvc_libchars_sort",key),localStorage.setItem("ttsvc_libchars_sort_dir",nextDir),libraryRenderCharacters()})});const 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&&checkedN0&&boxes.every(function(cb){return cb.checked});boxes.forEach(function(cb){cb.checked=!allChecked}),refreshBulkBtn()}),tblSelectAllCb&&tblSelectAllCb.addEventListener("change",function(){[...prod.querySelectorAll(".lib-char-select-cb")].forEach(function(cb){cb.checked=tblSelectAllCb.checked}),refreshBulkBtn()}),syncTblSelectAllCb();const runBulk=async function(btn,ids,verb,fn){btn.disabled=!0;const orig=btn.innerHTML;let done=0,failed=0,lastErrMsg="",repeatErrMsg="",repeatCount=0,aborted=!1;for(const id of ids){const rec=byId.get(id);if(rec){btn.innerHTML=' '+verb+" "+(done+failed+1)+" / "+ids.length+"\u2026";try{await fn(rec),done++,repeatCount=0}catch(e){if(failed++,lastErrMsg=e&&e.message?e.message:String(e),console.error("[bulk "+verb+"]",rec.name,e),lastErrMsg===repeatErrMsg?repeatCount++:(repeatErrMsg=lastErrMsg,repeatCount=1),repeatCount>=3){aborted=!0;break}}}}btn.innerHTML=orig;const remaining=ids.length-done-failed,suffix=failed?` (${failed} failed${aborted&&remaining?`, ${remaining} skipped`:""}${lastErrMsg?": "+lastErrMsg.slice(0,200):""})`:"";toast(`${verb} finished for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),await _flushPendingTtsRestart(),typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary({refresh:!0}).catch(()=>{}),libraryRenderCharacters()};bulkBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(bulkBtn,ids,"Assigning",_autoAssignVoice)}),designBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(designBtn,ids,"Designing",_charAutoDesignVoice)}),imageBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});if(!ids.length)return;const provider=imageProviderSel?imageProviderSel.value:"";runBulk(imageBtn,ids,"Generating images",function(rec){return _charAutoGenerateImage(rec,provider)})});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'
\u25CF
\u25CF'+a.ch+"
"}function _libStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _charAgeLabel(sh){return _libStr((sh==null?void 0:sh.age_estimate)||"").trim()}function _charAgeSortVal(sh){const m=_charAgeLabel(sh).match(/\d+/);return m?parseInt(m[0],10):9999}function _charLangLabel(rec){const sh=(rec==null?void 0:rec.sheet)||{},voiceLang=rec!=null&&rec.voice&&typeof rec.voice=="object"&&rec.voice.language||"";return _libStr(sh.languages||voiceLang).trim()}function _charGenderLabel(sh){const gender=String((sh==null?void 0:sh.gender)||"").trim();return gender?gender.charAt(0).toUpperCase()+gender.slice(1):""}function _charRelsHtml(rec,allChars){var _a2;if(!allChars||allChars.length<2)return"";const relText=_libStr((_a2=rec.sheet)==null?void 0:_a2.relationships).toLowerCase();if(!relText)return"";const hits=allChars.filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,5);return hits.length?'
'+hits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":""}let _avatarHoverEl=null;function _showAvatarHoverPreview(rec,anchorEl){if(!rec.image)return;_avatarHoverEl||(_avatarHoverEl=document.createElement("div"),_avatarHoverEl.className="lib-avatar-hover-preview",_avatarHoverEl.innerHTML="",document.body.appendChild(_avatarHoverEl)),_avatarHoverEl.querySelector("img").src=rec.image;const rect=anchorEl.getBoundingClientRect(),size=512;let left=rect.right+12;left+size>window.innerWidth&&(left=rect.left-size-12);let top=rect.top+rect.height/2-size/2;top=Math.max(8,Math.min(top,window.innerHeight-size-8)),_avatarHoverEl.style.left=Math.max(8,left)+"px",_avatarHoverEl.style.top=top+"px",_avatarHoverEl.hidden=!1}function _hideAvatarHoverPreview(){_avatarHoverEl&&(_avatarHoverEl.hidden=!0)}function _wireCharCards(root,recsById,allRecs,onChange,detailOpts){const refresh=onChange||libraryRenderCharacters;root.querySelectorAll(".lib-char-card").forEach(function(card){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;const charId=card.dataset.charId,rec=recsById.get?recsById.get(charId):recsById[charId];if(rec){if(card.addEventListener("click",function(e){e.target.closest("button, .lib-char-avatar, .lib-voice-picker-popup")||_charDetailPage(rec,allRecs,detailOpts)}),(_a2=card.querySelector(".lib-char-avatar"))==null||_a2.addEventListener("click",function(e){e.stopPropagation(),_openAvatarLightbox(rec,refresh)}),rec.image){const avatarEl=card.querySelector(".lib-char-avatar");avatarEl==null||avatarEl.addEventListener("mouseenter",function(){_showAvatarHoverPreview(rec,avatarEl)}),avatarEl==null||avatarEl.addEventListener("mouseleave",_hideAvatarHoverPreview)}(_b2=card.querySelector(".lib-char-voice-pill"))==null||_b2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_c2=card.querySelector(".lib-char-pick-voice"))==null||_c2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_d2=card.querySelector(".lib-char-auto-voice"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),refresh()}),(_e2=card.querySelector(".lib-char-redesign-voice"))==null||_e2.addEventListener("click",async function(e){e.stopPropagation();const btn=e.currentTarget;btn.disabled=!0;try{await _charAutoDesignVoice(rec,!0),_schedulePendingTtsRestart(),refresh()}catch(err){toast("Voice design failed: "+(err.message||err),"error")}finally{btn.disabled=!1}}),(_f2=card.querySelector(".lib-char-remove-voice"))==null||_f2.addEventListener("click",async function(e){e.stopPropagation(),await clPut(Object.assign({},rec,{voice:"",updated:new Date})),rec.voice="",_syncVoicePictureFromChar(rec),toast("Voice removed from "+rec.name,"success"),refresh()}),(_g2=card.querySelector(".lib-char-export"))==null||_g2.addEventListener("click",function(e){e.stopPropagation(),typeof stExportRecord=="function"&&stExportRecord(rec)}),(_h2=card.querySelector(".lib-char-online-voice"))==null||_h2.addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),(_i2=card.querySelector(".lib-char-gen-voice"))==null||_i2.addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),(_j2=card.querySelector(".lib-char-voice-play"))==null||_j2.addEventListener("click",function(e){e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)})}})}let _libVoicePreviewEl=null,_libVoicePreviewBtn=null;function _libStopVoicePreview(){if(_libVoicePreviewEl&&(_libVoicePreviewEl.pause(),_libVoicePreviewEl.src=""),_libVoicePreviewBtn){_libVoicePreviewBtn.classList.remove("playing","loading");const icon=_libVoicePreviewBtn.querySelector(".mdi");icon&&(icon.className="mdi mdi-play")}_libVoicePreviewBtn=null}async function _libPreviewCharVoice(rec,btn){const voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"";if(!voiceId){toast("No voice assigned yet","error");return}if(_libVoicePreviewBtn===btn&&_libVoicePreviewEl&&!_libVoicePreviewEl.paused){_libStopVoicePreview();return}_libStopVoicePreview();const icon=btn.querySelector(".mdi"),v=(window._voices||[]).find(x=>x.id===voiceId);if(!v||!v.path||typeof voiceFileUrl!="function"){toast("Voice file not found","error");return}btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{_libVoicePreviewEl||(_libVoicePreviewEl=new Audio,_libVoicePreviewEl.addEventListener("ended",_libStopVoicePreview)),_libVoicePreviewEl.src=voiceFileUrl(v),await _libVoicePreviewEl.play(),btn.classList.remove("loading"),_libVoicePreviewBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _voiceExists(voiceId){if(!voiceId)return!0;const voices=window._voices||[];return voices.length?voices.some(function(v){return v.id===voiceId}):!0}function _charCardHtml(rec,allChars){const sh=rec.sheet||{},hue=_charHue(rec.name),hue2=(hue+40)%360,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",snippet=_libStr(sh.mannerisms||sh.voice_pattern||sh.motivation||sh.backstory||"").slice(0,190),roleLine=_libStr(sh.profession||sh.archetype).trim(),ageLabel=_charAgeLabel(sh),langLabel=_charLangLabel(rec),genderLabel=_charGenderLabel(sh),bookLabel=_libStr(rec.book||""),lineLabel=sh.line_count!=null?String(sh.line_count)+" Zeilen":"",tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),tagsHtml=tagList.length?'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+"
":"",hasPhoto=!!rec.image,bannerStyle=hasPhoto?'style="background-image:linear-gradient(180deg, rgba(0,0,0,.05) 0%, rgba(0,0,0,.72) 100%), url("'+rec.image+'"); background-size:cover; background-position:center;"':'style="--ch1:hsl('+hue+",52%,35%);--ch2:hsl("+hue2+',56%,26%)"',avatarInner=hasPhoto?'':escHtml((rec.name||"?")[0].toUpperCase()),stat=function(label,value,icon){return value?'
'+escHtml(label)+''+escHtml(value)+"
":""},metaChips=[];return bookLabel&&metaChips.push(' '+escHtml(bookLabel)+""),'
'+avatarInner+'
'+(voiceId?'':"")+'
'+escHtml(rec.name)+''+tierBadge+"
"+(roleLine?'
'+escHtml(roleLine)+"
":"")+(_libStr(sh.title)?'
Titel: '+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
aka '+escHtml(_libStr(sh.aliases))+"
":"")+'
'+metaChips.join("")+'
'+stat("Occupation",_libStr(sh.profession),"mdi-briefcase-outline")+stat("Archetype",_libStr(sh.archetype),"mdi-shape-outline")+stat("Gender",genderLabel,"mdi-gender-male-female")+stat("Age",ageLabel,"mdi-cake-variant")+stat("Lines",lineLabel,"mdi-format-list-numbered")+"
"+_charAlignHtml(sh)+"
"}function _charsTableHtml(chars,sortMode,sortDir){const arrow=function(key){return sortMode===key?' ':""},th=function(key,label,title){return'"+label+arrow(key)+""},rows=chars.map(function(rec){const sh=rec.sheet||{},hue=_charHue(rec.name),voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),voiceLang=_charLangLabel(rec),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":gender?"mdi-gender-non-binary":"",genderLabel=_charGenderLabel(sh),score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),avatarInner=rec.image?''+escHtml(rec.name)+'':escHtml((rec.name||"?")[0].toUpperCase()),occupation=_libStr(sh.profession),ageLabel=_charAgeLabel(sh),bookLabel=_libStr(rec.book||""),aliasLabel=_libStr(sh.aliases),archetype=_libStr(sh.archetype),titleLabel=_libStr(sh.title),tableMeta=[aliasLabel?"aka "+aliasLabel:"",occupation?"Occupation: "+occupation:"",titleLabel?"Title: "+titleLabel:"",archetype?"Archetype: "+archetype:""].filter(Boolean).join(" \xB7 ");return'
'+avatarInner+'
'+tierBadge+escHtml(rec.name)+"
"+(tableMeta?'
'+escHtml(tableMeta)+"
":"")+(bookLabel?'
'+escHtml(bookLabel)+"
":"")+""+(genderLabel?escHtml(genderLabel):'\u2014')+""+(ageLabel?escHtml(ageLabel):'\u2014')+""+(sh.line_count!=null?sh.line_count:'\u2014')+""+(voiceLang?escHtml(voiceLang):'\u2014')+""+(pct!=null?'
':'\u2014')+'
'+(voiceId?'"+(voiceMissing?' ':"")+escHtml(voiceId)+"":'Keine Stimme')+'
'+(voiceId?'':"")+''+(voiceId?'':"")+'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+'
'}).join("");return'
'+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")+""+rows+"
TagsBook / Script
"}function _lcdSourcesHtml(sources){const list=Array.isArray(sources)?sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[];return list.length?'
'+list.map(function(s){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":""}function _lcdField(label,value,multiline){const v=_libStr(value);return v?'
'+label+'
'+escHtml(v)+"
":""}function _lcdSection(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdSectionFull(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdPromptBox(label,value,sheetKey){const has=!!(value&&String(value).trim());return'
'+escHtml(label)+(has?"":' \u2014 not generated yet')+'
'+escHtml(value||"")+'
"}function _lcdFieldEdit(label,value,sheetKey,sourceIdxs){const v=_libStr(value),links=(sourceIdxs||[]).map(function(idx){return''+(sourceIdxs.indexOf(idx)+1)+""}).join("");return'
'+(label||links?'
'+escHtml(label)+(links?' '+links+"":"")+"
":"")+'
'+escHtml(v)+"
"}function _jumpToReaderPage(pageNum){typeof navTo=="function"&&navTo("s-reader"),setTimeout(function(){var _a2,_b2;const pages=(_a2=window.readerState)==null?void 0:_a2.pages;if(pages&&pages.length>=pageNum){const pg=pages[pageNum-1];if(pg!=null&&pg.pageDiv){pg.pageDiv.scrollIntoView({behavior:"smooth",block:"start"});return}}const sentences=(_b2=window.readerState)==null?void 0:_b2.sentences;if(sentences&&sentences.length){const target0=pageNum-1,idx=sentences.findIndex(function(s){return(s.words||[]).some(function(w){var _a3,_b3;return((_b3=(_a3=w.page)!=null?_a3:w.para)!=null?_b3:0)>=target0})});if(idx>=0&&typeof readerJumpTo=="function"){readerJumpTo(idx);return}}toast('\xD6ffne das Buch in \u201EVorlesen" und klicke nochmal auf die Quelle',"info")},300)}function _charLineCount(c){if(window.rehState&&rehState.lines&&rehState.lines.length){const key=String(c.name||"").toUpperCase().trim(),live=rehState.lines.filter(function(l){return l.type==="dialog"&&String(l.speaker||"").toUpperCase().trim()===key}).length;if(live)return live}return Number(c.sheet&&c.sheet.line_count)||0}async function _charDetailPage(rec,allChars,opts){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;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='
'+(rec.image?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
")+'
',conceptArtHtml='
"+(sh.concept_art_image?'
Concept art \u2014 '+escHtml(rec.name)+'
':'
'+(_libStr(sh.concept_art_prompt).trim()?"Kein Konzeptbild":"Kein Konzeptbild-Prompt \u2014 erst unten bei Generation Prompts erzeugen")+"
")+"
",alignHtml=pct!=null?'
B\xF6seGut'+pct+'/100
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",promptsHtml='
'+_lcdPromptBox("Voice Design Prompt",sh.voice_design_prompt,"voice_design_prompt")+_lcdPromptBox("Character Image Prompt",sh.image_prompt,"image_prompt")+_lcdPromptBox("SillyTavern Character Prompt",sh.silly_tavern_prompt,"silly_tavern_prompt")+_lcdPromptBox("Concept Art Prompt",sh.concept_art_prompt,"concept_art_prompt")+"
",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",sourcesList=Array.isArray(sh.sources)?sh.sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[],sourcesByField={};sourcesList.forEach(function(s,idx){const key=_libStr(s.line_hint||s.hint||"").trim().toLowerCase();key&&(sourcesByField[key]=sourcesByField[key]||[]).push(idx)});const sourcesHtml=sourcesList.length?'
'+sourcesList.map(function(s,idx){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":"",sidebarHtml=(allChars||[]).slice().sort(function(a,b){return _charLineCount(b)-_charLineCount(a)}).map(function(c){const h=_charHue(c.name),count=_charLineCount(c);return'
'+escHtml((c.name||"?")[0].toUpperCase())+''+escHtml(c.name)+""+(count?''+count+"":"")+"
"}).join("");container.innerHTML="";const pg=document.createElement("div");pg.className="lib-char-page",pg.style.gridColumn="1 / -1",pg.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+'
'+escHtml(_libStr(sh.aliases))+'
'+escHtml(_libStr(sh.archetype))+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+conceptArtHtml+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdFieldEdit("Voller Name",sh.full_name,"full_name",sourcesByField.full_name),_lcdFieldEdit("Vorname",sh.first_name,"first_name",sourcesByField.first_name),_lcdFieldEdit("Nachname",sh.last_name,"last_name",sourcesByField.last_name),_lcdFieldEdit("Geschlecht",sh.gender,"gender",sourcesByField.gender),_lcdFieldEdit("Titel",sh.title,"title",sourcesByField.title),_lcdFieldEdit("Beruf / Rolle",sh.profession,"profession",sourcesByField.profession),_lcdFieldEdit("Auch bekannt als",sh.aliases,"aliases",sourcesByField.aliases)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdFieldEdit("K\xF6rperlich",sh.physical,"physical",sourcesByField.physical),_lcdFieldEdit("Kleidung & Aussehen",sh.clothing,"clothing",sourcesByField.clothing)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdFieldEdit("Eigenheiten & Verhalten",sh.mannerisms,"mannerisms",sourcesByField.mannerisms),_lcdFieldEdit("Stimme & Sprache",sh.voice_pattern,"voice_pattern",sourcesByField.voice_pattern)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdFieldEdit("Hintergrund & Herkunft",sh.backstory,"backstory",sourcesByField.backstory),_lcdFieldEdit("Motivation",sh.motivation,"motivation",sourcesByField.motivation),_lcdFieldEdit("\xC4ngste",sh.fears,"fears",sourcesByField.fears)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdFieldEdit("Fertigkeiten",sh.skills,"skills",sourcesByField.skills),_lcdFieldEdit("Besondere F\xE4higkeiten",sh.capabilities,"capabilities",sourcesByField.capabilities),_lcdFieldEdit("St\xE4rkstes Attribut",sh.attribute_high,"attribute_high"),_lcdFieldEdit("Schw\xE4chstes Attribut",sh.attribute_low,"attribute_low")])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdFieldEdit("",sh.relationships,"relationships",sourcesByField.relationships),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdFieldEdit("Konfliktstil",sh.conflict_style,"conflict_style",sourcesByField.conflict_style),_lcdFieldEdit("Siegbedingung",sh.win_condition,"win_condition",sourcesByField.win_condition)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdFieldEdit("Dunkles Geheimnis / fataler Fehler",sh.secret,"secret"),_lcdFieldEdit("Charakterentwicklung",sh.arc_note,"arc_note")])+promptsHtml+"
"+sourcesHtml+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+'
Charaktere \xB7 '+escHtml(rec.book||"")+"
"+sidebarHtml+"
",container.appendChild(pg),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=''+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,prompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh):"");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='';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='';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='
'+escHtml(rec.name)+' \u2014 Konzeptbild
Concept art \u2014 '+escHtml(rec.name)+'
',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=' 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?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
",alignHtml=pct!=null?'
B\xF6se
Gut
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.arc_note)?'
'+escHtml(_libStr(sh.arc_note))+"
":"")+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",ov=document.createElement("div");ov.className="lib-char-detail-ov",ov.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
auch bekannt als '+escHtml(_libStr(sh.aliases))+"
":"")+(_libStr(sh.archetype)?'
'+escHtml(_libStr(sh.archetype))+"
":"")+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdField("Voller Name",sh.full_name,!0),_lcdField("Vorname",sh.first_name,!0),_lcdField("Nachname",sh.last_name,!0),_lcdField("Geschlecht",sh.gender,!0),_lcdField("Titel",sh.title,!0),_lcdField("Beruf / Rolle",sh.profession,!0),_lcdField("Auch bekannt als",sh.aliases,!0)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdField("K\xF6rperlich",sh.physical,!0),_lcdField("Kleidung & Aussehen",sh.clothing,!0)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdField("Eigenheiten & Verhalten",sh.mannerisms,!0),_lcdField("Stimme & Sprache",sh.voice_pattern,!0)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdField("Hintergrund & Herkunft",sh.backstory,!0),_lcdField("Motivation",sh.motivation,!0),_lcdField("\xC4ngste",sh.fears,!0)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdField("Fertigkeiten",sh.skills,!0),_lcdField("Besondere F\xE4higkeiten",sh.capabilities,!0),_lcdField("St\xE4rkstes Attribut",sh.attribute_high,!1),_lcdField("Schw\xE4chstes Attribut",sh.attribute_low,!1)])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdField("",sh.relationships,!0),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdField("Konfliktstil",sh.conflict_style,!0),_lcdField("Siegbedingung",sh.win_condition,!0)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdField("Dunkles Geheimnis / fataler Fehler",sh.secret,!0),_lcdField("Charakterentwicklung",sh.arc_note,!0)])+"
"+_lcdSourcesHtml(sh.sources)+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+"
",document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector(".lcd-close-btn").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector(".lcd-edit-btn").addEventListener("click",function(){close(),typeof clEdit=="function"&&clEdit(rec.id)}),ov.querySelector(".lcd-pick-voice").addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){close(),libraryRenderCharacters()})}),ov.querySelector(".lcd-auto-voice").addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),close(),libraryRenderCharacters()}),ov.querySelector(".lcd-online-voice").addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),ov.querySelector(".lcd-gen-voice").addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),ov.querySelector(".lcd-avatar").addEventListener("click",function(){const inp=document.createElement("input");inp.type="file",inp.accept="image/*",inp.onchange=async function(){const file=inp.files[0];if(!file)return;const fr=new FileReader;fr.onload=async function(ev){typeof clSetImage=="function"&&await clSetImage(rec.id,ev.target.result),toast("Profile picture saved","success"),close(),libraryRenderCharacters()},fr.readAsDataURL(file)},inp.click()})}window._charDetailModal=_charDetailModal;function _openAvatarLightbox(rec,onSaved){var _a2;(_a2=document.getElementById("avatar-lightbox"))==null||_a2.remove();const sh=rec.sheet||{},currentPrompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh):""),ov=document.createElement("div");ov.id="avatar-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Profilbild
'+(rec.image?''+escHtml(rec.name)+'':'
')+'
',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=''+escHtml(rec.name)+''},setStatus=function(msg,cls){const el=ov.querySelector("#alb-status");el.textContent=msg||"",el.className="llm-active-status"+(cls?" "+cls:"")},commitImage=async function(dataUri){typeof clSetImage=="function"&&await clSetImage(rec.id,dataUri),rec.image=dataUri,setPreview(dataUri),document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),toast("Profilbild gespeichert","success"),_syncVoicePictureFromChar(rec),typeof onSaved=="function"&&onSaved()};ov.querySelector("#alb-file-input").addEventListener("change",function(){const file=this.files[0];if(!file)return;const fr=new FileReader;fr.onload=function(ev){commitImage(ev.target.result)},fr.readAsDataURL(file)}),ov.querySelector("#alb-url-btn").addEventListener("click",async function(){const url=ov.querySelector("#alb-url-input").value.trim();if(!url){toast("Bild-URL eingeben","error");return}const btn=this;btn.disabled=!0,setStatus("Wird heruntergeladen\u2026");try{const r=await fetch("/api/character-image-from-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),setStatus("\u2713 Heruntergeladen","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Download fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1}}),ov.querySelector("#alb-gen-btn").addEventListener("click",async function(){const prompt=ov.querySelector("#alb-prompt").value.trim();if(!prompt){toast("Prompt eingeben","error");return}const provider=ov.querySelector("#alb-provider").value,btn=this,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generiere\u2026',setStatus("Generiere\u2026 (kann bei lokalen Modellen etwas dauern)");try{const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),rec.sheet||(rec.sheet={}),rec.sheet.image_prompt!==prompt&&(rec.sheet.image_prompt=prompt,typeof clUpsert=="function"&&await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name}),rec.id)),setStatus("\u2713 Generiert","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Generierung fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}window._openAvatarLightbox=_openAvatarLightbox;async function _syncVoicePictureFromChar(rec){if(!rec||!rec.image||!rec.voice)return;const voiceId=typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice||"");if(voiceId)try{const existing=(window._voices||[]).find(function(v){return v.id===voiceId});if(existing&&existing.has_picture)return;const blob=await(await fetch(rec.image)).blob(),fd=new FormData;fd.append("voice_id",voiceId),fd.append("file",blob,"character.jpg"),await fetch("/api/voice/picture",{method:"POST",body:fd})}catch(e){console.warn("[voice picture sync]",e)}}window._syncVoicePictureFromChar=_syncVoicePictureFromChar;function _openVoicePicker(cardEl,rec,onDone){var _a2;document.querySelectorAll(".lib-voice-picker-popup").forEach(function(p){p.remove()});let voices=window._voices||[];const gender=String(((_a2=rec.sheet)==null?void 0:_a2.gender)||"").toLowerCase(),genderMatch=gender.startsWith("f")?"f":gender.startsWith("m")?"m":"",popup=document.createElement("div");popup.className="lib-voice-picker-popup",popup.innerHTML='
',popup.querySelector(".lib-vp-design-btn").addEventListener("click",function(){popup.remove(),typeof _charDesignVoiceInline=="function"&&_charDesignVoiceInline(rec)});function renderList(filter){let list=voices.filter(function(v){return v.enabled!==!1});if(filter){const f=filter.toLowerCase();list=list.filter(function(v){return(v.id||"").toLowerCase().includes(f)||(v.name||"").toLowerCase().includes(f)})}else genderMatch&&(list=list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg.startsWith(genderMatch)||!vg}).concat(list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg&&!vg.startsWith(genderMatch)})));const ul=popup.querySelector(".lib-vp-list");ul.innerHTML=list.slice(0,500).map(function(v){return'
'+escHtml(v.id||v.name||"")+(v.gender?' \xB7 '+escHtml(v.gender)+"":"")+"
"}).join("")+(list.length===0?'
No voices found
':""),ul.querySelectorAll(".lib-vp-item").forEach(function(item){item.addEventListener("click",async function(){const vid=item.dataset.vid;await clPut(Object.assign({},rec,{voice:vid,updated:new Date})),rec.voice=vid,_syncVoicePictureFromChar(rec),popup.remove(),onDone()})})}renderList(""),popup.querySelector(".lib-vp-input").addEventListener("input",function(e){renderList(e.target.value)}),voices.length===0&&typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().then(function(){popup.isConnected&&(voices=window._voices||[],renderList(popup.querySelector(".lib-vp-input").value||""))}).catch(function(){}),document.body.appendChild(popup);const rect=cardEl.getBoundingClientRect(),popupWidth=260;popup.style.position="fixed",popup.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-popupWidth-8))+"px",popup.style.width=popupWidth+"px";const spaceBelow=window.innerHeight-rect.bottom;spaceBelow>300||spaceBelow>rect.top?popup.style.top=rect.bottom+4+"px":popup.style.bottom=window.innerHeight-rect.top+4+"px",setTimeout(function(){function close(e){popup.contains(e.target)||(popup.remove(),document.removeEventListener("click",close))}document.addEventListener("click",close)},0),popup.querySelector(".lib-vp-input").focus()}async function _findVoiceFromSameCharacterElsewhere(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey)return null;let all=[];try{all=await clGetAll()}catch{return null}const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null,match=all.find(function(r){if(r.id===rec.id||!r.voice||String(r.name||"").trim().toLowerCase()!==nameKey)return!1;const vId=typeof r.voice=="object"?r.voice.id:r.voice;return!(!_voiceExists(vId)||bookCode&&_voiceLangCode(vId)!==bookCode)});return match?{voiceId:typeof match.voice=="object"?match.voice.id:match.voice,book:match.book}:null}function _voiceLangCode(voiceId){const m=/^([A-Za-z]{2,3})_/.exec(String(voiceId||""));return m?m[1].toUpperCase():null}async function _findVoiceByCharacterName(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey||nameKey.length<3)return null;const hits=(window._voices||[]).filter(function(v){return v.enabled!==!1}).filter(function(v){return String(v.id||v.name||"").toLowerCase().includes(nameKey)});if(!hits.length)return null;const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null;if(bookCode){const langHits=hits.filter(function(v){return _voiceLangCode(v.id)===bookCode});return langHits.length?langHits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]:null}return hits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]}async function _autoAssignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}const named=await _findVoiceByCharacterName(rec);if(named){await clPut(Object.assign({},rec,{voice:named.id,updated:new Date})),rec.voice=named.id,_syncVoicePictureFromChar(rec),toast(named.id+" \u2192 "+rec.name+" (matching voice already in the library)","success");return}if(typeof _charAutoDesignVoice=="function"){await _charAutoDesignVoice(rec);return}toast("No matching voice found","error")}function _charLang(rec){const sh=rec.sheet||{},text=[sh.backstory,sh.voice_pattern,sh.mannerisms,sh.relationships,sh.motivation,sh.archetype].filter(Boolean).join(" ");return typeof detectLang=="function"?detectLang(text):""}const _bookProfileCache=new Map;async function _getBookProfile(book){const key=String(book||"").trim();if(!key)return{};if(_bookProfileCache.has(key))return _bookProfileCache.get(key);let profile={};try{const r=await fetch("/api/book-profile?book="+encodeURIComponent(key));r.ok&&(profile=await r.json())}catch(e){console.warn("[book profile]",e)}return _bookProfileCache.set(key,profile),profile}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>>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='
Book context \u2014 '+escHtml(book)+`

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

',document.body.appendChild(ov);const close=function(){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='
Existing voice found for '+escHtml(rec.name)+'

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

',document.body.appendChild(ov);let audioEl=null;ov.querySelector("#cvr-play").addEventListener("click",async function(e){const btn=e.currentTarget,icon=btn.querySelector(".mdi");if(audioEl&&!audioEl.paused){audioEl.pause(),icon.className="mdi mdi-play";return}btn.disabled=!0,icon.className="mdi mdi-loading mdi-spin";try{const langHint=typeof _resolveBookLang=="function"?await _resolveBookLang(rec).catch(function(){return""}):"",text=typeof _charSampleTextFor=="function"&&_charSampleTextFor(rec,langHint)||"Hallo, ich bin "+rec.name+".",rv=(window._voices||[]).find(x=>x.id===reuse.voiceId),rBackend=rv&&(rv.origin==="designed"||!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='
Voice design prompt \u2014 '+escHtml(rec.name)+'

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

',document.body.appendChild(ov);const close=function(){ov.remove()};ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#cdi-cancel").addEventListener("click",close),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)}).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 prompt=hasExplicitPrompt?_libStr(sh.image_prompt).trim():typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh):"";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=''+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"),_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))}(_sc=document.getElementById("stu-mode-audiobook"))==null||_sc.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)}const details=document.getElementById("stu-mecast-details");details&&(details.hidden=audiobookMode)});let _stuCastView="identify";function _stuShowCastView(view){_stuCastView=view,document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(t){t.classList.toggle("active",t.dataset.stuCastView===view)});const identifySlot=document.getElementById("stu-cast-slot"),sheetsSlot=document.getElementById("stu-castchars-slot");if(identifySlot&&(identifySlot.hidden=view!=="identify"),sheetsSlot&&(sheetsSlot.hidden=view!=="sheets"),view==="identify")_stuBorrow("reader-audiobook-panel","stu-cast-slot"),_stuCallSuppressingNav(function(){if(typeof window.audiobookOpenCastView=="function")return window.audiobookOpenCastView();typeof window.showReaderView=="function"&&window.showReaderView("cast")}),_stuRelocateCastFoot();else if(view==="sheets"){_stuBorrow("reader-charsheets-panel","stu-castchars-slot");const panel=document.getElementById("reader-charsheets-panel");if(panel&&!panel.innerHTML.trim()){panel.innerHTML='
Character sheets

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

';const goBtn=document.getElementById("stu-goto-cast-menu");goBtn&&goBtn.addEventListener("click",function(){typeof window.csForReader=="function"&&window.csForReader()})}}}document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(tab){tab.addEventListener("click",function(){_stuShowCastView(tab.dataset.stuCastView)})});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))})}),(_tc=document.getElementById("stu-phase-prev"))==null||_tc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_uc=document.getElementById("stu-phase-next"))==null||_uc.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){_stuInstallNavGuardOnce(),_stuIsActive=!0,showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow; +`),personality:persona||prev.personality||"",scenario:rec.book||prev.scenario||"",first_mes:prev.first_mes||"",mes_example:sh.voice_pattern||prev.mes_example||"",creator_notes:"Exported from TTS Voice Creator"+(rec.book?" \xB7 "+rec.book:""),system_prompt:prev.system_prompt||"",post_history_instructions:prev.post_history_instructions||"",tags:String(rec.tags||rec.book||"").split(",").map(t=>t.trim()).filter(Boolean),creator:prev.creator||"",character_version:prev.character_version||"1.0",extensions:Object.assign({},prev.extensions,{tts_voice:rec.voice||""})}}}function stDownloadJson(card,filename){const blob=new Blob([JSON.stringify(card,null,2)],{type:"application/json"}),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=filename,document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(a.href),1e3)}async function stImportCards(book,fileList){let n=0;for(const file of fileList)try{const data=await stParseFile(file),sheet=stToSheet(data);typeof clUpsert=="function"&&(await clUpsert(book||sheet.name,Object.assign({},sheet,{tags:book||""})),n++)}catch(e){typeof toast=="function"&&toast("\u201C"+(file.name||"card")+"\u201D: "+(e.message||e),"error")}return n}function stImportDialog(book,onDone){const inp=document.createElement("input");inp.type="file",inp.accept=".json,.png",inp.multiple=!0,inp.onchange=async()=>{if(!inp.files.length)return;const n=await stImportCards(book,inp.files);typeof toast=="function"&&toast(n?"Imported "+n+" character"+(n>1?"s":""):"Nothing imported",n?"success":"error"),typeof onDone=="function"&&onDone()},inp.click()}function stExportRecord(rec){const card=stFromRecord(rec),safe=String(rec.name||"character").replace(/[^\w\- ]+/g,"").trim().replace(/\s+/g,"_")||"character";stDownloadJson(card,safe+".card.json")}window.stParseFile=stParseFile,window.stToSheet=stToSheet,window.stFromRecord=stFromRecord,window.stImportCards=stImportCards,window.stImportDialog=stImportDialog,window.stExportRecord=stExportRecord;const LIB_READER_API="/api/reader/docs";function prodKey(title){return String(title||"").trim().toLowerCase()}window._libraryView=function(){try{return localStorage.getItem("ttsvc_library_view")||"books"}catch{return"books"}}(),window.navLibraryView=function(view){typeof navTo=="function"&&navTo("s-library"),window._libraryView=view;try{localStorage.setItem("ttsvc_library_view",view)}catch{}document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="characters"&&typeof refreshWorkflowCrumbs=="function"&&refreshWorkflowCrumbs("castlib"),libraryRender(view)},window.libraryRender=function(view){view=view||window._libraryView||"books",document.querySelectorAll("[data-library-view]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryView===view)}),document.querySelectorAll("[data-library-panel]").forEach(function(el){el.classList.toggle("is-active",el.dataset.libraryPanel===view)}),view==="books"?libraryRenderBooks():view==="plays"?libraryRenderPlays():view==="characters"&&typeof window.libraryRenderCharacters=="function"&&window.libraryRenderCharacters()};function _libSkeleton(n){return Array.from({length:n},()=>'
').join("")}async function libraryRenderBooks(){const list=document.getElementById("lib-books-list");if(!list)return;list.innerHTML=_libSkeleton(4);let all=[];try{const r=await fetch(LIB_READER_API);r.ok&&(all=(await r.json()).docs||[])}catch{all=[]}if(!all.length){list.innerHTML='

No books yet.

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

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

No theater plays yet.

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

';return}all.sort(function(a,b){return new Date(b.updated||0)-new Date(a.updated||0)}),list.innerHTML=all.map(function(rec){const speakers=Object.keys(rec.cast||{}),total=typeof parseScript=="function"?parseScript(rec.script||"").filter(function(l){return l.type==="dialog"}).length:0,pct=total?Math.round((rec.lineIndex||0)/total*100):0,date=rec.updated?new Date(rec.updated).toLocaleDateString():"\u2014",cov=libBookCover(rec.title||"Untitled"),avatars=speakers.slice(0,5).map(function(sp){return''+(sp[0]||"?").toUpperCase()+""}).join("");return'
'+escHtml(rec.title||"Untitled")+'
'+avatars+"
"+total+" lines \xB7 "+speakers.length+' cast
'+pct+"% \xB7 "+date+"
"}).join(""),list.querySelectorAll(".lib-play").forEach(function(el){const id=parseInt(el.dataset.id,10),title=el.dataset.title;el.addEventListener("click",function(e){e.target.closest(".reh-book-act")||openPlayInRehearser(id)});const ra=el.querySelector(".lib-act-readaloud");ra&&ra.addEventListener("click",function(e){e.stopPropagation(),productionOpenInReader(title)});const del=el.querySelector(".lib-act-del-play");del&&del.addEventListener("click",function(e){e.stopPropagation(),libConfirmDelete(el,"Delete rehearsal?","This cannot be undone.",async function(){try{typeof rehDbDelete=="function"&&await rehDbDelete(id),toast("Rehearsal deleted","success"),libraryRenderPlays()}catch(err){toast(err.message||"Delete failed","error")}})})})}async function openPlayInRehearser(id){try{const rec=typeof rehDbGetById=="function"?await rehDbGetById(id):null;rec&&typeof loadRecord=="function"?loadRecord(rec):toast("Rehearsal not found","error")}catch{toast("Could not open rehearsal","error")}}async function productionOpenInRehearser(title){const key=prodKey(title);try{const match=(typeof rehDbGetAll=="function"?await rehDbGetAll():[]).find(function(p){return prodKey(p.title)===key});if(match){openPlayInRehearser(match.id);return}}catch{}let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&book.kind!=="pdf")try{const sr=await fetch(LIB_READER_API+"/"+book.id+"/source"),text=sr.ok?await sr.text():"";if(text&&typeof audiobookOpenInRehearser=="function"){audiobookOpenInRehearser(text,title,[]);return}}catch{}if(book&&book.kind==="pdf"){typeof readerOpenLibraryDoc=="function"&&readerOpenLibraryDoc(book.id),toast('Open this PDF book, then use "Cast as audiobook" to build a rehearsal',"info");return}toast("No source to rehearse for this title yet","error")}async function productionOpenInReader(title){const key=prodKey(title);let books=[];try{const r=await fetch(LIB_READER_API);r.ok&&(books=(await r.json()).docs||[])}catch{}const book=books.find(function(b){return prodKey(b.title)===key});if(book&&typeof readerOpenLibraryDoc=="function"){readerOpenLibraryDoc(book.id);return}typeof navTo=="function"&&navTo("s-reader"),toast("No audiobook for this title yet \u2014 import its source in Read Aloud","info")}async function castForProduction(title){const out={};if(typeof clGetAllByTagOrBook!="function")return out;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{recs=[]}return recs.forEach(function(r){const name=(r.name||"").trim();if(!name)return;const voice=r.voice&&r.voice.id?r.voice.id:typeof r.voice=="string"?r.voice:"";out[name.toLowerCase()]={name,voice:voice||"",gender:r.sheet&&r.sheet.gender||"",soul:r.sheet&&(r.sheet.voice_pattern||r.sheet.motivation)||"",tags:r.tags||""}}),out}window.castForProduction=castForProduction;async function castWriteBack(title,castMap){if(!title||!castMap||typeof clGetAllByTagOrBook!="function"||typeof clPut!="function")return;let recs=[];try{recs=await clGetAllByTagOrBook(title)}catch{return}if(!recs.length)return;const byName={};recs.forEach(function(r){byName[(r.name||"").trim().toLowerCase()]=r});let n=0;for(const sp of Object.keys(castMap)){if(String(sp).includes("NARRATOR"))continue;const voice=(castMap[sp]||{}).voice;if(!voice||voice==="me")continue;const rec=byName[String(sp).trim().toLowerCase()];if(!(!rec||(rec.voice&&rec.voice.id?rec.voice.id:typeof rec.voice=="string"?rec.voice:"")===voice)){rec.voice={id:voice},rec.updated=new Date;try{await clPut(rec),n++}catch{}}}return n}window.castWriteBack=castWriteBack;function libBookCover(title){let h=0;const s=String(title||"Untitled");for(let i=0;i'+heading+'
'+sub+'
',o.addEventListener("click",function(e){e.stopPropagation()}),o.querySelector("[data-lib-cancel]").addEventListener("click",function(e){e.stopPropagation(),o.remove()}),o.querySelector("[data-lib-ok]").addEventListener("click",async function(e){e.stopPropagation(),o.innerHTML='',await onConfirm()}),cardEl.appendChild(o)}window.libraryRenderBooks=libraryRenderBooks,window.libraryRenderPlays=libraryRenderPlays,window.productionOpenInRehearser=productionOpenInRehearser,window.productionOpenInReader=productionOpenInReader,window.prodKey=prodKey;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='
Loading characters\u2026
';const byId=new Map(all.map(function(rec){return[rec.id,rec]}));if(!all.length){container.innerHTML='

No characters yet.

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

',container.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),savedScrollTop!=null&&(mainEl.scrollTop=savedScrollTop);return}const byBook={};all.forEach(function(rec){const bk=rec.book||"Unsorted";byBook[bk]||(byBook[bk]=[]),byBook[bk].push(rec)}),Object.keys(byBook).forEach(function(bk){if(!byBook[bk].some(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"})){const narrRec={id:clKey(bk,"Narrator"),book:bk,name:"Narrator",tags:bk,voice:null,image:null,sheet:{}};byId.set(narrRec.id,narrRec),byBook[bk].unshift(narrRec)}}),container.innerHTML="";const viewMode=localStorage.getItem("ttsvc_libchars_view")==="table"?"table":"cards";let returnToReader=!1;try{returnToReader=sessionStorage.getItem("ttsvc_cast_return")==="reader"}catch{}const SORT_OPTIONS=[["tier","Rolle (Haupt zuerst)"],["alpha","Alphabet"],["lines","Anzahl Zeilen"],["gender","Geschlecht"],["voice","Stimme zugewiesen"]],sortMode=SORT_OPTIONS.some(function(o){return o[0]===localStorage.getItem("ttsvc_libchars_sort")})?localStorage.getItem("ttsvc_libchars_sort"):"tier",bar=document.createElement("div");bar.className="lib-chars-toolbar",bar.innerHTML=(returnToReader?'':"")+'
',bar.querySelector("#lib-chars-import").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog("",function(){libraryRenderCharacters()})}),(_a2=bar.querySelector("#lib-chars-back-reader"))==null||_a2.addEventListener("click",function(){try{sessionStorage.removeItem("ttsvc_cast_return")}catch{}typeof navTo=="function"&&navTo("s-reader")}),bar.querySelector("#lib-chars-sort-sel").addEventListener("change",function(){localStorage.setItem("ttsvc_libchars_sort",this.value),libraryRenderCharacters()}),bar.querySelectorAll(".lib-chars-view-toggle button").forEach(function(btn){btn.addEventListener("click",function(){localStorage.setItem("ttsvc_libchars_view",btn.dataset.view),libraryRenderCharacters()})}),container.appendChild(bar);const _charSortCmp={tier:function(a,b){var _a3,_b2,_c2,_d2;const tierOrder={main:0,supporting:1,minor:2},ta=(_b2=tierOrder[String(((_a3=a.sheet)==null?void 0:_a3.tier)||"minor").toLowerCase()])!=null?_b2:2,tb=(_d2=tierOrder[String(((_c2=b.sheet)==null?void 0:_c2.tier)||"minor").toLowerCase()])!=null?_d2:2;return ta-tb||(a.name||"").localeCompare(b.name||"")},alpha:function(a,b){return(a.name||"").localeCompare(b.name||"")},lines:function(a,b){var _a3,_b2;return(((_a3=b.sheet)==null?void 0:_a3.line_count)||0)-(((_b2=a.sheet)==null?void 0:_b2.line_count)||0)||(a.name||"").localeCompare(b.name||"")},gender:function(a,b){var _a3,_b2;const ga=String(((_a3=a.sheet)==null?void 0:_a3.gender)||"zzz"),gb=String(((_b2=b.sheet)==null?void 0:_b2.gender)||"zzz");return ga.localeCompare(gb)||(a.name||"").localeCompare(b.name||"")},voice:function(a,b){return(b.voice?1:0)-(a.voice?1:0)||(a.name||"").localeCompare(b.name||"")},age:function(a,b){return _charAgeSortVal(a.sheet)-_charAgeSortVal(b.sheet)||(a.name||"").localeCompare(b.name||"")},language:function(a,b){return _charLangLabel(a).localeCompare(_charLangLabel(b))||(a.name||"").localeCompare(b.name||"")},align:function(a,b){var _a3,_b2,_c2,_d2;return((_b2=(_a3=b.sheet)==null?void 0:_a3.moral_alignment_score)!=null?_b2:-1)-((_d2=(_c2=a.sheet)==null?void 0:_c2.moral_alignment_score)!=null?_d2:-1)||(a.name||"").localeCompare(b.name||"")}},sortDir=localStorage.getItem("ttsvc_libchars_sort_dir")==="desc"?"desc":"asc",productions=document.createDocumentFragment();if(Object.keys(byBook).sort().forEach(function(book){const chars=byBook[book].sort(_charSortCmp[sortMode]||_charSortCmp.tier);sortDir==="desc"&&chars.reverse();const narrIdx=chars.findIndex(function(r){return String(r.name||"").trim().toLowerCase()==="narrator"});narrIdx>0&&chars.unshift(chars.splice(narrIdx,1)[0]);const cov=libBookCover(book),prod=document.createElement("div");prod.className="lib-chars-production",prod.dataset.book=book;const collapseKey="ttsvc_libchars_collapsed::"+book;let isCollapsed=localStorage.getItem(collapseKey)==="1";window._libCharsScrollToBook&&(isCollapsed=book!==window._libCharsScrollToBook),isCollapsed&&prod.classList.add("lib-chars-production-collapsed"),prod.innerHTML='
'+escHtml(book)+'
`+(viewMode==="table"?_charsTableHtml(chars,sortMode,sortDir):'
'+chars.map(function(rec){return _charCardHtml(rec,chars)}).join("")+"
")+"
",prod.querySelector(".lib-chars-prod-collapse-btn").addEventListener("click",function(e){e.stopPropagation();const collapsed=prod.classList.toggle("lib-chars-production-collapsed");localStorage.setItem(collapseKey,collapsed?"1":"0")}),prod.querySelector(".lib-chars-prod-head").addEventListener("click",function(e){e.target.closest("button, select, input, a")||prod.querySelector(".lib-chars-prod-collapse-btn").click()}),prod.querySelector(".lib-chars-bookctx-btn").addEventListener("click",function(){_editBookProfile(book)}),prod.querySelector(".lib-chars-casting-btn").addEventListener("click",function(){typeof navTo=="function"&&navTo("s-reader")}),prod.querySelector(".lib-chars-cast-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book),toast("Open the book in Read Aloud then click Cast Characters","info")}),prod.querySelector(".lib-chars-reh-btn").addEventListener("click",function(){typeof productionOpenInRehearser=="function"&&productionOpenInRehearser(book)}),prod.querySelector(".lib-chars-read-btn").addEventListener("click",function(){typeof productionOpenInReader=="function"&&productionOpenInReader(book)}),prod.querySelector(".lib-chars-imp-btn").addEventListener("click",function(){typeof stImportDialog=="function"&&stImportDialog(book,function(){libraryRenderCharacters()})}),prod.querySelectorAll("[data-sort-key]").forEach(function(th){th.addEventListener("click",function(){const key=th.dataset.sortKey,nextDir=sortMode===key&&sortDir==="asc"?"desc":"asc";localStorage.setItem("ttsvc_libchars_sort",key),localStorage.setItem("ttsvc_libchars_sort_dir",nextDir),libraryRenderCharacters()})});const 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&&checkedN0&&boxes.every(function(cb){return cb.checked});boxes.forEach(function(cb){cb.checked=!allChecked}),refreshBulkBtn()}),tblSelectAllCb&&tblSelectAllCb.addEventListener("change",function(){[...prod.querySelectorAll(".lib-char-select-cb")].forEach(function(cb){cb.checked=tblSelectAllCb.checked}),refreshBulkBtn()}),syncTblSelectAllCb();const runBulk=async function(btn,ids,verb,fn){btn.disabled=!0;const orig=btn.innerHTML;let done=0,failed=0,lastErrMsg="",repeatErrMsg="",repeatCount=0,aborted=!1;for(const id of ids){const rec=byId.get(id);if(rec){btn.innerHTML=' '+verb+" "+(done+failed+1)+" / "+ids.length+"\u2026";try{await fn(rec),done++,repeatCount=0}catch(e){if(failed++,lastErrMsg=e&&e.message?e.message:String(e),console.error("[bulk "+verb+"]",rec.name,e),lastErrMsg===repeatErrMsg?repeatCount++:(repeatErrMsg=lastErrMsg,repeatCount=1),repeatCount>=3){aborted=!0;break}}}}btn.innerHTML=orig;const remaining=ids.length-done-failed,suffix=failed?` (${failed} failed${aborted&&remaining?`, ${remaining} skipped`:""}${lastErrMsg?": "+lastErrMsg.slice(0,200):""})`:"";toast(`${verb} finished for ${done} character${done!==1?"s":""}${suffix}`,failed&&!done?"error":"success"),await _flushPendingTtsRestart(),typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary({refresh:!0}).catch(()=>{}),libraryRenderCharacters()};bulkBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(bulkBtn,ids,"Assigning",_autoAssignVoice)}),designBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});ids.length&&runBulk(designBtn,ids,"Designing",_charAutoDesignVoice)}),imageBtn.addEventListener("click",function(){const ids=[...prod.querySelectorAll(".lib-char-select-cb:checked")].map(function(cb){return cb.dataset.charId});if(!ids.length)return;const provider=imageProviderSel?imageProviderSel.value:"";runBulk(imageBtn,ids,"Generating images",function(rec){return _charAutoGenerateImage(rec,provider)})});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'
\u25CF
\u25CF'+a.ch+"
"}function _libStr(v){return v==null?"":typeof v=="string"?v:Array.isArray(v)?v.filter(Boolean).join(", "):JSON.stringify(v)}function _charAgeLabel(sh){return _libStr((sh==null?void 0:sh.age_estimate)||"").trim()}function _charAgeSortVal(sh){const m=_charAgeLabel(sh).match(/\d+/);return m?parseInt(m[0],10):9999}function _charLangLabel(rec){const sh=(rec==null?void 0:rec.sheet)||{},voiceLang=rec!=null&&rec.voice&&typeof rec.voice=="object"&&rec.voice.language||"";return _libStr(sh.languages||voiceLang).trim()}function _charGenderLabel(sh){const gender=String((sh==null?void 0:sh.gender)||"").trim();return gender?gender.charAt(0).toUpperCase()+gender.slice(1):""}function _charRelsHtml(rec,allChars){var _a2;if(!allChars||allChars.length<2)return"";const relText=_libStr((_a2=rec.sheet)==null?void 0:_a2.relationships).toLowerCase();if(!relText)return"";const hits=allChars.filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,5);return hits.length?'
'+hits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":""}let _avatarHoverEl=null;function _showAvatarHoverPreview(rec,anchorEl){if(!rec.image)return;_avatarHoverEl||(_avatarHoverEl=document.createElement("div"),_avatarHoverEl.className="lib-avatar-hover-preview",_avatarHoverEl.innerHTML="",document.body.appendChild(_avatarHoverEl)),_avatarHoverEl.querySelector("img").src=rec.image;const rect=anchorEl.getBoundingClientRect(),size=512;let left=rect.right+12;left+size>window.innerWidth&&(left=rect.left-size-12);let top=rect.top+rect.height/2-size/2;top=Math.max(8,Math.min(top,window.innerHeight-size-8)),_avatarHoverEl.style.left=Math.max(8,left)+"px",_avatarHoverEl.style.top=top+"px",_avatarHoverEl.hidden=!1}function _hideAvatarHoverPreview(){_avatarHoverEl&&(_avatarHoverEl.hidden=!0)}function _wireCharCards(root,recsById,allRecs,onChange,detailOpts){const refresh=onChange||libraryRenderCharacters;root.querySelectorAll(".lib-char-card").forEach(function(card){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;const charId=card.dataset.charId,rec=recsById.get?recsById.get(charId):recsById[charId];if(rec){if(card.addEventListener("click",function(e){e.target.closest("button, .lib-char-avatar, .lib-voice-picker-popup")||_charDetailPage(rec,allRecs,detailOpts)}),(_a2=card.querySelector(".lib-char-avatar"))==null||_a2.addEventListener("click",function(e){e.stopPropagation(),_openAvatarLightbox(rec,refresh)}),rec.image){const avatarEl=card.querySelector(".lib-char-avatar");avatarEl==null||avatarEl.addEventListener("mouseenter",function(){_showAvatarHoverPreview(rec,avatarEl)}),avatarEl==null||avatarEl.addEventListener("mouseleave",_hideAvatarHoverPreview)}(_b2=card.querySelector(".lib-char-voice-pill"))==null||_b2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_c2=card.querySelector(".lib-char-pick-voice"))==null||_c2.addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){refresh()})}),(_d2=card.querySelector(".lib-char-auto-voice"))==null||_d2.addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),refresh()}),(_e2=card.querySelector(".lib-char-redesign-voice"))==null||_e2.addEventListener("click",async function(e){e.stopPropagation();const btn=e.currentTarget;btn.disabled=!0;try{await _charAutoDesignVoice(rec,!0),_schedulePendingTtsRestart(),refresh()}catch(err){toast("Voice design failed: "+(err.message||err),"error")}finally{btn.disabled=!1}}),(_f2=card.querySelector(".lib-char-remove-voice"))==null||_f2.addEventListener("click",async function(e){e.stopPropagation(),await clPut(Object.assign({},rec,{voice:"",updated:new Date})),rec.voice="",_syncVoicePictureFromChar(rec),toast("Voice removed from "+rec.name,"success"),refresh()}),(_g2=card.querySelector(".lib-char-export"))==null||_g2.addEventListener("click",function(e){e.stopPropagation(),typeof stExportRecord=="function"&&stExportRecord(rec)}),(_h2=card.querySelector(".lib-char-online-voice"))==null||_h2.addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),(_i2=card.querySelector(".lib-char-gen-voice"))==null||_i2.addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),(_j2=card.querySelector(".lib-char-voice-play"))==null||_j2.addEventListener("click",function(e){e.stopPropagation(),_libPreviewCharVoice(rec,e.currentTarget)})}})}let _libVoicePreviewEl=null,_libVoicePreviewBtn=null;function _libStopVoicePreview(){if(_libVoicePreviewEl&&(_libVoicePreviewEl.pause(),_libVoicePreviewEl.src=""),_libVoicePreviewBtn){_libVoicePreviewBtn.classList.remove("playing","loading");const icon=_libVoicePreviewBtn.querySelector(".mdi");icon&&(icon.className="mdi mdi-play")}_libVoicePreviewBtn=null}async function _libPreviewCharVoice(rec,btn){const voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"";if(!voiceId){toast("No voice assigned yet","error");return}if(_libVoicePreviewBtn===btn&&_libVoicePreviewEl&&!_libVoicePreviewEl.paused){_libStopVoicePreview();return}_libStopVoicePreview();const icon=btn.querySelector(".mdi"),v=(window._voices||[]).find(x=>x.id===voiceId);if(!v||!v.path||typeof voiceFileUrl!="function"){toast("Voice file not found","error");return}btn.classList.add("loading"),icon&&(icon.className="mdi mdi-loading");try{_libVoicePreviewEl||(_libVoicePreviewEl=new Audio,_libVoicePreviewEl.addEventListener("ended",_libStopVoicePreview)),_libVoicePreviewEl.src=voiceFileUrl(v),await _libVoicePreviewEl.play(),btn.classList.remove("loading"),_libVoicePreviewBtn=btn,btn.classList.add("playing"),icon&&(icon.className="mdi mdi-stop")}catch(e){btn.classList.remove("loading"),icon&&(icon.className="mdi mdi-play"),toast("Preview failed: "+(e.message||e),"error")}}function _voiceExists(voiceId){if(!voiceId)return!0;const voices=window._voices||[];return voices.length?voices.some(function(v){return v.id===voiceId}):!0}function _charCardHtml(rec,allChars){const sh=rec.sheet||{},hue=_charHue(rec.name),hue2=(hue+40)%360,voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":"mdi-gender-non-binary",snippet=_libStr(sh.mannerisms||sh.voice_pattern||sh.motivation||sh.backstory||"").slice(0,190),roleLine=_libStr(sh.profession||sh.archetype).trim(),ageLabel=_charAgeLabel(sh),langLabel=_charLangLabel(rec),genderLabel=_charGenderLabel(sh),bookLabel=_libStr(rec.book||""),lineLabel=sh.line_count!=null?String(sh.line_count)+" Zeilen":"",tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),tagsHtml=tagList.length?'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+"
":"",hasPhoto=!!rec.image,bannerStyle=hasPhoto?'style="background-image:linear-gradient(180deg, rgba(0,0,0,.05) 0%, rgba(0,0,0,.72) 100%), url("'+rec.image+'"); background-size:cover; background-position:center;"':'style="--ch1:hsl('+hue+",52%,35%);--ch2:hsl("+hue2+',56%,26%)"',avatarInner=hasPhoto?'':escHtml((rec.name||"?")[0].toUpperCase()),stat=function(label,value,icon){return value?'
'+escHtml(label)+''+escHtml(value)+"
":""},metaChips=[];return bookLabel&&metaChips.push(' '+escHtml(bookLabel)+""),'
'+avatarInner+'
'+(voiceId?'':"")+'
'+escHtml(rec.name)+''+tierBadge+"
"+(roleLine?'
'+escHtml(roleLine)+"
":"")+(_libStr(sh.title)?'
Titel: '+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
aka '+escHtml(_libStr(sh.aliases))+"
":"")+'
'+metaChips.join("")+'
'+stat("Occupation",_libStr(sh.profession),"mdi-briefcase-outline")+stat("Archetype",_libStr(sh.archetype),"mdi-shape-outline")+stat("Gender",genderLabel,"mdi-gender-male-female")+stat("Age",ageLabel,"mdi-cake-variant")+stat("Lines",lineLabel,"mdi-format-list-numbered")+"
"+_charAlignHtml(sh)+"
"}function _charsTableHtml(chars,sortMode,sortDir){const arrow=function(key){return sortMode===key?' ':""},th=function(key,label,title){return'"+label+arrow(key)+""},rows=chars.map(function(rec){const sh=rec.sheet||{},hue=_charHue(rec.name),voiceId=rec.voice?typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice):"",voiceMissing=!!voiceId&&!_voiceExists(voiceId),voiceLang=_charLangLabel(rec),tier=String(sh.tier||"").toLowerCase(),tierBadge=tier==="main"?'Haupt':tier==="supporting"?'Neben':"",gender=String(sh.gender||"").toLowerCase(),genderIcon=gender.startsWith("f")?"mdi-gender-female":gender.startsWith("m")?"mdi-gender-male":gender?"mdi-gender-non-binary":"",genderLabel=_charGenderLabel(sh),score=sh.moral_alignment_score,pct=score!=null?Math.max(0,Math.min(100,score)):null,tagList=String(rec.tags||"").split(",").map(function(t){return t.trim()}).filter(Boolean),avatarInner=rec.image?''+escHtml(rec.name)+'':escHtml((rec.name||"?")[0].toUpperCase()),occupation=_libStr(sh.profession),ageLabel=_charAgeLabel(sh),bookLabel=_libStr(rec.book||""),aliasLabel=_libStr(sh.aliases),archetype=_libStr(sh.archetype),titleLabel=_libStr(sh.title),tableMeta=[aliasLabel?"aka "+aliasLabel:"",occupation?"Occupation: "+occupation:"",titleLabel?"Title: "+titleLabel:"",archetype?"Archetype: "+archetype:""].filter(Boolean).join(" \xB7 ");return'
'+avatarInner+'
'+tierBadge+escHtml(rec.name)+"
"+(tableMeta?'
'+escHtml(tableMeta)+"
":"")+(bookLabel?'
'+escHtml(bookLabel)+"
":"")+""+(genderLabel?escHtml(genderLabel):'\u2014')+""+(ageLabel?escHtml(ageLabel):'\u2014')+""+(sh.line_count!=null?sh.line_count:'\u2014')+""+(voiceLang?escHtml(voiceLang):'\u2014')+""+(pct!=null?'
':'\u2014')+'
'+(voiceId?'"+(voiceMissing?' ':"")+escHtml(voiceId)+"":'Keine Stimme')+'
'+(voiceId?'':"")+''+(voiceId?'':"")+'
'+tagList.map(function(t){return''+escHtml(t)+""}).join("")+'
'}).join("");return'
'+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")+""+rows+"
TagsBook / Script
"}function _lcdSourcesHtml(sources){const list=Array.isArray(sources)?sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[];return list.length?'
'+list.map(function(s){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":""}function _lcdField(label,value,multiline){const v=_libStr(value);return v?'
'+label+'
'+escHtml(v)+"
":""}function _lcdSection(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdSectionFull(icon,label,fields){const body=fields.join("");return body?'
"+body+"
":""}function _lcdPromptBox(label,value,sheetKey){const has=!!(value&&String(value).trim());return'
'+escHtml(label)+(has?"":' \u2014 not generated yet')+'
'+escHtml(value||"")+'
"}function _lcdFieldEdit(label,value,sheetKey,sourceIdxs){const v=_libStr(value),links=(sourceIdxs||[]).map(function(idx){return''+(sourceIdxs.indexOf(idx)+1)+""}).join("");return'
'+(label||links?'
'+escHtml(label)+(links?' '+links+"":"")+"
":"")+'
'+escHtml(v)+"
"}function _jumpToReaderPage(pageNum){typeof navTo=="function"&&navTo("s-reader"),setTimeout(function(){var _a2,_b2;const pages=(_a2=window.readerState)==null?void 0:_a2.pages;if(pages&&pages.length>=pageNum){const pg=pages[pageNum-1];if(pg!=null&&pg.pageDiv){pg.pageDiv.scrollIntoView({behavior:"smooth",block:"start"});return}}const sentences=(_b2=window.readerState)==null?void 0:_b2.sentences;if(sentences&&sentences.length){const target0=pageNum-1,idx=sentences.findIndex(function(s){return(s.words||[]).some(function(w){var _a3,_b3;return((_b3=(_a3=w.page)!=null?_a3:w.para)!=null?_b3:0)>=target0})});if(idx>=0&&typeof readerJumpTo=="function"){readerJumpTo(idx);return}}toast('\xD6ffne das Buch in \u201EVorlesen" und klicke nochmal auf die Quelle',"info")},300)}function _charLineCount(c){if(window.rehState&&rehState.lines&&rehState.lines.length){const key=String(c.name||"").toUpperCase().trim(),live=rehState.lines.filter(function(l){return l.type==="dialog"&&String(l.speaker||"").toUpperCase().trim()===key}).length;if(live)return live}return Number(c.sheet&&c.sheet.line_count)||0}async function _charDetailPage(rec,allChars,opts){var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2;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='
'+(rec.image?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
")+'
',conceptArtHtml='
"+(sh.concept_art_image?'
Concept art \u2014 '+escHtml(rec.name)+'
':'
'+(_libStr(sh.concept_art_prompt).trim()?"Kein Konzeptbild":"Kein Konzeptbild-Prompt \u2014 erst unten bei Generation Prompts erzeugen")+"
")+"
",alignHtml=pct!=null?'
B\xF6seGut'+pct+'/100
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",promptsHtml='
'+_lcdPromptBox("Voice Design Prompt",sh.voice_design_prompt,"voice_design_prompt")+_lcdPromptBox("Character Image Prompt",sh.image_prompt,"image_prompt")+_lcdPromptBox("SillyTavern Character Prompt",sh.silly_tavern_prompt,"silly_tavern_prompt")+_lcdPromptBox("Concept Art Prompt",sh.concept_art_prompt,"concept_art_prompt")+"
",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",sourcesList=Array.isArray(sh.sources)?sh.sources.filter(function(s){return s&&(s.quote||s.page!=null)}):[],sourcesByField={};sourcesList.forEach(function(s,idx){const key=_libStr(s.line_hint||s.hint||"").trim().toLowerCase();key&&(sourcesByField[key]=sourcesByField[key]||[]).push(idx)});const sourcesHtml=sourcesList.length?'
'+sourcesList.map(function(s,idx){const page=s.page!=null?"Seite "+s.page:"",hint=_libStr(s.line_hint||s.hint||"");return'
'+(page||hint?'
'+escHtml([page,hint].filter(Boolean).join(" \xB7 "))+"
":"")+(s.quote?'
\u201E'+escHtml(_libStr(s.quote))+'"
':"")+"
"}).join("")+"
":"",sidebarHtml=(allChars||[]).slice().sort(function(a,b){return _charLineCount(b)-_charLineCount(a)}).map(function(c){const h=_charHue(c.name),count=_charLineCount(c);return'
'+escHtml((c.name||"?")[0].toUpperCase())+''+escHtml(c.name)+""+(count?''+count+"":"")+"
"}).join("");container.innerHTML="";const pg=document.createElement("div");pg.className="lib-char-page",pg.style.gridColumn="1 / -1",pg.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+'
'+escHtml(_libStr(sh.aliases))+'
'+escHtml(_libStr(sh.archetype))+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+conceptArtHtml+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdFieldEdit("Voller Name",sh.full_name,"full_name",sourcesByField.full_name),_lcdFieldEdit("Vorname",sh.first_name,"first_name",sourcesByField.first_name),_lcdFieldEdit("Nachname",sh.last_name,"last_name",sourcesByField.last_name),_lcdFieldEdit("Geschlecht",sh.gender,"gender",sourcesByField.gender),_lcdFieldEdit("Titel",sh.title,"title",sourcesByField.title),_lcdFieldEdit("Beruf / Rolle",sh.profession,"profession",sourcesByField.profession),_lcdFieldEdit("Auch bekannt als",sh.aliases,"aliases",sourcesByField.aliases)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdFieldEdit("K\xF6rperlich",sh.physical,"physical",sourcesByField.physical),_lcdFieldEdit("Kleidung & Aussehen",sh.clothing,"clothing",sourcesByField.clothing)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdFieldEdit("Eigenheiten & Verhalten",sh.mannerisms,"mannerisms",sourcesByField.mannerisms),_lcdFieldEdit("Stimme & Sprache",sh.voice_pattern,"voice_pattern",sourcesByField.voice_pattern)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdFieldEdit("Hintergrund & Herkunft",sh.backstory,"backstory",sourcesByField.backstory),_lcdFieldEdit("Motivation",sh.motivation,"motivation",sourcesByField.motivation),_lcdFieldEdit("\xC4ngste",sh.fears,"fears",sourcesByField.fears)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdFieldEdit("Fertigkeiten",sh.skills,"skills",sourcesByField.skills),_lcdFieldEdit("Besondere F\xE4higkeiten",sh.capabilities,"capabilities",sourcesByField.capabilities),_lcdFieldEdit("St\xE4rkstes Attribut",sh.attribute_high,"attribute_high"),_lcdFieldEdit("Schw\xE4chstes Attribut",sh.attribute_low,"attribute_low")])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdFieldEdit("",sh.relationships,"relationships",sourcesByField.relationships),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdFieldEdit("Konfliktstil",sh.conflict_style,"conflict_style",sourcesByField.conflict_style),_lcdFieldEdit("Siegbedingung",sh.win_condition,"win_condition",sourcesByField.win_condition)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdFieldEdit("Dunkles Geheimnis / fataler Fehler",sh.secret,"secret"),_lcdFieldEdit("Charakterentwicklung",sh.arc_note,"arc_note")])+promptsHtml+"
"+sourcesHtml+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+'
Charaktere \xB7 '+escHtml(rec.book||"")+"
"+sidebarHtml+"
",container.appendChild(pg),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=''+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,prompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh):"");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='';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='';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='
'+escHtml(rec.name)+' \u2014 Konzeptbild
Concept art \u2014 '+escHtml(rec.name)+'
',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=' 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?'
'+escHtml(rec.name)+'
':'
'+escHtml((rec.name||"?")[0].toUpperCase())+"
",alignHtml=pct!=null?'
B\xF6se
Gut
'+arcInfo.ch+" "+arcInfo.label+(pct>=70?" \xB7 Rechtschaffen ("+pct+"/100)":pct<=30?" \xB7 B\xF6se ("+pct+"/100)":" \xB7 Moralisch ambivalent ("+pct+"/100)")+"
"+(_libStr(sh.arc_note)?'
'+escHtml(_libStr(sh.arc_note))+"
":"")+(_libStr(sh.alignment)?'
'+escHtml(_libStr(sh.alignment))+"
":"")+"
":"",relText=_libStr(sh.relationships).toLowerCase(),relHits=(allChars||[]).filter(function(c){return c.id!==rec.id&&(c.name||"").length>1}).map(function(c){const re=new RegExp(c.name.replace(/[.*+?^${}()|[\]\\]/g,"\\$&"),"gi");return{c,n:(relText.match(re)||[]).length}}).filter(function(x){return x.n>0}).sort(function(a,b){return b.n-a.n}).slice(0,8),relDotsHtml=relHits.length?'
'+relHits.map(function(x){return''+escHtml((x.c.name||"?")[0].toUpperCase())+""}).join("")+"
":"",ov=document.createElement("div");ov.className="lib-char-detail-ov",ov.innerHTML='
'+avatarHtml+'
'+escHtml(rec.name)+"
"+(_libStr(sh.full_name)&&_libStr(sh.full_name).toLowerCase()!==String(rec.name||"").toLowerCase()?'
'+escHtml(_libStr(sh.full_name))+"
":"")+(_libStr(sh.title)?'
'+escHtml(_libStr(sh.title))+"
":"")+(_libStr(sh.aliases)?'
auch bekannt als '+escHtml(_libStr(sh.aliases))+"
":"")+(_libStr(sh.archetype)?'
'+escHtml(_libStr(sh.archetype))+"
":"")+'
'+(tierLabel?''+tierLabel+"":"")+(gender?' '+escHtml(gender)+"":"")+'
'+(voiceId?_voiceExists(voiceId)?escHtml(voiceId):' '+escHtml(voiceId)+"":'Noch keine Stimme zugewiesen')+'
'+alignHtml+'
'+_lcdSection("mdi-card-account-details-outline","Identit\xE4t",[_lcdField("Voller Name",sh.full_name,!0),_lcdField("Vorname",sh.first_name,!0),_lcdField("Nachname",sh.last_name,!0),_lcdField("Geschlecht",sh.gender,!0),_lcdField("Titel",sh.title,!0),_lcdField("Beruf / Rolle",sh.profession,!0),_lcdField("Auch bekannt als",sh.aliases,!0)])+_lcdSection("mdi-account-outline","Erscheinung",[_lcdField("K\xF6rperlich",sh.physical,!0),_lcdField("Kleidung & Aussehen",sh.clothing,!0)])+_lcdSection("mdi-drama-masks","Pers\xF6nlichkeit",[_lcdField("Eigenheiten & Verhalten",sh.mannerisms,!0),_lcdField("Stimme & Sprache",sh.voice_pattern,!0)])+_lcdSection("mdi-book-open-outline","Geschichte",[_lcdField("Hintergrund & Herkunft",sh.backstory,!0),_lcdField("Motivation",sh.motivation,!0),_lcdField("\xC4ngste",sh.fears,!0)])+_lcdSection("mdi-sword","F\xE4higkeiten",[_lcdField("Fertigkeiten",sh.skills,!0),_lcdField("Besondere F\xE4higkeiten",sh.capabilities,!0),_lcdField("St\xE4rkstes Attribut",sh.attribute_high,!1),_lcdField("Schw\xE4chstes Attribut",sh.attribute_low,!1)])+_lcdSectionFull("mdi-account-group-outline","Beziehungen",[_lcdField("",sh.relationships,!0),relDotsHtml])+_lcdSection("mdi-shield-sword-outline","Konflikt & Strategie",[_lcdField("Konfliktstil",sh.conflict_style,!0),_lcdField("Siegbedingung",sh.win_condition,!0)])+_lcdSection("mdi-eye-outline","Geheimnisse & Bogen",[_lcdField("Dunkles Geheimnis / fataler Fehler",sh.secret,!0),_lcdField("Charakterentwicklung",sh.arc_note,!0)])+"
"+_lcdSourcesHtml(sh.sources)+(rec.analysis?'
'+escHtml(String(rec.analysis))+"
":"")+"
",document.body.appendChild(ov);const close=function(){ov.remove()};ov.querySelector(".lcd-close-btn").addEventListener("click",close),ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector(".lcd-edit-btn").addEventListener("click",function(){close(),typeof clEdit=="function"&&clEdit(rec.id)}),ov.querySelector(".lcd-pick-voice").addEventListener("click",function(e){e.stopPropagation(),_openVoicePicker(e.currentTarget,rec,function(){close(),libraryRenderCharacters()})}),ov.querySelector(".lcd-auto-voice").addEventListener("click",async function(e){e.stopPropagation(),await _autoAssignVoice(rec),close(),libraryRenderCharacters()}),ov.querySelector(".lcd-online-voice").addEventListener("click",function(e){e.stopPropagation(),_charSearchOnline(rec)}),ov.querySelector(".lcd-gen-voice").addEventListener("click",function(e){e.stopPropagation(),_charDesignVoiceInline(rec)}),ov.querySelector(".lcd-avatar").addEventListener("click",function(){const inp=document.createElement("input");inp.type="file",inp.accept="image/*",inp.onchange=async function(){const file=inp.files[0];if(!file)return;const fr=new FileReader;fr.onload=async function(ev){typeof clSetImage=="function"&&await clSetImage(rec.id,ev.target.result),toast("Profile picture saved","success"),close(),libraryRenderCharacters()},fr.readAsDataURL(file)},inp.click()})}window._charDetailModal=_charDetailModal;function _openAvatarLightbox(rec,onSaved){var _a2;(_a2=document.getElementById("avatar-lightbox"))==null||_a2.remove();const sh=rec.sheet||{},currentPrompt=_libStr(sh.image_prompt).trim()||(typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh):""),ov=document.createElement("div");ov.id="avatar-lightbox",ov.className="audiobook-overlay",ov.innerHTML='
'+escHtml(rec.name)+' \u2014 Profilbild
'+(rec.image?''+escHtml(rec.name)+'':'
')+'
',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=''+escHtml(rec.name)+''},setStatus=function(msg,cls){const el=ov.querySelector("#alb-status");el.textContent=msg||"",el.className="llm-active-status"+(cls?" "+cls:"")},commitImage=async function(dataUri){typeof clSetImage=="function"&&await clSetImage(rec.id,dataUri),rec.image=dataUri,setPreview(dataUri),document.querySelectorAll('.lib-char-avatar[data-char-id="'+CSS.escape(rec.id)+'"]').forEach(function(av){av.innerHTML=''+escHtml(rec.name)+''}),toast("Profilbild gespeichert","success"),_syncVoicePictureFromChar(rec),typeof onSaved=="function"&&onSaved()};ov.querySelector("#alb-file-input").addEventListener("change",function(){const file=this.files[0];if(!file)return;const fr=new FileReader;fr.onload=function(ev){commitImage(ev.target.result)},fr.readAsDataURL(file)}),ov.querySelector("#alb-url-btn").addEventListener("click",async function(){const url=ov.querySelector("#alb-url-input").value.trim();if(!url){toast("Bild-URL eingeben","error");return}const btn=this;btn.disabled=!0,setStatus("Wird heruntergeladen\u2026");try{const r=await fetch("/api/character-image-from-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({url})}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),setStatus("\u2713 Heruntergeladen","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Download fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1}}),ov.querySelector("#alb-gen-btn").addEventListener("click",async function(){const prompt=ov.querySelector("#alb-prompt").value.trim();if(!prompt){toast("Prompt eingeben","error");return}const provider=ov.querySelector("#alb-provider").value,btn=this,orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Generiere\u2026',setStatus("Generiere\u2026 (kann bei lokalen Modellen etwas dauern)");try{const body={prompt};provider&&(body.provider=provider);const r=await fetch("/api/character-generate-image",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}),d=await r.json();if(!r.ok)throw new Error(d.detail||r.statusText);await commitImage(d.image),rec.sheet||(rec.sheet={}),rec.sheet.image_prompt!==prompt&&(rec.sheet.image_prompt=prompt,typeof clUpsert=="function"&&await clUpsert(rec.book,Object.assign({},rec.sheet,{name:rec.name}),rec.id)),setStatus("\u2713 Generiert","ok")}catch(e){setStatus("Fehlgeschlagen","err"),toast("Generierung fehlgeschlagen: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig}})}window._openAvatarLightbox=_openAvatarLightbox;async function _syncVoicePictureFromChar(rec){if(!rec||!rec.image||!rec.voice)return;const voiceId=typeof rec.voice=="object"?rec.voice.id||"":String(rec.voice||"");if(voiceId)try{const existing=(window._voices||[]).find(function(v){return v.id===voiceId});if(existing&&existing.has_picture)return;const blob=await(await fetch(rec.image)).blob(),fd=new FormData;fd.append("voice_id",voiceId),fd.append("file",blob,"character.jpg"),await fetch("/api/voice/picture",{method:"POST",body:fd})}catch(e){console.warn("[voice picture sync]",e)}}window._syncVoicePictureFromChar=_syncVoicePictureFromChar;function _openVoicePicker(cardEl,rec,onDone){var _a2;document.querySelectorAll(".lib-voice-picker-popup").forEach(function(p){p.remove()});let voices=window._voices||[];const gender=String(((_a2=rec.sheet)==null?void 0:_a2.gender)||"").toLowerCase(),genderMatch=gender.startsWith("f")?"f":gender.startsWith("m")?"m":"",popup=document.createElement("div");popup.className="lib-voice-picker-popup",popup.innerHTML='
',popup.querySelector(".lib-vp-design-btn").addEventListener("click",function(){popup.remove(),typeof _charDesignVoiceInline=="function"&&_charDesignVoiceInline(rec)});function renderList(filter){let list=voices.filter(function(v){return v.enabled!==!1});if(filter){const f=filter.toLowerCase();list=list.filter(function(v){return(v.id||"").toLowerCase().includes(f)||(v.name||"").toLowerCase().includes(f)})}else genderMatch&&(list=list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg.startsWith(genderMatch)||!vg}).concat(list.filter(function(v){const vg=String(v.gender||"").toLowerCase();return vg&&!vg.startsWith(genderMatch)})));const ul=popup.querySelector(".lib-vp-list");ul.innerHTML=list.slice(0,500).map(function(v){return'
'+escHtml(v.id||v.name||"")+(v.gender?' \xB7 '+escHtml(v.gender)+"":"")+"
"}).join("")+(list.length===0?'
No voices found
':""),ul.querySelectorAll(".lib-vp-item").forEach(function(item){item.addEventListener("click",async function(){const vid=item.dataset.vid;await clPut(Object.assign({},rec,{voice:vid,updated:new Date})),rec.voice=vid,_syncVoicePictureFromChar(rec),popup.remove(),onDone()})})}renderList(""),popup.querySelector(".lib-vp-input").addEventListener("input",function(e){renderList(e.target.value)}),voices.length===0&&typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().then(function(){popup.isConnected&&(voices=window._voices||[],renderList(popup.querySelector(".lib-vp-input").value||""))}).catch(function(){}),document.body.appendChild(popup);const rect=cardEl.getBoundingClientRect(),popupWidth=260;popup.style.position="fixed",popup.style.left=Math.max(8,Math.min(rect.left,window.innerWidth-popupWidth-8))+"px",popup.style.width=popupWidth+"px";const spaceBelow=window.innerHeight-rect.bottom;spaceBelow>300||spaceBelow>rect.top?popup.style.top=rect.bottom+4+"px":popup.style.bottom=window.innerHeight-rect.top+4+"px",setTimeout(function(){function close(e){popup.contains(e.target)||(popup.remove(),document.removeEventListener("click",close))}document.addEventListener("click",close)},0),popup.querySelector(".lib-vp-input").focus()}async function _findVoiceFromSameCharacterElsewhere(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey)return null;let all=[];try{all=await clGetAll()}catch{return null}const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null,match=all.find(function(r){if(r.id===rec.id||!r.voice||String(r.name||"").trim().toLowerCase()!==nameKey)return!1;const vId=typeof r.voice=="object"?r.voice.id:r.voice;return!(!_voiceExists(vId)||bookCode&&_voiceLangCode(vId)!==bookCode)});return match?{voiceId:typeof match.voice=="object"?match.voice.id:match.voice,book:match.book}:null}function _voiceLangCode(voiceId){const m=/^([A-Za-z]{2,3})_/.exec(String(voiceId||""));return m?m[1].toUpperCase():null}async function _findVoiceByCharacterName(rec){const nameKey=String(rec.name||"").trim().toLowerCase();if(!nameKey||nameKey.length<3)return null;const hits=(window._voices||[]).filter(function(v){return v.enabled!==!1}).filter(function(v){return String(v.id||v.name||"").toLowerCase().includes(nameKey)});if(!hits.length)return null;const bookLang=await _resolveBookLang(rec),bookCode=bookLang&&typeof DESIGN_LANG_CODE!="undefined"?DESIGN_LANG_CODE[bookLang]:null;if(bookCode){const langHits=hits.filter(function(v){return _voiceLangCode(v.id)===bookCode});return langHits.length?langHits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]:null}return hits.sort(function(a,b){return String(b.id).length-String(a.id).length})[0]}async function _autoAssignVoice(rec){const reuse=await _findVoiceFromSameCharacterElsewhere(rec);if(reuse){await clPut(Object.assign({},rec,{voice:reuse.voiceId,updated:new Date})),rec.voice=reuse.voiceId,_syncVoicePictureFromChar(rec),toast(reuse.voiceId+" \u2192 "+rec.name+' (reused from "'+reuse.book+'" for series consistency)',"success");return}const named=await _findVoiceByCharacterName(rec);if(named){await clPut(Object.assign({},rec,{voice:named.id,updated:new Date})),rec.voice=named.id,_syncVoicePictureFromChar(rec),toast(named.id+" \u2192 "+rec.name+" (matching voice already in the library)","success");return}if(typeof _charAutoDesignVoice=="function"){await _charAutoDesignVoice(rec);return}toast("No matching voice found","error")}function _charLang(rec){const sh=rec.sheet||{},text=[sh.backstory,sh.voice_pattern,sh.mannerisms,sh.relationships,sh.motivation,sh.archetype].filter(Boolean).join(" ");return typeof detectLang=="function"?detectLang(text):""}const _bookProfileCache=new Map;async function _getBookProfile(book){const key=String(book||"").trim();if(!key)return{};if(_bookProfileCache.has(key))return _bookProfileCache.get(key);let profile={};try{const r=await fetch("/api/book-profile?book="+encodeURIComponent(key));r.ok&&(profile=await r.json())}catch(e){console.warn("[book profile]",e)}return _bookProfileCache.set(key,profile),profile}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>>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='
Book context \u2014 '+escHtml(book)+`

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

',document.body.appendChild(ov);const close=function(){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='
Existing voice found for '+escHtml(rec.name)+'

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

',document.body.appendChild(ov);let audioEl=null;ov.querySelector("#cvr-play").addEventListener("click",async function(e){const btn=e.currentTarget,icon=btn.querySelector(".mdi");if(audioEl&&!audioEl.paused){audioEl.pause(),icon.className="mdi mdi-play";return}btn.disabled=!0,icon.className="mdi mdi-loading mdi-spin";try{const langHint=typeof _resolveBookLang=="function"?await _resolveBookLang(rec).catch(function(){return""}):"",text=typeof _charSampleTextFor=="function"&&_charSampleTextFor(rec,langHint)||"Hallo, ich bin "+rec.name+".",rv=(window._voices||[]).find(x=>x.id===reuse.voiceId),rBackend=rv&&(rv.origin==="designed"||!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='
Voice design prompt \u2014 '+escHtml(rec.name)+'

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

',document.body.appendChild(ov);const close=function(){ov.remove()};ov.addEventListener("click",function(e){e.target===ov&&close()}),ov.querySelector("#cdi-cancel").addEventListener("click",close),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 prompt=hasExplicitPrompt?_libStr(sh.image_prompt).trim():typeof csBuildImagePrompt=="function"?csBuildImagePrompt(sh):"";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=''+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"),_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))}(_sc=document.getElementById("stu-mode-audiobook"))==null||_sc.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)}const details=document.getElementById("stu-mecast-details");details&&(details.hidden=audiobookMode)});let _stuCastView="identify";function _stuShowCastView(view){_stuCastView=view,document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(t){t.classList.toggle("active",t.dataset.stuCastView===view)});const identifySlot=document.getElementById("stu-cast-slot"),sheetsSlot=document.getElementById("stu-castchars-slot");if(identifySlot&&(identifySlot.hidden=view!=="identify"),sheetsSlot&&(sheetsSlot.hidden=view!=="sheets"),view==="identify")_stuBorrow("reader-audiobook-panel","stu-cast-slot"),_stuCallSuppressingNav(function(){if(typeof window.audiobookOpenCastView=="function")return window.audiobookOpenCastView();typeof window.showReaderView=="function"&&window.showReaderView("cast")}),_stuRelocateCastFoot();else if(view==="sheets"){_stuBorrow("reader-charsheets-panel","stu-castchars-slot");const panel=document.getElementById("reader-charsheets-panel");if(panel&&!panel.innerHTML.trim()){panel.innerHTML='
Character sheets

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

';const goBtn=document.getElementById("stu-goto-cast-menu");goBtn&&goBtn.addEventListener("click",function(){typeof window.csForReader=="function"&&window.csForReader()})}}}document.querySelectorAll("#stu-cast-inner-tabs .stu-inner-tab").forEach(function(tab){tab.addEventListener("click",function(){_stuShowCastView(tab.dataset.stuCastView)})});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))})}),(_tc=document.getElementById("stu-phase-prev"))==null||_tc.addEventListener("click",function(){_stuActive>1&&showStudioPhase(_stuActive-1)}),(_uc=document.getElementById("stu-phase-next"))==null||_uc.addEventListener("click",function(){_stuActive<4&&showStudioPhase(_stuActive+1)});function studioOnShow(){_stuInstallNavGuardOnce(),_stuIsActive=!0,showStudioPhase(_stuActive)}window.studioOnShow=studioOnShow; diff --git a/static/index.html b/static/index.html index 9bc0852..57f2105 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/library-characters.js b/static/js/library-characters.js index 56f3f45..207dc75 100644 --- a/static/js/library-characters.js +++ b/static/js/library-characters.js @@ -2714,6 +2714,11 @@ async function _charAutoDesignVoice(rec, force, instructOverride) { tag: rec.book || undefined, transcript: sampleText, note: 'Voice Design: ' + instruct.slice(0, 240), + // For a designed voice the instruct IS the voice's identity — the TTS + // engine reproduces it from this text alone. `note` is a short display + // summary and is deliberately clipped, so it cannot be the source of + // truth; store the prompt in full here. + voice_design_prompt: instruct, }).catch(function () {}); } diff --git a/static/js/voice-clone.js b/static/js/voice-clone.js index 596e475..0362c6d 100644 --- a/static/js/voice-clone.js +++ b/static/js/voice-clone.js @@ -898,6 +898,10 @@ $('design-save-btn').addEventListener('click', async () => { flag: LANG_FLAG_DEFAULT[$('d-lang').value] || undefined, transcript: $('d-transcript').value, note: 'Voice Design: ' + $('design-instruct').value.slice(0, 240), + origin: 'designed', + // `note` is a clipped display summary; the engine needs the whole + // prompt, since for a designed voice the instruct is the identity. + voice_design_prompt: $('design-instruct').value, }).catch(()=>{}); await loadVoiceLibrary().catch(()=>{}); $('design-save-result').style.display='flex';