diff --git a/static/css/voice-design.css b/static/css/voice-design.css index d375d34..3664ad4 100644 --- a/static/css/voice-design.css +++ b/static/css/voice-design.css @@ -56,3 +56,16 @@ .design-chip { min-height: 44px; min-width: 44px; padding: 0 14px; border-radius: 999px; border: 1px solid var(--border); background: var(--surface); color: var(--text); font: inherit; font-size: 13px; cursor: pointer; } .design-chip[aria-pressed="true"] { background: var(--accent); border-color: var(--accent); color: #fff; } .design-chip:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; } + +/* Voice archetype gallery */ +.arch-bar { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; } +.arch-bar input, .arch-bar select { min-height: 44px; flex: 1 1 200px; } +.arch-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 10px; margin: 10px 0; } +.arch-card { border: 1px solid var(--border); border-radius: 10px; padding: 12px; background: var(--surface); display: flex; flex-direction: column; gap: 6px; } +.arch-card h3 { margin: 0; font-size: 15px; } +.arch-card p { margin: 0; font-size: 12px; color: var(--subtext); } +.arch-facets { font-size: 12px; color: var(--subtext); } +.arch-actions { display: flex; flex-wrap: wrap; gap: 6px; margin-top: auto; } +.arch-actions button { min-height: 44px; min-width: 44px; } +.arch-fav[aria-pressed="true"] { color: var(--accent); } +@media (prefers-reduced-motion: reduce) { .arch-card, .arch-actions button { transition: none; animation: none; } } diff --git a/static/dist/main.min.js b/static/dist/main.min.js index bec0fe1..74ef556 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -456,7 +456,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 suggestedStyleVoiceId2(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=>["change","input"].forEach(ev=>$(id).addEventListener(ev,_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=loadDesignPresets2();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=loadDesignPresets2();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)}));const DESIGN_CHIPS=[["Gender",!0,["male voice","female voice","neutral voice"]],["Age",!0,["child","teenager","young adult","middle-aged","elderly"]],["Pitch",!0,["high pitch","mid pitch","low pitch"]],["Pace",!0,["fast pace","moderate pace","slow pace"]],["Accent / region",!1,["British accent","American accent","Australian accent","Irish accent","German accent","Southern accent"]],["Tone / timbre",!1,["warm","husky","crisp","smooth","rich","magnetic","authoritative"]],["Emotion",!1,["cheerful","calm","serious","gentle","enthusiastic","sad"]]],_chipRe=p=>new RegExp("(^|[,.;]\\s*)"+p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(?=\\s*(?:[,.;]|$))","i"),chipHas=(text,p)=>_chipRe(p).test(text);function chipRemove(text,p){return text.replace(_chipRe(p),"").replace(/^\s*[,;]\s*/,"")}function chipAdd(text,p){const t=text.trimEnd();return t?/[.,;]$/.test(t)?t+" "+p:t+", "+p:p}function syncDesignChips(){const v=$("design-instruct").value;document.querySelectorAll("#design-chips [data-phrase]").forEach(b=>b.setAttribute("aria-pressed",String(chipHas(v,b.dataset.phrase))))}function renderDesignChips(){const tr=window.t||(x=>x),box=$("design-chips");DESIGN_CHIPS.forEach(([label,exclusive,phrases],i)=>{const g=document.createElement("div");g.className="design-chip-group",g.setAttribute("role","group"),g.setAttribute("aria-label",tr(label)),g.innerHTML='',g.firstChild.textContent=tr(label),phrases.forEach(p=>{const b=document.createElement("button");b.type="button",b.className="design-chip",b.dataset.phrase=p,b.dataset.group=i,b.setAttribute("aria-pressed","false"),b.textContent=tr(p),g.appendChild(b)}),box.appendChild(g)}),box.addEventListener("click",e=>{const b=e.target.closest("[data-phrase]");if(!b)return;const ta=$("design-instruct");let v=ta.value;chipHas(v,b.dataset.phrase)?v=chipRemove(v,b.dataset.phrase):(DESIGN_CHIPS[b.dataset.group][1]&&DESIGN_CHIPS[b.dataset.group][2].forEach(o=>{v=chipRemove(v,o)}),v=chipAdd(v,b.dataset.phrase)),ta.value=v,ta.dispatchEvent(new Event("input",{bubbles:!0}))}),box.addEventListener("focusin",syncDesignChips),box.addEventListener("pointerenter",syncDesignChips),$("design-instruct").addEventListener("input",syncDesignChips),syncDesignChips()}renderDesignChips(),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();designedFileId2=d.id,trimmedFileId2=null,editingVoiceId2=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(!designedFileId2){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:designedFileId2,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(!designedFileId2)return;const a=document.createElement("a");a.href="/api/audio/"+designedFileId2,a.download=($("d-voice-id").value.trim()||"voice_design")+".wav",a.click()}),(_d=$("clone-refresh-stt-btn"))==null||_d.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=trimmedFileId2||designedFileId2||currentFileId2;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=>["change","input"].forEach(ev=>{var _a3;return(_a3=g(id))==null?void 0:_a3.addEventListener(ev,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 trimmedFileId2!="undefined"&&trimmedFileId2||typeof designedFileId2!="undefined"&&designedFileId2)&&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")}(),Object.assign(root,{CLONE_SAMPLE_TEXTS,CLONE_SAMPLE_NAMES,cloneSampleForLang,ws,wsRegions,currentFileId:currentFileId2,trimmedFileId:trimmedFileId2,designedFileId:designedFileId2,editingVoiceId:editingVoiceId2,editingVoicePath:editingVoicePath2,initWaveSurfer,updateRegion,updateDurationLabel,loadAudioId:loadAudioId2,dropZone,fileInput,uploadFile,loadLibraryVoiceAudio:loadLibraryVoiceAudio2,RAW_MIC_CONSTRAINTS,visibleMicrophoneCount,microphoneErrorMessage:microphoneErrorMessage2,requestMicrophoneStream:requestMicrophoneStream2,_cloneMonState,_cloneRenderMeter,_cloneStartMeter,_cloneStartMonitor,_cloneStopMonitor,mediaRec,recChunks,recTimer,recSecs,DESIGN_LANG_CODE,DESIGN_GENDER_WORD,DESIGN_PRESET_KEY,DESIGN_PRESET_SEEDED_KEY,DEFAULT_DESIGN_PRESETS,QWEN_DESIGN_SAMPLES,currentDesignSource,loadDesignPresets:loadDesignPresets2,saveDesignPresets,syncDesignPresetsToServer,seedDesignPresets,refreshDesignPresetSelect,applyDesignPreset,renderDesignPresetLibrary,renderQwenSampleCards,applyQwenSample,isDialogueDesign,voiceDesignPayload,_dVoiceIdManual,designSafeName,voiceIdSafePart,suggestedStyleVoiceId:suggestedStyleVoiceId2,_updateDVoiceId,runVoiceDesign}),root.TTVC=root.TTVC||{},root.TTVC.voiceClone=root.TTVC.voiceClone||{},Object.assign(root.TTVC.voiceClone,{samples:{forLanguage:cloneSampleForLang},audio:{init:initWaveSurfer,load:loadAudioId2,upload:uploadFile},design:{run:runVoiceDesign,buildPayload:voiceDesignPayload}})})(window),setupVocalIsolation({btn:"yt-iso-btn",toggle:"yt-iso-toggle",msg:"yt-iso-msg",getId:()=>currentFileId,getDur:()=>parseFloat($("trim-end").value)||0,apply:(id,dur)=>loadAudioId(id,dur)}),function(root){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r;$("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 _voices2=[],_pendingSelectId2=null,_sortField="id",_sortDir=1,_libraryIssueFilter="",_activePlayButton=null,_activePlayVoiceId=null,_activePlayUrl=null,_libraryLoadPromise2=null;const BENCHMARK_SAMPLE_STORAGE_KEY="vcf-benchmark-sample-text",_VL_CACHE_KEY="ttsvc_vc";function _vlCacheRead(){try{const value=JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY)||"null");return Array.isArray(value)?value:Array.isArray(value==null?void 0:value.voices)?value.voices: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(),renderVoiceList2()}function toggleSortDir(){_sortDir*=-1,syncSortHeaders(),renderVoiceList2()}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"),renderVoiceList2()}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.

')}renderVoiceList2()}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),renderVoiceList2()}});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 saveMeta2(voiceId,payload);const v=_voices2.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_LABELS2={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_LABELS2[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;(_voices2||[]).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))return!1}return!0}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(),renderVoiceList2()}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 fmtDbfs2(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);if(f!=null)return f.toFixed(2)+"x";const b=voiceBenchmark(v);return b&&!b.ok?"ERR":"-"}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 fmtBenchmark2(v){const elapsed=fmtElapsed(v),factor=fmtFactor(v);return elapsed==="-"&&factor==="-"?"-":[elapsed,factor].filter(x=>x!=="-").join(" \xB7 ")}function benchmarkClass2(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 voiceFileUrl2(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 benchmarkTitle2(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(voiceFileUrl2(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=_bulkSelected2&&_bulkSelected2.size>0?(_voices2||[]).filter(v=>_bulkSelected2.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 saveMeta2(v.id,{loudness:v.loudness}).catch(()=>{}),calculated++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs2(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=voiceFileUrl2(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(_voices2||[]).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=_voices2.length,active=_voices2.filter(v=>v.enabled!==!1).length,hidden=total-active,bench=_voices2.map(voiceBenchmark).filter(Boolean),slow=bench.filter(b=>b&&b.ok&&b.realtime_ok===!1).length,dbValues=_voices2.map(voiceDbfs).filter(v=>v!=null),avgDb=dbValues.length?(dbValues.reduce((a,b)=>a+b,0)/dbValues.length).toFixed(1):"-",missingRef=_voices2.filter(v=>!v.transcript).length,restart=_voices2.filter(v=>v.needs_tts_restart).length,tiles=[{value:`${_voices2.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 loadVoiceLibrary2(options={}){const forceRefresh=!!(options&&options.refresh);return _libraryLoadPromise2?_libraryLoadPromise2.then(()=>{shouldRenderVoiceLibrary()&&_voices2.length&&renderVoiceList2()}):(_libraryLoadPromise2=(async()=>{setBusyButton("refresh-voices-btn",!0);const list=$("voice-list"),renderVisibleList=shouldRenderVoiceLibrary(),cached=_voices2.length===0?_vlCacheRead():null;cached&&Array.isArray(cached)&&cached.length&&(_voices2=cached,window._voices=_voices2,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices2),renderVisibleList&&renderVoiceList2(),updatePreviewVoiceMatchPanel(),status(`Loaded ${_voices2.length} voices`));const silent=_voices2.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 fresh=typeof window.fetchVoiceLibrary=="function"?await window.fetchVoiceLibrary({refresh:forceRefresh}):await fetch("/api/voices"+(forceRefresh?"?refresh=1":"")).then(async r=>{if(!r.ok)throw new Error(r.statusText);return r.json()});_vlCacheWrite(fresh),_voices2=fresh,window._voices=_voices2,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices2),shouldRenderVoiceLibrary()&&renderVoiceList2(),updatePreviewVoiceMatchPanel(),typeof renderPerfHistory=="function"&&renderPerfHistory(),status(`Loaded ${_voices2.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),_libraryLoadPromise2=null}})(),_libraryLoadPromise2)}$("refresh-voices-btn").addEventListener("click",()=>{_vlCacheClear(),loadVoiceLibrary2({refresh:!0})});async function exportVoiceBundles(ids){let ok=0;for(const id of ids)try{const r=await fetch("/api/voices/"+encodeURIComponent(id)+"/export");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const url=URL.createObjectURL(await r.blob()),a=document.createElement("a");a.href=url,a.download=id+".voice.zip",document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(url),1e4),ok++}catch(e){toast(`Export of ${id} failed: ${e.message}`,"error")}return ok&&toast(`Exported ${ok} voice bundle${ok===1?"":"s"}`,"success"),ok}async function importVoiceBundles(files){const msgs=[];let failed=0;for(const f of files)try{const fd=new FormData;fd.append("file",f);const r=await fetch("/api/voices/import",{method:"POST",body:fd}),d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.detail||r.statusText);msgs.push(d.status==="unchanged"?`${d.voice_id} already present`:d.status==="renamed"?`${d.voice_id} (name was taken)`:d.voice_id)}catch(e){failed++,toast(`Import of ${f.name} failed: ${e.message}`,"error")}return msgs.length&&(typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),await loadVoiceLibrary2({refresh:!0}),toast("Imported: "+msgs.join(", "),failed?"error":"success")),msgs.length}window.exportVoiceBundles=exportVoiceBundles,window.importVoiceBundles=importVoiceBundles,$("export-voices-btn").addEventListener("click",async()=>{if(!_bulkSelected2.size){toast("Check one or more voices first","error");return}$("export-voices-btn").disabled=!0;try{await exportVoiceBundles([..._bulkSelected2])}finally{$("export-voices-btn").disabled=!1}}),$("import-voices-btn").addEventListener("click",()=>$("import-voices-input").click()),$("import-voices-input").addEventListener("change",async ev=>{const files=[...ev.target.files];if(ev.target.value="",!!files.length){$("import-voices-btn").disabled=!0;try{await importVoiceBundles(files)}finally{$("import-voices-btn").disabled=!1}}}),$("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();typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),await loadVoiceLibrary2({refresh:!0});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 _voices2.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,renderVoiceList2(),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=_bulkSelected2&&_bulkSelected2.size>0?`${_bulkSelected2.size} selected`:"visible";status(`Calculating voice loudness (${_calcTarget})\u2026`);try{const d=await clientCalculateVoiceDb();renderVoiceList2();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}: ${fmtDbfs2(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),renderVoiceList2(),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 clearTtsRestartFlags2(){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 _voices2.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);let lastFailure="",sameFailure=0,aborted=!1;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=benchmarkClass2(voice),btitle=benchmarkTitle2(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:""}`;const reason=b&&b.error?String(b.error):"";sameFailure=reason&&reason===lastFailure?sameFailure+1:1,lastFailure=reason,/unreachable/i.test(reason)&&sameFailure>=3&&(toast(reason,"error"),aborted=!0)}}}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]));_voices2.forEach(v=>{const hit=byId.get(v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark)})}window.loadVoiceLibrary=loadVoiceLibrary2,window.mergeBenchmarkResults=mergeBenchmarkResults;function activeBenchmarkVoices(){return(_voices2||[]).filter(v=>v.enabled!==!1)}function benchmarkTargetVoices(){return typeof _bulkSelected2!="undefined"&&_bulkSelected2.size>0?(_voices2||[]).filter(v=>_bulkSelected2.has(v.id)):activeBenchmarkVoices()}function showBenchmarkConfirm(){const voices=benchmarkTargetVoices(),onlySelected=typeof _bulkSelected2!="undefined"&&_bulkSelected2.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),(_a=$("benchmark-confirm-cancel"))==null||_a.addEventListener("click",hideBenchmarkConfirm),(_b=$("benchmark-confirm-start"))==null||_b.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 loadVoiceLibrary2();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=_bulkSelected2&&_bulkSelected2.size>0,ids=useSelected?[..._bulkSelected2]: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}(_c=$("verify-voices-stt-btn"))==null||_c.addEventListener("click",async()=>{const ids=_bulkSelected2&&_bulkSelected2.size>0?[..._bulkSelected2]: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 suggestedStyleVoiceId2(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=>["change","input"].forEach(ev=>$(id).addEventListener(ev,_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=loadDesignPresets2();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=loadDesignPresets2();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)}));const DESIGN_CHIPS=[["Gender",!0,["male voice","female voice","neutral voice"]],["Age",!0,["child","teenager","young adult","middle-aged","elderly"]],["Pitch",!0,["high pitch","mid pitch","low pitch"]],["Pace",!0,["fast pace","moderate pace","slow pace"]],["Accent / region",!1,["British accent","American accent","Australian accent","Irish accent","German accent","Southern accent"]],["Tone / timbre",!1,["warm","husky","crisp","smooth","rich","magnetic","authoritative"]],["Emotion",!1,["cheerful","calm","serious","gentle","enthusiastic","sad"]]],_chipRe=p=>new RegExp("(^|[,.;]\\s*)"+p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(?=\\s*(?:[,.;]|$))","i"),chipHas=(text,p)=>_chipRe(p).test(text);function chipRemove(text,p){return text.replace(_chipRe(p),"").replace(/^\s*[,;]\s*/,"")}function chipAdd(text,p){const t=text.trimEnd();return t?/[.,;]$/.test(t)?t+" "+p:t+", "+p:p}function syncDesignChips(){const v=$("design-instruct").value;document.querySelectorAll("#design-chips [data-phrase]").forEach(b=>b.setAttribute("aria-pressed",String(chipHas(v,b.dataset.phrase))))}function renderDesignChips(){const tr=window.t||(x=>x),box=$("design-chips");DESIGN_CHIPS.forEach(([label,exclusive,phrases],i)=>{const g=document.createElement("div");g.className="design-chip-group",g.setAttribute("role","group"),g.setAttribute("aria-label",tr(label)),g.innerHTML='',g.firstChild.textContent=tr(label),phrases.forEach(p=>{const b=document.createElement("button");b.type="button",b.className="design-chip",b.dataset.phrase=p,b.dataset.group=i,b.setAttribute("aria-pressed","false"),b.textContent=tr(p),g.appendChild(b)}),box.appendChild(g)}),box.addEventListener("click",e=>{const b=e.target.closest("[data-phrase]");if(!b)return;const ta=$("design-instruct");let v=ta.value;chipHas(v,b.dataset.phrase)?v=chipRemove(v,b.dataset.phrase):(DESIGN_CHIPS[b.dataset.group][1]&&DESIGN_CHIPS[b.dataset.group][2].forEach(o=>{v=chipRemove(v,o)}),v=chipAdd(v,b.dataset.phrase)),ta.value=v,ta.dispatchEvent(new Event("input",{bubbles:!0}))}),box.addEventListener("focusin",syncDesignChips),box.addEventListener("pointerenter",syncDesignChips),$("design-instruct").addEventListener("input",syncDesignChips),syncDesignChips()}renderDesignChips(),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();designedFileId2=d.id,trimmedFileId2=null,editingVoiceId2=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(!designedFileId2){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:designedFileId2,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(!designedFileId2)return;const a=document.createElement("a");a.href="/api/audio/"+designedFileId2,a.download=($("d-voice-id").value.trim()||"voice_design")+".wav",a.click()}),(_d=$("clone-refresh-stt-btn"))==null||_d.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=trimmedFileId2||designedFileId2||currentFileId2;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=>["change","input"].forEach(ev=>{var _a3;return(_a3=g(id))==null?void 0:_a3.addEventListener(ev,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 trimmedFileId2!="undefined"&&trimmedFileId2||typeof designedFileId2!="undefined"&&designedFileId2)&&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")}(),Object.assign(root,{CLONE_SAMPLE_TEXTS,CLONE_SAMPLE_NAMES,cloneSampleForLang,ws,wsRegions,currentFileId:currentFileId2,trimmedFileId:trimmedFileId2,designedFileId:designedFileId2,editingVoiceId:editingVoiceId2,editingVoicePath:editingVoicePath2,initWaveSurfer,updateRegion,updateDurationLabel,loadAudioId:loadAudioId2,dropZone,fileInput,uploadFile,loadLibraryVoiceAudio:loadLibraryVoiceAudio2,RAW_MIC_CONSTRAINTS,visibleMicrophoneCount,microphoneErrorMessage:microphoneErrorMessage2,requestMicrophoneStream:requestMicrophoneStream2,_cloneMonState,_cloneRenderMeter,_cloneStartMeter,_cloneStartMonitor,_cloneStopMonitor,mediaRec,recChunks,recTimer,recSecs,DESIGN_LANG_CODE,DESIGN_GENDER_WORD,DESIGN_PRESET_KEY,DESIGN_PRESET_SEEDED_KEY,DEFAULT_DESIGN_PRESETS,QWEN_DESIGN_SAMPLES,currentDesignSource,loadDesignPresets:loadDesignPresets2,saveDesignPresets,syncDesignPresetsToServer,seedDesignPresets,refreshDesignPresetSelect,applyDesignPreset,renderDesignPresetLibrary,renderQwenSampleCards,applyQwenSample,isDialogueDesign,voiceDesignPayload,_dVoiceIdManual,designSafeName,voiceIdSafePart,suggestedStyleVoiceId:suggestedStyleVoiceId2,_updateDVoiceId,runVoiceDesign}),root.TTVC=root.TTVC||{},root.TTVC.voiceClone=root.TTVC.voiceClone||{},Object.assign(root.TTVC.voiceClone,{samples:{forLanguage:cloneSampleForLang},audio:{init:initWaveSurfer,load:loadAudioId2,upload:uploadFile},design:{run:runVoiceDesign,buildPayload:voiceDesignPayload}})})(window),setupVocalIsolation({btn:"yt-iso-btn",toggle:"yt-iso-toggle",msg:"yt-iso-msg",getId:()=>currentFileId,getDur:()=>parseFloat($("trim-end").value)||0,apply:(id,dur)=>loadAudioId(id,dur)}),function(){const det=document.getElementById("archetype-gallery");if(!det)return;const tr=s=>window.t?window.t(s):s,$a=id=>document.getElementById(id),PAGE=24,FAV_KEY="ttsvc-archetype-favs",FACETS=[["gender","Gender",["male","female","neutral"]],["age","Age",["child","teenager","young adult","middle-aged","elderly"]],["pitch","Pitch",["high","mid","low"]],["accent","Accent / region",["none","british","american","australian","irish","german","southern"]],["language","Language",["German","English"]]],st={gender:"",age:"",pitch:"",accent:"",language:"",category:"",q:"",favOnly:!1,offset:0,total:0};let favs=new Set,booted=!1,token=0,objUrl="";try{favs=new Set(JSON.parse(localStorage.getItem(FAV_KEY)||"[]"))}catch{}const saveFavs=()=>{try{localStorage.setItem(FAV_KEY,JSON.stringify([...favs]))}catch{}},say=m=>{$a("arch-status").textContent=m},lang=()=>window.__APP_LANG__==="de"?"de":"en";function buildChips(){const box=$a("arch-chips");FACETS.forEach(([key,label,vals])=>{const g=document.createElement("div");g.className="design-chip-group",g.setAttribute("role","group"),g.setAttribute("aria-label",tr(label));const l=document.createElement("span");l.className="design-chip-label",l.textContent=tr(label),g.appendChild(l),vals.forEach(v=>{const b=document.createElement("button");b.type="button",b.className="design-chip",b.dataset.k=key,b.dataset.v=v,b.setAttribute("aria-pressed","false"),b.textContent=tr(v),g.appendChild(b)}),box.appendChild(g)});const f=document.createElement("button");f.type="button",f.className="design-chip",f.id="arch-fav-only",f.setAttribute("aria-pressed","false"),f.textContent=tr("Favourites only"),box.appendChild(f),box.addEventListener("click",e=>{const b=e.target.closest("button");b&&(b.id==="arch-fav-only"?st.favOnly=!st.favOnly:st[b.dataset.k]=st[b.dataset.k]===b.dataset.v?"":b.dataset.v,box.querySelectorAll("[data-k]").forEach(x=>x.setAttribute("aria-pressed",String(st[x.dataset.k]===x.dataset.v))),f.setAttribute("aria-pressed",String(st.favOnly)),load(!0))})}function card(a){const name=lang()==="de"?a.name_de:a.name_en,el=document.createElement("article");el.className="arch-card",el.dataset.id=a.id;const h=document.createElement("h3");h.textContent=name;const f=document.createElement("div");f.className="arch-facets",f.textContent=[a.language,...Object.values(a.facets)].map(tr).join(" \xB7 ");const p=document.createElement("p");p.textContent=a.category;const row=document.createElement("div");row.className="arch-actions";const mk=(cls,text,label)=>{const b=document.createElement("button");return b.type="button",b.className=cls,b.textContent=text,label&&b.setAttribute("aria-label",label+": "+name),row.appendChild(b),b};return mk("btn-primary arch-play",tr("Preview"),tr("Preview")),mk("btn-secondary arch-use",tr("Use voice"),tr("Use voice")),mk("btn-secondary arch-fav",favs.has(a.id)?"\u2605":"\u2606",tr("Favourite")).setAttribute("aria-pressed",String(favs.has(a.id))),el.append(h,f,p,row),el}async function load(reset){const my=++token;reset&&(st.offset=0,$a("arch-grid").textContent="");const q=new URLSearchParams({offset:st.offset,limit:st.favOnly?100:PAGE});["gender","age","pitch","accent","language","category","q"].forEach(k=>st[k]&&q.set(k,st[k])),say(tr("Loading\u2026"));try{const r=await fetch("/api/archetypes?"+q);if(!r.ok)throw new Error(r.statusText);const d=await r.json();if(my!==token)return;if(!$a("arch-category").options.length){const sel=$a("arch-category");[["",tr("All categories")],...d.categories.map(c=>[c,c])].forEach(([v,t])=>sel.add(new Option(t,v)))}const items=st.favOnly?d.items.filter(a=>favs.has(a.id)):d.items,grid=$a("arch-grid");items.forEach(a=>grid.appendChild(card(a))),st.offset+=d.items.length,st.total=d.total,$a("arch-more").hidden=st.favOnly||st.offset>=d.total,say(grid.children.length?grid.children.length+" / "+(st.favOnly?grid.children.length:d.total):tr("No voices match."))}catch{my===token&&say(tr("Could not load the gallery."))}}async function play(a,btn){btn.disabled=!0,say(tr("Rendering preview\u2026"));try{const r=await fetch("/api/archetypes/"+encodeURIComponent(a)+"/preview",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}objUrl&&URL.revokeObjectURL(objUrl),objUrl=URL.createObjectURL(await r.blob());const au=$a("arch-audio");au.src=objUrl,await au.play().catch(()=>{}),say(tr("Preview ready"))}catch(e){say(tr("Preview failed")+": "+e.message)}finally{btn.disabled=!1}}async function use(id,btn){btn.disabled=!0,say(tr("Saving voice\u2026"));try{const r=await fetch("/api/archetypes/"+encodeURIComponent(id)+"/use",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();if(say(tr("Voice saved")+": "+d.voice_id),typeof toast=="function"&&toast(tr("Voice saved")+": "+d.voice_id,"success"),!btn.parentNode.querySelector(".arch-open")){const o=document.createElement("button");o.type="button",o.className="btn-secondary arch-open",o.textContent=tr("Open in Design"),o.dataset.instruct=d.instruct,o.dataset.language=d.language,btn.parentNode.insertBefore(o,btn.nextSibling),o.focus()}typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().catch(()=>{})}catch(e){say(tr("Save failed")+": "+e.message),typeof toast=="function"&&toast(e.message,"error")}finally{btn.disabled=!1}}function boot(){if(booted)return;booted=!0,buildChips();let t;$a("arch-q").addEventListener("input",e=>{clearTimeout(t),t=setTimeout(()=>{st.q=e.target.value.trim(),load(!0)},250)}),$a("arch-category").addEventListener("change",e=>{st.category=e.target.value,load(!0)}),$a("arch-more").addEventListener("click",()=>load(!1)),$a("arch-grid").addEventListener("click",e=>{const b=e.target.closest("button");if(!b)return;const id=b.closest(".arch-card").dataset.id;if(b.classList.contains("arch-play"))play(id,b);else if(b.classList.contains("arch-use"))use(id,b);else if(b.classList.contains("arch-fav"))favs.has(id)?favs.delete(id):favs.add(id),saveFavs(),b.textContent=favs.has(id)?"\u2605":"\u2606",b.setAttribute("aria-pressed",String(favs.has(id)));else if(b.classList.contains("arch-open")){const ta=$a("design-instruct");ta.value=b.dataset.instruct,ta.dispatchEvent(new Event("input",{bubbles:!0}));const sel=$a("d-lang");if(sel)for(const o of sel.options)o.value===b.dataset.language&&(sel.value=o.value);ta.scrollIntoView({behavior:matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth",block:"center"}),ta.focus()}}),load(!0)}det.addEventListener("toggle",()=>{det.open&&boot()}),det.open&&boot()}(),function(root){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r;$("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 _voices2=[],_pendingSelectId2=null,_sortField="id",_sortDir=1,_libraryIssueFilter="",_activePlayButton=null,_activePlayVoiceId=null,_activePlayUrl=null,_libraryLoadPromise2=null;const BENCHMARK_SAMPLE_STORAGE_KEY="vcf-benchmark-sample-text",_VL_CACHE_KEY="ttsvc_vc";function _vlCacheRead(){try{const value=JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY)||"null");return Array.isArray(value)?value:Array.isArray(value==null?void 0:value.voices)?value.voices: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(),renderVoiceList2()}function toggleSortDir(){_sortDir*=-1,syncSortHeaders(),renderVoiceList2()}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"),renderVoiceList2()}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.

')}renderVoiceList2()}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),renderVoiceList2()}});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 saveMeta2(voiceId,payload);const v=_voices2.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_LABELS2={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_LABELS2[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;(_voices2||[]).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))return!1}return!0}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(),renderVoiceList2()}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 fmtDbfs2(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);if(f!=null)return f.toFixed(2)+"x";const b=voiceBenchmark(v);return b&&!b.ok?"ERR":"-"}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 fmtBenchmark2(v){const elapsed=fmtElapsed(v),factor=fmtFactor(v);return elapsed==="-"&&factor==="-"?"-":[elapsed,factor].filter(x=>x!=="-").join(" \xB7 ")}function benchmarkClass2(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 voiceFileUrl2(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 benchmarkTitle2(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(voiceFileUrl2(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=_bulkSelected2&&_bulkSelected2.size>0?(_voices2||[]).filter(v=>_bulkSelected2.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 saveMeta2(v.id,{loudness:v.loudness}).catch(()=>{}),calculated++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs2(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=voiceFileUrl2(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(_voices2||[]).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=_voices2.length,active=_voices2.filter(v=>v.enabled!==!1).length,hidden=total-active,bench=_voices2.map(voiceBenchmark).filter(Boolean),slow=bench.filter(b=>b&&b.ok&&b.realtime_ok===!1).length,dbValues=_voices2.map(voiceDbfs).filter(v=>v!=null),avgDb=dbValues.length?(dbValues.reduce((a,b)=>a+b,0)/dbValues.length).toFixed(1):"-",missingRef=_voices2.filter(v=>!v.transcript).length,restart=_voices2.filter(v=>v.needs_tts_restart).length,tiles=[{value:`${_voices2.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 loadVoiceLibrary2(options={}){const forceRefresh=!!(options&&options.refresh);return _libraryLoadPromise2?_libraryLoadPromise2.then(()=>{shouldRenderVoiceLibrary()&&_voices2.length&&renderVoiceList2()}):(_libraryLoadPromise2=(async()=>{setBusyButton("refresh-voices-btn",!0);const list=$("voice-list"),renderVisibleList=shouldRenderVoiceLibrary(),cached=_voices2.length===0?_vlCacheRead():null;cached&&Array.isArray(cached)&&cached.length&&(_voices2=cached,window._voices=_voices2,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices2),renderVisibleList&&renderVoiceList2(),updatePreviewVoiceMatchPanel(),status(`Loaded ${_voices2.length} voices`));const silent=_voices2.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 fresh=typeof window.fetchVoiceLibrary=="function"?await window.fetchVoiceLibrary({refresh:forceRefresh}):await fetch("/api/voices"+(forceRefresh?"?refresh=1":"")).then(async r=>{if(!r.ok)throw new Error(r.statusText);return r.json()});_vlCacheWrite(fresh),_voices2=fresh,window._voices=_voices2,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices2),shouldRenderVoiceLibrary()&&renderVoiceList2(),updatePreviewVoiceMatchPanel(),typeof renderPerfHistory=="function"&&renderPerfHistory(),status(`Loaded ${_voices2.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),_libraryLoadPromise2=null}})(),_libraryLoadPromise2)}$("refresh-voices-btn").addEventListener("click",()=>{_vlCacheClear(),loadVoiceLibrary2({refresh:!0})});async function exportVoiceBundles(ids){let ok=0;for(const id of ids)try{const r=await fetch("/api/voices/"+encodeURIComponent(id)+"/export");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const url=URL.createObjectURL(await r.blob()),a=document.createElement("a");a.href=url,a.download=id+".voice.zip",document.body.appendChild(a),a.click(),a.remove(),setTimeout(()=>URL.revokeObjectURL(url),1e4),ok++}catch(e){toast(`Export of ${id} failed: ${e.message}`,"error")}return ok&&toast(`Exported ${ok} voice bundle${ok===1?"":"s"}`,"success"),ok}async function importVoiceBundles(files){const msgs=[];let failed=0;for(const f of files)try{const fd=new FormData;fd.append("file",f);const r=await fetch("/api/voices/import",{method:"POST",body:fd}),d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.detail||r.statusText);msgs.push(d.status==="unchanged"?`${d.voice_id} already present`:d.status==="renamed"?`${d.voice_id} (name was taken)`:d.voice_id)}catch(e){failed++,toast(`Import of ${f.name} failed: ${e.message}`,"error")}return msgs.length&&(typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),await loadVoiceLibrary2({refresh:!0}),toast("Imported: "+msgs.join(", "),failed?"error":"success")),msgs.length}window.exportVoiceBundles=exportVoiceBundles,window.importVoiceBundles=importVoiceBundles,$("export-voices-btn").addEventListener("click",async()=>{if(!_bulkSelected2.size){toast("Check one or more voices first","error");return}$("export-voices-btn").disabled=!0;try{await exportVoiceBundles([..._bulkSelected2])}finally{$("export-voices-btn").disabled=!1}}),$("import-voices-btn").addEventListener("click",()=>$("import-voices-input").click()),$("import-voices-input").addEventListener("change",async ev=>{const files=[...ev.target.files];if(ev.target.value="",!!files.length){$("import-voices-btn").disabled=!0;try{await importVoiceBundles(files)}finally{$("import-voices-btn").disabled=!1}}}),$("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();typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),await loadVoiceLibrary2({refresh:!0});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 _voices2.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,renderVoiceList2(),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=_bulkSelected2&&_bulkSelected2.size>0?`${_bulkSelected2.size} selected`:"visible";status(`Calculating voice loudness (${_calcTarget})\u2026`);try{const d=await clientCalculateVoiceDb();renderVoiceList2();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}: ${fmtDbfs2(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),renderVoiceList2(),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 clearTtsRestartFlags2(){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 _voices2.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);let lastFailure="",sameFailure=0,aborted=!1;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=benchmarkClass2(voice),btitle=benchmarkTitle2(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:""}`;const reason=b&&b.error?String(b.error):"";sameFailure=reason&&reason===lastFailure?sameFailure+1:1,lastFailure=reason,/unreachable/i.test(reason)&&sameFailure>=3&&(toast(reason,"error"),aborted=!0)}}}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]));_voices2.forEach(v=>{const hit=byId.get(v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark)})}window.loadVoiceLibrary=loadVoiceLibrary2,window.mergeBenchmarkResults=mergeBenchmarkResults;function activeBenchmarkVoices(){return(_voices2||[]).filter(v=>v.enabled!==!1)}function benchmarkTargetVoices(){return typeof _bulkSelected2!="undefined"&&_bulkSelected2.size>0?(_voices2||[]).filter(v=>_bulkSelected2.has(v.id)):activeBenchmarkVoices()}function showBenchmarkConfirm(){const voices=benchmarkTargetVoices(),onlySelected=typeof _bulkSelected2!="undefined"&&_bulkSelected2.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),(_a=$("benchmark-confirm-cancel"))==null||_a.addEventListener("click",hideBenchmarkConfirm),(_b=$("benchmark-confirm-start"))==null||_b.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 loadVoiceLibrary2();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=_bulkSelected2&&_bulkSelected2.size>0,ids=useSelected?[..._bulkSelected2]: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}(_c=$("verify-voices-stt-btn"))==null||_c.addEventListener("click",async()=>{const ids=_bulkSelected2&&_bulkSelected2.size>0?[..._bulkSelected2]: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=`
@@ -521,7 +521,6 @@ This warms each voice so the engine caches its .pt and first playback is instant ${benchText!=="-"?` ${escHtml(benchText.split(" ")[0])}`:""}
${starsHtml}
${isClone?"Clone":"Design"} - ${window.provenanceBadge?window.provenanceBadge(v):""}
diff --git a/static/js/i18n.js b/static/js/i18n.js index 276ab22..0579152 100644 --- a/static/js/i18n.js +++ b/static/js/i18n.js @@ -21,6 +21,54 @@ const I18N_LANGS = { // English for languages that don't have it yet (see window.t / applyI18n). const I18N_DICT = { de: { + // Voice archetype gallery + 'Voice gallery': 'Stimmengalerie', + '40 ready-made German-first voices. Preview, save or open in the editor below.': '40 fertige Stimmen, Deutsch zuerst. Anhören, speichern oder unten im Editor öffnen.', + 'Favourites only': 'Nur Favoriten', + 'Use voice': 'Stimme verwenden', + 'Favourite': 'Favorit', + 'Open in Design': 'In Design öffnen', + 'Loading…': 'Lädt…', + 'No voices match.': 'Keine passende Stimme.', + 'Could not load the gallery.': 'Galerie konnte nicht geladen werden.', + 'Rendering preview…': 'Vorschau wird erzeugt…', + 'Preview ready': 'Vorschau bereit', + 'Preview failed': 'Vorschau fehlgeschlagen', + 'Saving voice…': 'Stimme wird gespeichert…', + 'Voice saved': 'Stimme gespeichert', + 'Save failed': 'Speichern fehlgeschlagen', + 'All categories': 'Alle Kategorien', + 'Category': 'Kategorie', + 'Load more': 'Mehr laden', + 'Age': 'Alter', + 'Language': 'Sprache', + 'male': 'männlich', + 'female': 'weiblich', + 'neutral': 'neutral', + 'child': 'Kind', + 'teenager': 'Jugendlich', + 'young adult': 'jung', + 'middle-aged': 'mittleren Alters', + 'elderly': 'älter', + 'high': 'hoch', + 'mid': 'mittel', + 'low': 'tief', + 'none': 'ohne', + 'british': 'britisch', + 'american': 'amerikanisch', + 'australian': 'australisch', + 'irish': 'irisch', + 'german': 'deutsch', + 'southern': 'südlich', + 'German': 'Deutsch', + 'English': 'Englisch', + 'warm': 'warm', + 'husky': 'rau', + 'crisp': 'klar', + 'smooth': 'weich', + 'rich': 'voll', + 'magnetic': 'magnetisch', + 'authoritative': 'autoritär', // Seed Finder auto-pick 'Generates up to 8 candidates, scores them and highlights the best. Nothing is saved until you click Use seed.': 'Erzeugt bis zu 8 Kandidaten, bewertet sie und hebt den besten hervor. Gespeichert wird erst nach Klick auf „Seed verwenden“.', 'Auto-pick best': 'Besten automatisch wählen', diff --git a/static/js/voice-archetypes.js b/static/js/voice-archetypes.js new file mode 100644 index 0000000..3772385 --- /dev/null +++ b/static/js/voice-archetypes.js @@ -0,0 +1,153 @@ +// -- Voice archetype gallery (Design a Voice) --------------------------------- +// Lazy: nothing is fetched or rendered until the
is opened. +(function () { +const det = document.getElementById('archetype-gallery'); +if (!det) return; +const tr = s => (window.t ? window.t(s) : s); +const $a = id => document.getElementById(id); +const PAGE = 24, FAV_KEY = 'ttsvc-archetype-favs'; +const FACETS = [ + ['gender', 'Gender', ['male', 'female', 'neutral']], + ['age', 'Age', ['child', 'teenager', 'young adult', 'middle-aged', 'elderly']], + ['pitch', 'Pitch', ['high', 'mid', 'low']], + ['accent', 'Accent / region', ['none', 'british', 'american', 'australian', 'irish', 'german', 'southern']], + ['language', 'Language', ['German', 'English']], +]; +const st = { gender: '', age: '', pitch: '', accent: '', language: '', category: '', q: '', favOnly: false, offset: 0, total: 0 }; +let favs = new Set(), booted = false, token = 0, objUrl = ''; +try { favs = new Set(JSON.parse(localStorage.getItem(FAV_KEY) || '[]')); } catch (_) {} +const saveFavs = () => { try { localStorage.setItem(FAV_KEY, JSON.stringify([...favs])); } catch (_) {} }; +const say = m => { $a('arch-status').textContent = m; }; +const lang = () => (window.__APP_LANG__ === 'de' ? 'de' : 'en'); + +function buildChips() { + const box = $a('arch-chips'); + FACETS.forEach(([key, label, vals]) => { + const g = document.createElement('div'); + g.className = 'design-chip-group'; g.setAttribute('role', 'group'); g.setAttribute('aria-label', tr(label)); + const l = document.createElement('span'); l.className = 'design-chip-label'; l.textContent = tr(label); g.appendChild(l); + vals.forEach(v => { + const b = document.createElement('button'); + b.type = 'button'; b.className = 'design-chip'; b.dataset.k = key; b.dataset.v = v; + b.setAttribute('aria-pressed', 'false'); b.textContent = tr(v); + g.appendChild(b); + }); + box.appendChild(g); + }); + const f = document.createElement('button'); + f.type = 'button'; f.className = 'design-chip'; f.id = 'arch-fav-only'; f.setAttribute('aria-pressed', 'false'); + f.textContent = tr('Favourites only'); box.appendChild(f); + box.addEventListener('click', e => { + const b = e.target.closest('button'); if (!b) return; + if (b.id === 'arch-fav-only') st.favOnly = !st.favOnly; + else st[b.dataset.k] = st[b.dataset.k] === b.dataset.v ? '' : b.dataset.v; + box.querySelectorAll('[data-k]').forEach(x => x.setAttribute('aria-pressed', String(st[x.dataset.k] === x.dataset.v))); + f.setAttribute('aria-pressed', String(st.favOnly)); + load(true); + }); +} + +function card(a) { + const name = lang() === 'de' ? a.name_de : a.name_en; + const el = document.createElement('article'); + el.className = 'arch-card'; el.dataset.id = a.id; + const h = document.createElement('h3'); h.textContent = name; + const f = document.createElement('div'); f.className = 'arch-facets'; + f.textContent = [a.language, ...Object.values(a.facets)].map(tr).join(' · '); + const p = document.createElement('p'); p.textContent = a.category; + const row = document.createElement('div'); row.className = 'arch-actions'; + const mk = (cls, text, label) => { + const b = document.createElement('button'); b.type = 'button'; b.className = cls; + b.textContent = text; if (label) b.setAttribute('aria-label', label + ': ' + name); row.appendChild(b); return b; + }; + mk('btn-primary arch-play', tr('Preview'), tr('Preview')); + mk('btn-secondary arch-use', tr('Use voice'), tr('Use voice')); + const fav = mk('btn-secondary arch-fav', favs.has(a.id) ? '★' : '☆', tr('Favourite')); + fav.setAttribute('aria-pressed', String(favs.has(a.id))); + el.append(h, f, p, row); + return el; +} + +async function load(reset) { + const my = ++token; + if (reset) { st.offset = 0; $a('arch-grid').textContent = ''; } + const q = new URLSearchParams({ offset: st.offset, limit: st.favOnly ? 100 : PAGE }); + ['gender', 'age', 'pitch', 'accent', 'language', 'category', 'q'].forEach(k => st[k] && q.set(k, st[k])); + say(tr('Loading…')); + try { + const r = await fetch('/api/archetypes?' + q); + if (!r.ok) throw new Error(r.statusText); + const d = await r.json(); + if (my !== token) return; + if (!$a('arch-category').options.length) { + const sel = $a('arch-category'); + [['', tr('All categories')], ...d.categories.map(c => [c, c])].forEach(([v, t]) => sel.add(new Option(t, v))); + } + const items = st.favOnly ? d.items.filter(a => favs.has(a.id)) : d.items; + const grid = $a('arch-grid'); + items.forEach(a => grid.appendChild(card(a))); + st.offset += d.items.length; st.total = d.total; + $a('arch-more').hidden = st.favOnly || st.offset >= d.total; + say(grid.children.length ? grid.children.length + ' / ' + (st.favOnly ? grid.children.length : d.total) : tr('No voices match.')); + } catch (e) { if (my === token) say(tr('Could not load the gallery.')); } +} + +async function play(a, btn) { + btn.disabled = true; say(tr('Rendering preview…')); + try { + const r = await fetch('/api/archetypes/' + encodeURIComponent(a) + '/preview', { method: 'POST' }); + if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); } + if (objUrl) URL.revokeObjectURL(objUrl); + objUrl = URL.createObjectURL(await r.blob()); + const au = $a('arch-audio'); au.src = objUrl; await au.play().catch(() => {}); + say(tr('Preview ready')); + } catch (e) { say(tr('Preview failed') + ': ' + e.message); } + finally { btn.disabled = false; } +} + +async function use(id, btn) { + btn.disabled = true; say(tr('Saving voice…')); + try { + const r = await fetch('/api/archetypes/' + encodeURIComponent(id) + '/use', { method: 'POST' }); + if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.detail || r.statusText); } + const d = await r.json(); + say(tr('Voice saved') + ': ' + d.voice_id); + if (typeof toast === 'function') toast(tr('Voice saved') + ': ' + d.voice_id, 'success'); + if (!btn.parentNode.querySelector('.arch-open')) { + const o = document.createElement('button'); o.type = 'button'; o.className = 'btn-secondary arch-open'; + o.textContent = tr('Open in Design'); o.dataset.instruct = d.instruct; o.dataset.language = d.language; + btn.parentNode.insertBefore(o, btn.nextSibling); o.focus(); + } + if (typeof loadVoiceLibrary === 'function') loadVoiceLibrary().catch(() => {}); + } catch (e) { say(tr('Save failed') + ': ' + e.message); if (typeof toast === 'function') toast(e.message, 'error'); } + finally { btn.disabled = false; } +} + +function boot() { + if (booted) return; booted = true; + buildChips(); + let t; + $a('arch-q').addEventListener('input', e => { clearTimeout(t); t = setTimeout(() => { st.q = e.target.value.trim(); load(true); }, 250); }); + $a('arch-category').addEventListener('change', e => { st.category = e.target.value; load(true); }); + $a('arch-more').addEventListener('click', () => load(false)); + $a('arch-grid').addEventListener('click', e => { + const b = e.target.closest('button'); if (!b) return; + const id = b.closest('.arch-card').dataset.id; + if (b.classList.contains('arch-play')) play(id, b); + else if (b.classList.contains('arch-use')) use(id, b); + else if (b.classList.contains('arch-fav')) { + favs.has(id) ? favs.delete(id) : favs.add(id); saveFavs(); + b.textContent = favs.has(id) ? '★' : '☆'; b.setAttribute('aria-pressed', String(favs.has(id))); + } else if (b.classList.contains('arch-open')) { + const ta = $a('design-instruct'); + ta.value = b.dataset.instruct; ta.dispatchEvent(new Event('input', { bubbles: true })); + const sel = $a('d-lang'); if (sel) for (const o of sel.options) if (o.value === b.dataset.language) sel.value = o.value; + ta.scrollIntoView({ behavior: matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth', block: 'center' }); + ta.focus(); + } + }); + load(true); +} +det.addEventListener('toggle', () => { if (det.open) boot(); }); +if (det.open) boot(); +})(); diff --git a/static/module-manifest.json b/static/module-manifest.json index d2ff0d7..4f5183c 100644 --- a/static/module-manifest.json +++ b/static/module-manifest.json @@ -13,6 +13,7 @@ "routing", "vocal-isolation", "voice-clone", + "voice-archetypes", "voice-library", "tts-preview", "generation", diff --git a/static/sections/s-design.html b/static/sections/s-design.html index fb7df2d..6a4031a 100644 --- a/static/sections/s-design.html +++ b/static/sections/s-design.html @@ -21,6 +21,24 @@
+
+ +
+

Describe the voice

Describe the voice you want. Qwen3-TTS VoiceDesign will synthesise it from the description and sample text.

diff --git a/tests/archetype-gallery.spec.js b/tests/archetype-gallery.spec.js new file mode 100644 index 0000000..2fed027 --- /dev/null +++ b/tests/archetype-gallery.spec.js @@ -0,0 +1,55 @@ +// @ts-check +// Voice archetype gallery; every /api route is mocked (no live writes, no generation). +const { test, expect } = require('@playwright/test'); + +const items = Array.from({ length: 24 }, (_, i) => ({ + id: 'v' + i, name_de: 'Stimme ' + i, name_en: 'Voice ' + i, instruct: 'Tiefe Stimme ' + i, + category: 'Narration & Story', language: 'German', + facets: { gender: i % 2 ? 'female' : 'male', age: 'elderly', pitch: 'low', accent: 'none', tone: 'warm' }, +})); + +test('gallery lazy-renders, filters, previews, uses and opens in design', async ({ page }) => { + const calls = []; + await page.route('**/api/archetypes**', route => { + const req = route.request(), url = new URL(req.url()); + calls.push(req.method() + ' ' + url.pathname + url.search); + if (req.method() === 'GET') { + const g = url.searchParams.get('gender'); + const rows = g ? items.filter(i => i.facets.gender === g) : items; + return route.fulfill({ json: { total: 30, offset: 0, limit: 24, items: rows, categories: ['Narration & Story'], cached: [] } }); + } + if (url.pathname.endsWith('/preview')) return route.fulfill({ status: 502, json: { detail: 'mocked engine down' } }); + return route.fulfill({ json: { voice_id: 'archetype_v0', created: true, instruct: 'Tiefe Stimme 0', language: 'German' } }); + }); + await page.route(/\/api\/(?!archetypes)/, route => route.request().method() === 'GET' + ? route.fulfill({ json: [] }) : route.fulfill({ json: { ok: true } })); + await page.route('**/static/dist/main.min.js*', r => r.abort()); + await page.goto('/', { waitUntil: 'domcontentloaded' }); + await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 }); + await page.waitForSelector('#app-skeleton', { state: 'detached', timeout: 15_000 }); + await page.evaluate(() => window.navTo('s-design')); + + await expect(page.locator('.arch-card')).toHaveCount(0); // lazy: nothing before opening + expect(calls).toEqual([]); + await page.locator('#archetype-gallery > summary').click(); + await expect(page.locator('.arch-card')).toHaveCount(24); + + await page.locator('#arch-chips [data-k="gender"][data-v="female"]').click(); + await expect(page.locator('.arch-card')).toHaveCount(12); + await expect(page.locator('#arch-chips [data-v="female"]')).toHaveAttribute('aria-pressed', 'true'); + + const play = page.locator('.arch-play').first(); + const box = await play.boundingBox(); + expect(box && box.height).toBeGreaterThanOrEqual(44); + await play.click(); + await expect(page.locator('#arch-status')).toContainText('mocked engine down'); + + await page.locator('.arch-fav').first().click(); + const stored = await page.evaluate(() => localStorage.getItem('ttsvc-archetype-favs')); + expect(JSON.parse(stored || '[]')).toHaveLength(1); + + await page.locator('.arch-use').first().click(); + await expect(page.locator('.arch-open').first()).toBeVisible(); + await page.locator('.arch-open').first().click(); + await expect(page.locator('#design-instruct')).toHaveValue(/Tiefe Stimme/); +});