tts-voice-creator-clone-and.../static/dist/main.min.js
mARTin-B78 b1c0197652
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
Release v1.53.2
2026-09-24 07:44:16 +02:00

879 lines
427 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

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

(function(){"use strict";const _pickers={};function _voiceData(id){return(window._voices||[]).find(v=>v.id===id)||null}function _voiceLang(v,id){const raw=String((v==null?void 0:v.lang)||(v==null?void 0:v.language)||"").trim(),fromMeta=raw&&raw.length<=5?raw:"",fromId=String(id||"").split("_")[0]||"";return(fromMeta||fromId).toUpperCase()}function _voiceGender(v,id){const raw=String((v==null?void 0:v.gender)||(v==null?void 0:v.sex)||"").trim(),first=raw?raw.charAt(0).toUpperCase():"";if(["F","M","N"].includes(first))return first;const fromId=(String(id||"").split("_")[1]||"").charAt(0).toUpperCase();return["F","M","N"].includes(fromId)?fromId:""}function _voiceMetaLabel(id){const v=_voiceData(id);return[_voiceGender(v,id),_voiceLang(v,id)].filter(Boolean).join(" ")}function _voiceOptionLabel(id){const meta=_voiceMetaLabel(id);return meta?`${id} ${meta}`:id}function _voiceSearchText(id,label){const v=_voiceData(id)||{},tags=Array.isArray(v.tags)?v.tags.join(" "):v.tags||v.tag||"";return[id,label,v.name,tags].map(x=>String(x||"").toLowerCase()).join(" ")}const VOICE_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"},VOICE_AVATAR_COLORS={male:"#3b82f6",female:"#ec4899",neutral:"#6b7280",robot:"#0ea5e9",animal:"#f59e0b"};window.VOICE_AVATAR_ICONS=VOICE_AVATAR_ICONS,window.voiceAvatarIcon=function(avatarKey,size){const icon=VOICE_AVATAR_ICONS[avatarKey];if(!icon)return null;const s=size+"px",r=Math.round(size/2)+"px",bg=VOICE_AVATAR_COLORS[avatarKey]||"#6b7280";return`<span class="vp-avatar" style="width:${s};height:${s};border-radius:${r};background:${bg};color:#fff;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0"><span class="mdi ${icon}" style="font-size:${Math.round(size*.58)}px"></span></span>`};function _avatarHtml(id,size){const v=_voiceData(id),s=size+"px",r=Math.round(size/2)+"px";if(v!=null&&v.has_picture)return`<img src="/api/voice/picture/${encodeURIComponent(id)}" class="vp-avatar" style="width:${s};height:${s};border-radius:${r};object-fit:cover;flex-shrink:0" alt="">`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(v==null?void 0:v.avatar,size):null;if(icon)return icon;const lang=(v==null?void 0:v.lang)||"",color=_langColor(lang,id),init=(id||"?")[0].toUpperCase();return`<span class="vp-avatar" style="width:${s};height:${s};border-radius:${r};background:${color};color:#fff;font-weight:700;font-size:${Math.round(size*.44)}px;display:inline-flex;align-items:center;justify-content:center;flex-shrink:0">${init}</span>`}function _langColor(lang,id){const str=(lang||id||"").toLowerCase();if(str.startsWith("de"))return"#3b82f6";if(str.startsWith("en"))return"#10b981";if(str.startsWith("fr"))return"#8b5cf6";if(str.startsWith("es"))return"#f59e0b";if(str.startsWith("it"))return"#ef4444";if(str.startsWith("zh"))return"#ec4899";if(str.startsWith("ja"))return"#f97316";const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<str.length;i++)h=h*31+str.charCodeAt(i)>>>0;return palette[h%palette.length]}function _flagSpan(v){return v&&v.flag?`<span class="vp-flag">${v.flag}</span>`:""}function _buildItem(id,label){const v=_voiceData(id),meta=_voiceMetaLabel(id),name=id||label||"";return`<div class="vp-item" data-value="${_esc(id)}" tabindex="-1">
${_avatarHtml(id,26)}
<span class="vp-item-name">${_esc(name)}</span>
${meta?`<span class="vp-item-meta">${_esc(meta)}</span>`:_flagSpan(v)}
</div>`}function _esc(s){return String(s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function upgrade(selectId){const sel=document.getElementById(selectId);if(!sel||sel.dataset.vpUpgraded)return;sel.dataset.vpUpgraded="1",sel.style.display="none";const root=document.createElement("div");root.className="vp-root",root.dataset.vpFor=selectId,sel.parentNode.insertBefore(root,sel);const trigger=document.createElement("div");trigger.className="vp-trigger",trigger.tabIndex=0,trigger.innerHTML='<span class="vp-trigger-content"><span class="vp-trigger-placeholder">\u2014 select after fetch \u2014</span></span><span class="mdi mdi-chevron-down vp-chevron"></span>',root.appendChild(trigger);const drop=document.createElement("div");drop.className="vp-drop",drop.hidden=!0,drop.innerHTML='<div class="vp-search-wrap"><span class="mdi mdi-magnify vp-search-icon"></span><input class="vp-search" placeholder="Search voices\u2026" autocomplete="off" spellcheck="false"></div><div class="vp-list"></div>',root.appendChild(drop);const searchInp=drop.querySelector(".vp-search"),list=drop.querySelector(".vp-list");function _renderList(filter){const opts=Array.from(sel.options).filter(o=>o.value),q=(filter||"").toLowerCase(),filtered=q?opts.filter(o=>_voiceSearchText(o.value,o.textContent).includes(q)):opts;if(!filtered.length){list.innerHTML='<div class="vp-empty">No voices found</div>';return}list.innerHTML=filtered.map(o=>_buildItem(o.value,o.textContent)).join(""),list.querySelectorAll(".vp-item").forEach(el=>{el.classList.toggle("selected",el.dataset.value===sel.value)})}function _open(){drop.hidden=!1,drop.style.display="",trigger.classList.add("open"),searchInp.value="",_renderList(""),searchInp.focus(),requestAnimationFrame(()=>{const sel2=list.querySelector(".vp-item.selected");sel2&&sel2.scrollIntoView({block:"nearest"})})}function _close(){drop.hidden=!0,drop.style.display="none",trigger.classList.remove("open"),searchInp.blur()}function _select(value){sel.value=value,sel.dispatchEvent(new Event("change",{bubbles:!0})),_syncTrigger(),_close(),trigger.focus()}function _syncTrigger(){const v=sel.value,content=trigger.querySelector(".vp-trigger-content");if(!v)content.innerHTML='<span class="vp-trigger-placeholder">\u2014 select after fetch \u2014</span>';else{const label=v,meta=_voiceMetaLabel(v);content.innerHTML=`${_avatarHtml(v,24)}<span class="vp-trigger-name">${_esc(label)}</span>${meta?`<span class="vp-trigger-meta">${_esc(meta)}</span>`:""}`}}trigger.addEventListener("click",()=>drop.hidden?_open():_close()),trigger.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),_open())}),searchInp.addEventListener("input",()=>_renderList(searchInp.value)),searchInp.addEventListener("keydown",e=>{var _a;e.key==="Escape"&&(e.stopPropagation(),_close(),trigger.focus()),e.key==="ArrowDown"&&(e.preventDefault(),(_a=list.querySelector(".vp-item"))==null||_a.focus())}),list.addEventListener("pointerdown",e=>{const item=e.target.closest(".vp-item");item&&(e.preventDefault(),_select(item.dataset.value))}),list.addEventListener("click",e=>{const item=e.target.closest(".vp-item");item&&_select(item.dataset.value)}),list.addEventListener("keydown",e=>{var _a;const item=e.target.closest(".vp-item");item&&((e.key==="Enter"||e.key===" ")&&(e.preventDefault(),_select(item.dataset.value)),e.key==="ArrowDown"&&(e.preventDefault(),(item.nextElementSibling||item).focus()),e.key==="ArrowUp"&&(e.preventDefault(),(_a=item.previousElementSibling||item)!=null&&_a.focus()||searchInp.focus()),e.key==="Escape"&&(_close(),trigger.focus()))}),document.addEventListener("click",e=>{root.contains(e.target)||_close()},!0),sel.addEventListener("change",()=>{_syncTrigger(),_close()}),new MutationObserver(()=>{_syncTrigger()}).observe(sel,{childList:!0,attributes:!0,subtree:!0}),_pickers[selectId]={root,trigger,drop,searchInp,list,sel,renderList:_renderList,syncTrigger:_syncTrigger}}function populate(selectId,voices){const p=_pickers[selectId];if(!p)return;const{sel,renderList,syncTrigger}=p,prev=sel.value;sel.innerHTML='<option value="">\u2014 select after fetch \u2014</option>',voices.forEach(id=>{const opt=document.createElement("option");opt.value=id,opt.textContent=_voiceOptionLabel(id),sel.appendChild(opt)}),prev&&voices.includes(prev)&&(sel.value=prev),renderList(""),syncTrigger()}function getValue(selectId){var _a;return((_a=document.getElementById(selectId))==null?void 0:_a.value)||""}function setValue(selectId,value){var _a;const sel=document.getElementById(selectId);sel&&(sel.value=value,(_a=_pickers[selectId])==null||_a.syncTrigger())}function attachTextPicker(input){if(!input||input.dataset.vpTextAttached)return;input.dataset.vpTextAttached="1";const btn=document.createElement("button");btn.type="button",btn.className="vp-text-browse",btn.title="Browse voices",btn.innerHTML='<span class="mdi mdi-chevron-down"></span>',input.insertAdjacentElement("afterend",btn);const drop=document.createElement("div");drop.className="vp-drop vp-drop-floating",drop.hidden=!0,drop.innerHTML='<div class="vp-search-wrap"><span class="mdi mdi-magnify vp-search-icon"></span><input class="vp-search" placeholder="Search voices\u2026" autocomplete="off" spellcheck="false"></div><div class="vp-list"></div>',document.body.appendChild(drop);const searchInp=drop.querySelector(".vp-search"),list=drop.querySelector(".vp-list");function _voices2(){return(window._voices||[]).filter(v=>v.enabled!==!1)}function _renderList(filter){const q=(filter||"").toLowerCase(),filtered=q?_voices2().filter(v=>_voiceSearchText(v.id,v.name||v.id).includes(q)):_voices2();if(!filtered.length){list.innerHTML='<div class="vp-empty">No voices found</div>';return}list.innerHTML=filtered.map(v=>_buildItem(v.id,v.id)).join(""),list.querySelectorAll(".vp-item").forEach(el=>el.classList.toggle("selected",el.dataset.value===input.value))}function _position(){const r=input.getBoundingClientRect();drop.style.left=r.left+"px",drop.style.top=r.bottom+4+"px",drop.style.width=Math.max(240,r.width+32)+"px"}function _open(){_position(),drop.hidden=!1,searchInp.value="",_renderList(""),searchInp.focus()}function _close(){drop.hidden=!0}function _select(value){input.value=value,input.dispatchEvent(new Event("input",{bubbles:!0})),input.dispatchEvent(new Event("change",{bubbles:!0})),_close()}btn.addEventListener("click",e=>{e.stopPropagation(),drop.hidden?_open():_close()}),searchInp.addEventListener("input",()=>_renderList(searchInp.value)),searchInp.addEventListener("keydown",e=>{e.key==="Escape"&&(e.stopPropagation(),_close())}),list.addEventListener("click",e=>{const item=e.target.closest(".vp-item");item&&_select(item.dataset.value)}),document.addEventListener("click",e=>{!drop.contains(e.target)&&e.target!==btn&&_close()},!0),window.addEventListener("resize",()=>{drop.hidden||_position()})}window.VoicePicker={upgrade,populate,getValue,setValue,attachTextPicker}})(),function(){"use strict";const pickers={};function esc(value){return String(value==null?"":value).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;")}function norm(value){if(typeof value=="string")return{id:value,label:value,meta:null,raw:value};const id=(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||(value==null?void 0:value.voice_id)||"",hasExplicitMeta=Object.prototype.hasOwnProperty.call(value||{},"meta"),looksLikeVoice=!!(value!=null&&value.display_name||value!=null&&value.duration||value!=null&&value.benchmark||value!=null&&value.has_picture||value!=null&&value.transcript);return{id,label:(value==null?void 0:value.label)||(value==null?void 0:value.display_name)||(value==null?void 0:value.name)||(value==null?void 0:value.voice)||id,meta:hasExplicitMeta?value.meta:looksLikeVoice?value:null,raw:value}}function libraryVoice(id){return(window._voices||[]).find(v=>v&&v.id===id)||null}function metaFor(item){return(item==null?void 0:item.meta)||libraryVoice(item==null?void 0:item.id)||(item==null?void 0:item.raw)||null}function langColor(lang,id){const str=String(lang||id||"").toLowerCase();if(str.startsWith("de"))return"#3b82f6";if(str.startsWith("en"))return"#10b981";if(str.startsWith("fr"))return"#8b5cf6";if(str.startsWith("es"))return"#f59e0b";if(str.startsWith("it"))return"#ef4444";if(str.startsWith("zh"))return"#ec4899";if(str.startsWith("ja"))return"#f97316";const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<str.length;i++)h=h*31+str.charCodeAt(i)>>>0;return palette[h%palette.length]}function avatarHtml(item,size){var _a;const id=(item==null?void 0:item.id)||"",meta=metaFor(item),s=`${size}px`,r=`${Math.round(size/2)}px`;if(meta!=null&&meta.has_picture)return`<img src="/api/voice/picture/${encodeURIComponent(id)}" class="bvp-avatar" style="width:${s};height:${s};border-radius:${r};object-fit:cover" alt="">`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta==null?void 0:meta.avatar,size):null;if(icon)return icon.replace(/vp-avatar/g,"bvp-avatar");const color=langColor((meta==null?void 0:meta.lang)||(meta==null?void 0:meta.language),id),init=((_a=((item==null?void 0:item.label)||id||"?").trim()[0])==null?void 0:_a.toUpperCase())||"?";return`<span class="bvp-avatar" style="width:${s};height:${s};border-radius:${r};background:${color};color:#fff;font-weight:700;font-size:${Math.round(size*.44)}px">${esc(init)}</span>`}function numeric(value){const n=Number(value);return Number.isFinite(n)?n:null}function statsFor(item){var _a,_b,_c,_d,_e,_f;const meta=metaFor(item)||{},b=meta.benchmark||(item==null?void 0:item.benchmark)||{},parts=[],duration=numeric((_a=meta.duration)!=null?_a:item==null?void 0:item.duration),speed=numeric((_c=(_b=b.speed)!=null?_b:b.avg_speed)!=null?_c:meta.speed),rtf=numeric((_e=(_d=b.rtf)!=null?_d:b.avg_rtf)!=null?_e:meta.rtf),elapsed=numeric((_f=b.elapsed_sec)!=null?_f:b.avg_elapsed_sec);return duration!=null&&duration>0&&parts.push(`${duration.toFixed(1)}s`),speed!=null&&speed>0?parts.push(`${speed.toFixed(2)}x`):rtf!=null&&rtf>0&&parts.push(`RTF ${rtf.toFixed(2)}`),!parts.length&&elapsed!=null&&elapsed>0&&parts.push(`${elapsed.toFixed(1)}s TTS`),!parts.length&&b.ok===!1&&parts.push("benchmark failed"),parts.join(" \xB7 ")}function searchText(item){const meta=metaFor(item)||{};return[item==null?void 0:item.id,item==null?void 0:item.label,meta.display_name,meta.name,meta.lang,meta.language,Array.isArray(meta.tags)?meta.tags.join(" "):meta.tags,statsFor(item)].filter(Boolean).join(" ").toLowerCase()}function optionLabel(item){const stats=statsFor(item);return`${item.label||item.id}${stats?` (${stats})`:""}`}function renderTrigger(picker){const value=picker.select.value,item=picker.items.find(v=>v.id===value),content=picker.trigger.querySelector(".bvp-trigger-content");if(!item){content.innerHTML=`<span class="bvp-placeholder">${esc(picker.placeholder)}</span>`;return}const stats=statsFor(item);content.innerHTML=`${picker.plain?"":avatarHtml(item,24)}<span class="bvp-trigger-main"><span class="bvp-trigger-name">${esc(item.label||item.id)}</span>${stats?`<span class="bvp-trigger-stats">${esc(stats)}</span>`:""}</span>`}function renderList(picker,filter){const q=String(filter||"").trim().toLowerCase(),items=q?picker.items.filter(item=>searchText(item).includes(q)):picker.items;if(!items.length){picker.list.innerHTML=`<div class="bvp-empty">${esc(q?`No ${picker.noun} found`:picker.empty)}</div>`;return}picker.list.innerHTML=items.map(item=>{const meta=metaFor(item)||{},stats=statsFor(item),sub=[meta.lang||meta.language,meta.file_type].filter(Boolean).join(" \xB7 ");return`<button class="bvp-item${item.id===picker.select.value?" selected":""}" type="button" data-value="${esc(item.id)}">
${picker.plain?"":avatarHtml(item,28)}
<span class="bvp-item-main"><span class="bvp-item-name">${esc(item.label||item.id)}</span>${sub?`<span class="bvp-item-sub">${esc(sub)}</span>`:""}</span>
${stats?`<span class="bvp-item-stats">${esc(stats)}</span>`:""}
</button>`}).join("")}function close(picker){picker&&(picker.drop.hidden=!0,picker.drop.style.display="none",picker.trigger.classList.remove("open"))}function closeAll(except){Object.values(pickers).forEach(p=>{p!==except&&close(p)})}function open(picker){closeAll(picker),picker.drop.hidden=!1,picker.drop.style.display="",picker.trigger.classList.add("open"),picker.search.value="",renderList(picker,""),picker.search.focus(),requestAnimationFrame(()=>{var _a;return(_a=picker.list.querySelector(".bvp-item.selected"))==null?void 0:_a.scrollIntoView({block:"nearest"})})}function setValue(picker,value,dispatch){picker.select.value=value||"",renderTrigger(picker),renderList(picker,picker.search.value),close(picker),dispatch!==!1&&picker.select.dispatchEvent(new Event("change",{bubbles:!0})),picker.trigger.focus()}function upgrade(selectId,options){var _a;const select=document.getElementById(selectId);if(!select)return null;if(pickers[selectId])return pickers[selectId];const picker={select,items:[],placeholder:(options==null?void 0:options.placeholder)||((_a=Array.from(select.options).find(o=>!o.value))==null?void 0:_a.textContent)||"\u2014 select after fetch \u2014",empty:(options==null?void 0:options.empty)||"No voices",noun:(options==null?void 0:options.noun)||"voices",plain:!!(options!=null&&options.plain)};select.dataset.bvpUpgraded="1",select.style.display="none";const root=document.createElement("div");return root.className="bvp-root",root.dataset.bvpFor=selectId,select.parentNode.insertBefore(root,select),root.innerHTML=`<button class="bvp-trigger" type="button">
<span class="bvp-trigger-content"><span class="bvp-placeholder">${esc(picker.placeholder)}</span></span>
<span class="mdi mdi-chevron-down bvp-chevron"></span>
</button>
<div class="bvp-drop" hidden>
<div class="bvp-search-wrap"><span class="mdi mdi-magnify bvp-search-icon"></span><input class="bvp-search" type="search" placeholder="Search ${esc(picker.noun)}..." autocomplete="off" spellcheck="false"></div>
<div class="bvp-list"></div>
</div>`,Object.assign(picker,{root,trigger:root.querySelector(".bvp-trigger"),drop:root.querySelector(".bvp-drop"),search:root.querySelector(".bvp-search"),list:root.querySelector(".bvp-list")}),picker.trigger.addEventListener("click",e=>{e.preventDefault(),picker.drop.hidden?open(picker):close(picker)}),picker.trigger.addEventListener("keydown",e=>{var _a2;(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),open(picker)),e.key==="ArrowDown"&&(e.preventDefault(),open(picker),(_a2=picker.list.querySelector(".bvp-item"))==null||_a2.focus())}),picker.search.addEventListener("input",()=>renderList(picker,picker.search.value)),picker.search.addEventListener("keydown",e=>{var _a2;e.key==="Escape"&&(e.preventDefault(),close(picker),picker.trigger.focus()),e.key==="ArrowDown"&&(e.preventDefault(),(_a2=picker.list.querySelector(".bvp-item"))==null||_a2.focus())}),picker.list.addEventListener("click",e=>{const item=e.target.closest(".bvp-item");item&&setValue(picker,item.dataset.value,!0)}),picker.list.addEventListener("keydown",e=>{const item=e.target.closest(".bvp-item");item&&((e.key==="Enter"||e.key===" ")&&(e.preventDefault(),setValue(picker,item.dataset.value,!0)),e.key==="ArrowDown"&&(e.preventDefault(),(item.nextElementSibling||item).focus()),e.key==="ArrowUp"&&(e.preventDefault(),(item.previousElementSibling||picker.search).focus()),e.key==="Escape"&&(e.preventDefault(),close(picker),picker.trigger.focus()))}),select.addEventListener("change",()=>{renderTrigger(picker),close(picker)}),document.addEventListener("click",e=>{root.contains(e.target)||close(picker)},!0),pickers[selectId]=picker,syncFromSelect(selectId),picker}function populate(selectId,values,options){var _a;const picker=upgrade(selectId,options||{});if(!picker)return;picker.placeholder=(options==null?void 0:options.placeholder)||picker.placeholder,picker.empty=(options==null?void 0:options.empty)||picker.empty;const previous=picker.select.value;picker.items=(values||[]).map(norm).filter(item=>item.id),picker.select.innerHTML=`<option value="">${esc(picker.placeholder)}</option>`+picker.items.map(item=>`<option value="${esc(item.id)}">${esc(optionLabel(item))}</option>`).join(""),previous&&picker.items.some(item=>item.id===previous)&&(picker.select.value=previous),renderTrigger(picker),renderList(picker,((_a=picker.search)==null?void 0:_a.value)||"")}function syncFromSelect(selectId){const picker=pickers[selectId];if(!picker)return;const blank=Array.from(picker.select.options).find(o=>!o.value);blank&&blank.textContent&&(picker.placeholder=blank.textContent),picker.items=Array.from(picker.select.options).filter(o=>o.value).map(o=>({id:o.value,label:o.textContent||o.value,meta:libraryVoice(o.value)||{}})),renderTrigger(picker),renderList(picker,"")}function getValue(selectId){var _a;return((_a=document.getElementById(selectId))==null?void 0:_a.value)||""}function set(selectId,value){const picker=pickers[selectId]||upgrade(selectId);picker&&setValue(picker,value,!0)}window.BenchmarkVoicePicker={upgrade,populate,syncFromSelect,getValue,set}}(),function(root){let _selectedVoiceWrap=null;function selectVoice2(wrap){var _a;const inspector=document.getElementById("voices-inspector");if(!inspector)return;function restoreToRow(targetWrap){(targetWrap._extracted||[]).forEach(({el,target})=>{el&&target&&target.appendChild(el)}),targetWrap._extracted=[],inspector.querySelectorAll(".vr-main-row,.vr-optimizer").forEach(el=>targetWrap.appendChild(el))}if(_selectedVoiceWrap&&_selectedVoiceWrap!==wrap&&(_selectedVoiceWrap.classList.remove("vr-selected"),restoreToRow(_selectedVoiceWrap)),_selectedVoiceWrap===wrap){restoreToRow(wrap),wrap.classList.remove("vr-selected"),_selectedVoiceWrap=null,inspector.innerHTML='<div class="inspector-placeholder"><span><span class="mdi mdi-account-voice"></span></span><p>Pick a voice on the left<br>to edit it here</p></div>',typeof window.onMobileInspectorClose=="function"&&window.onMobileInspectorClose();return}_selectedVoiceWrap=wrap,wrap.classList.add("vr-selected"),typeof window.onMobileInspectorOpen=="function"&&window.onMobileInspectorOpen();const voiceId=wrap.dataset.id||"",color=wrap.dataset.color||"#9575CD",isClone=wrap.dataset.hasRef==="true",dbfs=wrap.dataset.dbfs||"-",hasPicture=wrap.dataset.hasPicture==="true",v=(_voices||[]).find(vv=>vv.id===voiceId)||{},langCode=v.lang||wrap.dataset.lang||"",flagCc=v.flag||wrap.dataset.flagCc||langCode,rating=v.rating||0,curGender=v.gender||"N",nameParts=voiceId.split("_"),dispName=v.name&&String(v.name).trim()?String(v.name).trim():nameParts.length>1?nameParts[nameParts.length-1]:voiceId,initial=(dispName[0]||voiceId[0]||"?").toUpperCase(),picSrcInsp=hasPicture?`/api/voice/picture/${encodeURIComponent(voiceId)}`:null,inspFlagCc=flagCc||langCode,inspFlagIcon=inspFlagCc?`<span class="fi fi-${inspFlagCc.toLowerCase()}" role="img" aria-label="${langCode}"></span>`:null,silHtml=`<span class="mdi ${curGender==="M"?"mdi-face-man":curGender==="F"?"mdi-face-woman":"mdi-account"}" style="color:#fff;font-size:32px;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:${color}"></span>`,inspIconHtml=window.voiceAvatarIcon?window.voiceAvatarIcon(v.avatar,48):null,avatarHtml=picSrcInsp?`<img src="${picSrcInsp}" alt="" class="insp-avatar-img">`:inspIconHtml||silHtml,avatarBgStyle=picSrcInsp||inspIconHtml?"":`style="background:${color}"`,flagIconHtml=inspFlagIcon||"",makeStars=n=>[1,2,3,4,5].map(i=>`<span class="insp-star${i<=n?" on":""}" data-val="${i}"><span class="mdi mdi-star"></span></span>`).join(""),inspBenchText=typeof fmtBenchmark=="function"?fmtBenchmark(v):"-",inspBenchTitle=typeof benchmarkTitle=="function"?benchmarkTitle(v):"",inspBenchCls=typeof benchmarkClass=="function"?benchmarkClass(v):"",LANGS=["EN","DE","IT","ES","FR","PT","NL","PL","ZH","JA","KO","AR","RU","TR","HI","SV","DA","FI","NB","HU","CS","RO","UK"],_gM={F:"\u2640",M:"\u2642",N:"\u26A5","":"?"},_gL={F:"Female",M:"Male",N:"Diverse","":"\u2014"},_gC={F:"g-f",M:"g-m",N:"g-n","":"g-n"},genderLabelHtml=`${_gM[curGender]||"?"} ${_gL[curGender]||"\u2014"}`;inspector.innerHTML=`
<div class="inspector-header">
<div class="insp-hd-top">
<div class="inspector-avatar${picSrcInsp?" insp-avatar-photo":""} insp-avatar-clickable" ${avatarBgStyle} title="Click to change photo">${avatarHtml}</div>
<div class="insp-title-stack">
<div class="insp-hd-row1">
<h3 class="insp-disp-name" title="Double-click to rename the display name">${escHtml(dispName)}</h3>
<input class="insp-name-edit" value="${escHtml(voiceId)}" spellcheck="false" style="display:none" placeholder="Voice ID">
<div class="insp-actions-save"></div>
</div>
<div class="insp-hd-row2">
<div class="insp-id-block">
<span class="insp-full-id" title="This is also the voice's filename on disk \u2014 click to rename it (updates the .wav, .json and .txt files too)">${escHtml(voiceId)}</span>
<button class="insp-edit-id-btn" type="button" title="Rename this voice's files (.wav, .json, .txt) \u2014 the voice ID is its filename"><span class="mdi mdi-pencil-outline"></span> rename filename</button>
<button class="insp-copy-id-btn" type="button" title="Copy filename">copy filename</button>
</div>
<div class="insp-hd-row2-right" style="display:flex; flex-direction:column; align-items:flex-end; gap:4px;">
<div class="insp-actions-active"></div>
${v.seed!==void 0&&v.seed!==null?`<div class="insp-pinned-seed" style="color:#d32f2f; font-weight:600; font-size:12px;">Pin Seed # ${v.seed}</div>`:'<div class="insp-pinned-seed" style="color:#d32f2f; font-weight:600; font-size:12px; display:none;"></div>'}
</div>
</div>
<div class="insp-hd-divider"></div>
<div class="insp-subtitle">
<span class="insp-flag" title="Click to change accent flag">
<span class="insp-flag-icon">${flagIconHtml}</span>
</span>
<span class="insp-lang-wrap" title="Click to change language">
<span class="insp-lang-code">${escHtml(langCode)}</span>
</span>
<span class="insp-gender-label" title="Click to cycle gender">${escHtml(genderLabelHtml)}</span>
<span class="vl-type-label ${isClone?"vl-type-clone":"vl-type-design"}">${isClone?"Clone":"Design"}</span>
${inspBenchText!=="-"?`<span class="insp-bench-chip ${escHtml(inspBenchCls)}" title="${escHtml(inspBenchTitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(inspBenchText)}</span>`:""}
<span class="insp-stars-wrap">
<span class="insp-stars">${makeStars(rating)}</span>
<span class="insp-rating-label">${rating}/5</span>
</span>
<input class="insp-tag-input" type="text" placeholder="tag1, tag2\u2026" value="${escHtml(v.tag||"")}" autocomplete="off">
</div>
<div class="insp-note-delete-row">
<div class="insp-note-slot"></div>
<div class="insp-actions-delete"></div>
</div>
</div>
</div>
</div>
<div class="inspector-body"></div>
`,window.innerWidth<=767&&typeof window.onMobileInspectorOpen=="function"&&window.onMobileInspectorOpen();const saveSlot=inspector.querySelector(".insp-actions-save"),activeSlot=inspector.querySelector(".insp-actions-active"),deleteSlot=inspector.querySelector(".insp-actions-delete"),body=inspector.querySelector(".inspector-body"),saveBtn=document.createElement("button");saveBtn.className="btn-primary insp-save-btn",saveBtn.textContent="Save changes",saveBtn.addEventListener("click",()=>{var _a2;return(_a2=body.querySelector(".opt-save-text"))==null?void 0:_a2.click()}),saveSlot.appendChild(saveBtn);const detailRow=wrap.querySelector(".vr-detail-row"),activeEl=detailRow==null?void 0:detailRow.querySelector(".vr-detail-active"),deleteEl=detailRow==null?void 0:detailRow.querySelector(".vr-detail-delete");if(activeEl&&activeSlot.appendChild(activeEl),deleteEl){deleteSlot.appendChild(deleteEl);const _dBtn=deleteEl.querySelector(".delete-btn"),_dCnl=deleteEl.querySelector(".delete-confirm-cancel");_dBtn==null||_dBtn.addEventListener("click",()=>deleteEl.classList.add("delete-pending")),_dCnl==null||_dCnl.addEventListener("click",()=>deleteEl.classList.remove("delete-pending"))}const inspAvatar=inspector.querySelector(".inspector-avatar");inspAvatar.addEventListener("click",()=>{var _a2;(_a2=body.querySelector(".photo-input"))==null||_a2.click()}),inspAvatar.addEventListener("dragenter",e=>{e.preventDefault(),inspAvatar.classList.add("drag-over")}),inspAvatar.addEventListener("dragover",e=>{e.preventDefault(),inspAvatar.classList.add("drag-over")}),inspAvatar.addEventListener("dragleave",()=>inspAvatar.classList.remove("drag-over")),inspAvatar.addEventListener("drop",async e=>{if(e.preventDefault(),inspAvatar.classList.remove("drag-over"),e.dataTransfer.files&&e.dataTransfer.files.length>0){const photoInput=body.querySelector(".photo-input");photoInput&&(photoInput.files=e.dataTransfer.files,photoInput.dispatchEvent(new Event("change")));return}let url=e.dataTransfer.getData("text/uri-list");if(!url){const html=e.dataTransfer.getData("text/html");if(html){const match=html.match(/src=["'](.*?)["']/);match&&(url=match[1])}}if(url||(url=e.dataTransfer.getData("text/plain")),url&&/^https?:\/\//i.test(url)){typeof status=="function"&&status("Downloading picture from URL...");try{const r=await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,image_url:url})});if(!r.ok)throw new Error((await r.json()).detail);const photoCell=body.querySelector(".vr-photo");photoCell&&photoCell.dispatchEvent(new Event("update-photo")),typeof toast=="function"&&toast("Photo saved from URL","success"),typeof status=="function"&&status("Photo saved successfully")}catch(err){typeof toast=="function"&&toast("Photo URL download failed: "+err.message,"error"),typeof status=="function"&&status("Photo URL download failed")}}}),inspector.querySelector(".insp-copy-id-btn").addEventListener("click",()=>{copyText(voiceId).then(()=>toast("Copied: "+voiceId))});const dispNameEl=inspector.querySelector(".insp-disp-name"),nameEditEl=inspector.querySelector(".insp-name-edit"),fullIdEl=inspector.querySelector(".insp-full-id");let _editMode=null,_editCancelled=!1;const showEditor=(mode,val)=>{_editMode=mode,_editCancelled=!1,dispNameEl.style.display="none",fullIdEl.style.display="none",nameEditEl.style.display="block",nameEditEl.placeholder=mode==="name"?"Display name":"Voice ID",nameEditEl.value=val,nameEditEl.focus(),nameEditEl.select()},startNameEdit=()=>showEditor("name",dispName),startInspRename=()=>showEditor("id",voiceId),commitEdit=async()=>{const mode=_editMode;if(_editMode=null,dispNameEl.style.display="",fullIdEl.style.display="",nameEditEl.style.display="none",_editCancelled||!mode)return;const val=nameEditEl.value.trim();if(mode==="id"){if(!val||val===voiceId)return;const rowInput=wrap.querySelector(".vr-name-input")||document.querySelector("#voices-inspector .vr-name-input"),rowOk=wrap.querySelector(".rename-ok")||document.querySelector("#voices-inspector .rename-ok");rowInput&&rowOk?(rowInput.value=val,rowOk.click()):typeof toast=="function"&&toast("Rename control not found \u2014 try renaming from the voice list instead","error");return}if(val!==dispName)try{const r=await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,name:val})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}v.name=val,dispNameEl.textContent=val||voiceId.split("_").pop(),typeof toast=="function"&&toast("Display name updated","success"),typeof renderVoiceList=="function"&&renderVoiceList()}catch(e){typeof toast=="function"&&toast("Rename failed: "+(e.message||e),"error")}};dispNameEl.addEventListener("dblclick",startNameEdit),fullIdEl.addEventListener("dblclick",startInspRename),fullIdEl.addEventListener("click",startInspRename),(_a=inspector.querySelector(".insp-edit-id-btn"))==null||_a.addEventListener("click",startInspRename),nameEditEl.addEventListener("blur",commitEdit),nameEditEl.addEventListener("keydown",e=>{e.key==="Enter"&&nameEditEl.blur(),e.key==="Escape"&&(_editCancelled=!0,nameEditEl.blur())});const flagSpan=inspector.querySelector(".insp-flag"),flagIconEl=inspector.querySelector(".insp-flag-icon"),applyFlag=async cc=>{v.flag=cc,wrap.dataset.flagCc=cc;const fi=`<span class="fi fi-${cc.toLowerCase()}" role="img" aria-label="${cc}"></span>`;flagIconEl&&(flagIconEl.innerHTML=fi),await saveMeta(voiceId,{flag:cc})};flagSpan.addEventListener("click",()=>{const langList=FLAG_OPTIONS[langCode.toUpperCase()]||[],items=uniqueFlagOptions([langList,ALL_FLAGS]).map(([cc,name])=>[cc.toLowerCase(),name]);createSearchablePicker(flagSpan,items,applyFlag,{placeholder:"Country or accent\u2026",renderItem:(cc,name)=>`<span class="fi fi-${cc}" style="width:20px;height:14px;background-size:cover;border-radius:2px;flex-shrink:0;display:inline-block"></span><span>${escHtml(name)}</span>`})});const langCodeEl=inspector.querySelector(".insp-lang-code"),langWrap=inspector.querySelector(".insp-lang-wrap"),applyLang=async newLang=>{v.lang=newLang,wrap.dataset.lang=newLang,langCodeEl&&(langCodeEl.textContent=newLang),await saveMeta(voiceId,{lang:newLang})};langWrap==null||langWrap.addEventListener("click",()=>{const items=LANGS.map(l=>[l,LANGUAGE_LABELS[l]?`${LANGUAGE_LABELS[l]} (${l})`:l]);createSearchablePicker(langWrap,items,applyLang,{placeholder:"Language\u2026",renderItem:(l,label)=>`<span class="ipi-code">${escHtml(l)}</span><span>${escHtml(LANGUAGE_LABELS[l]||l)}</span>`})});const genderLabelEl=inspector.querySelector(".insp-gender-label"),genderSel=inspector.querySelector(".insp-gender-sel"),applyGender=async ng=>{v.gender=ng,genderLabelEl&&(genderLabelEl.textContent=`${_gM[ng]||"?"} ${_gL[ng]||"\u2014"}`),genderSel&&(genderSel.value=ng);const gBadge=wrap.querySelector(".gender-badge");gBadge&&(gBadge.innerHTML=`<span class="gender-sym">${_gM[ng]||"?"}</span><span class="gender-txt">${_gL[ng]||"\u2014"}</span>`,gBadge.className="gender-badge "+(_gC[ng]||"g-n")),await saveMeta(voiceId,{gender:ng})};genderLabelEl==null||genderLabelEl.addEventListener("click",()=>{const cycle=["F","M","N"];applyGender(cycle[(cycle.indexOf(v.gender||"N")+1)%3])}),genderSel==null||genderSel.addEventListener("change",()=>applyGender(genderSel.value));const tagInput=inspector.querySelector(".insp-tag-input"),getAllKnownTags=()=>{const fromDb=(_voices||[]).flatMap(vv=>(vv.tag||"").split(",").map(t=>t.trim()).filter(Boolean)),fromStorage=getStoredTags();return[...new Set([...fromStorage,...fromDb])].sort((a,b)=>a.localeCompare(b))},tagLastToken=val=>val.split(",").pop().trimStart(),tagReplaceLastToken=(val,rep)=>{const parts=val.split(",");return parts[parts.length-1]=parts.length>1?" "+rep:rep,parts.join(",")};let _tagDrop=null;const hideTagDrop=()=>{_tagDrop==null||_tagDrop.remove(),_tagDrop=null},showTagDrop=()=>{hideTagDrop();const token=tagLastToken(tagInput.value),matches=getAllKnownTags().filter(t=>t.toLowerCase().startsWith(token.toLowerCase())&&t.toLowerCase()!==token.toLowerCase());if(!matches.length)return;_tagDrop=document.createElement("div"),_tagDrop.className="tag-suggest",matches.slice(0,10).forEach(tag=>{const btn=document.createElement("button");btn.type="button",btn.className="tag-suggest-item",btn.textContent=tag,btn.addEventListener("mousedown",e=>{e.preventDefault(),tagInput.value=tagReplaceLastToken(tagInput.value,tag),tagInput.dispatchEvent(new Event("input")),hideTagDrop(),tagInput.focus()}),_tagDrop.appendChild(btn)}),document.body.appendChild(_tagDrop);const r=tagInput.getBoundingClientRect();_tagDrop.style.top=r.bottom+3+"px",_tagDrop.style.left=r.left+"px",_tagDrop.style.minWidth=Math.max(r.width,140)+"px"};tagInput.addEventListener("input",()=>showTagDrop()),tagInput.addEventListener("focus",()=>showTagDrop()),tagInput.addEventListener("blur",()=>setTimeout(hideTagDrop,150)),tagInput.addEventListener("keydown",e=>{e.key==="Escape"&&hideTagDrop(),e.key===","&&_tagDrop&&setTimeout(showTagDrop,10)});const saveTagValue=debounce(async()=>{tagInput.value.split(",").map(t=>t.trim()).filter(Boolean).forEach(addStoredTag),v.tag=tagInput.value,await saveMeta(voiceId,{tag:tagInput.value})},700);tagInput.addEventListener("input",saveTagValue),tagInput.addEventListener("change",saveTagValue);const updateRating=async newRating=>{v.rating=newRating,wrap.dataset.rating=newRating,inspector.querySelectorAll(".insp-star").forEach(s=>s.classList.toggle("on",parseInt(s.dataset.val)<=newRating)),inspector.querySelectorAll(".insp-rating-label, .insp-meta-rating-label").forEach(el=>el.textContent=`${newRating}/5`),wrap.querySelectorAll(".vr-rating .star").forEach((s,i)=>s.classList.toggle("on",i<newRating)),await saveMeta(voiceId,{rating:newRating})};[".insp-stars",".insp-meta-stars"].forEach(sel=>{const stars=[...inspector.querySelectorAll(`${sel} .insp-star`)];stars.forEach(s=>{s.addEventListener("click",()=>{const val=parseInt(s.dataset.val);updateRating(val===(v.rating||0)?0:val)}),s.addEventListener("mouseenter",()=>{const val=parseInt(s.dataset.val);stars.forEach(ss=>ss.classList.toggle("on",parseInt(ss.dataset.val)<=val))}),s.addEventListener("mouseleave",()=>stars.forEach(ss=>ss.classList.toggle("on",parseInt(ss.dataset.val)<=(v.rating||0))))})});const noteEl=detailRow==null?void 0:detailRow.querySelector(".vr-note"),noteRowEl=inspector.querySelector(".insp-note-slot");noteEl&&noteRowEl&&noteRowEl.appendChild(noteEl);const mainRow=wrap.querySelector(".vr-main-row"),optimizer=wrap.querySelector(".vr-optimizer");mainRow&&body.appendChild(mainRow),optimizer&&body.appendChild(optimizer),wrap._loadOptimizer&&wrap._loadOptimizer().catch(e=>console.warn("Auto-load waveform failed:",e));const personaVal=v.persona||"",personaPanel=document.createElement("div");personaPanel.className="insp-persona-panel",personaPanel.innerHTML=`
<div class="insp-persona-head">
<span class="insp-persona-title">Character persona</span>
<span class="insp-persona-hint">Used by LLM to rewrite text in this voice's style. Leave empty to skip.</span>
</div>
<textarea class="insp-persona-input" rows="3" placeholder="e.g. calm British narrator with a warm tone, measured pace, formal vocabulary\u2026">${escHtml(personaVal)}</textarea>
<div class="insp-persona-actions">
<button class="btn-primary btn-sm insp-persona-save-btn">Save persona</button>
<span class="insp-persona-status"></span>
</div>
`,body.appendChild(personaPanel);const personaTextarea=personaPanel.querySelector(".insp-persona-input"),personaStatus=personaPanel.querySelector(".insp-persona-status");personaPanel.querySelector(".insp-persona-save-btn").addEventListener("click",async()=>{const pText=personaTextarea.value.trim();try{await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,persona:pText})}),v.persona=pText,personaStatus.textContent="Saved.",setTimeout(()=>{personaStatus.textContent=""},2e3),toast("Persona saved","success")}catch(e){toast("Save failed: "+e.message,"error")}});const maintTitle=body.querySelector(".opt-maintenance .opt-group-title");maintTitle&&(maintTitle.innerHTML=`<span class="opt-chevron"></span><span class="opt-title-text">Loudness <span class="opt-group-meta">Current ${escHtml(dbfs)} dBFS</span></span>`),body.querySelectorAll(".opt-group").forEach(group=>{const title=group.querySelector(":scope > .opt-group-title");title&&title.addEventListener("click",()=>{group.classList.toggle("open")})}),typeof attachSeedFinder=="function"&&attachSeedFinder(voiceId,body),wrap._extracted=[activeEl?{el:activeEl,target:detailRow}:null,deleteEl?{el:deleteEl,target:detailRow}:null,noteEl?{el:noteEl,target:detailRow}:null].filter(Boolean)}root.selectVoice=selectVoice2,root.TTVC=root.TTVC||{},root.TTVC.voiceInspector=root.TTVC.voiceInspector||{},Object.assign(root.TTVC.voiceInspector,{select:selectVoice2})}(window),function(root){var _a;const SEED_FINDER_TEXT_DE="Die dreitausendf\xFCnfhundertsiebenundsechzig neuen Ger\xE4te f\xFCr das Update ben\xF6tigten eine au\xDFergew\xF6hnlich starke K\xFChlung und regelm\xE4\xDFige Wartung, um die Leistung bei gro\xDFer Last zu gew\xE4hrleisten. Notiere dir den einundzwanzigsten Juni um vierzehn Uhr. Es ist reine Zeitverschwendung, etwas Mittelm\xE4\xDFiges zu tun! Tr\xE4ume beginnen mit einem positiven Mindset.",SEED_FINDER_TEXT_EN="The new devices for the update required exceptionally strong cooling and regular maintenance to guarantee performance under heavy load. Make a note for the twenty-first of June at two in the afternoon. It is a genuine waste of time to do something mediocre! Dreams begin with a positive mindset.",SEED_FINDER_TEXT_TORTURE="Die 3.567 neuen High-End Ger\xE4te f\xFCr das Server-Update ben\xF6tigen eine au\xDFergew\xF6hnlich starke K\xFChlung und regelm\xE4\xDFige Ma\xDFnahmen, um die Performance bei gro\xDFer Last zu gew\xE4hrleisten. - The system administrator successfully configured the customized Docker stacks and benchmarked the inference engines at exactly 8:45 AM. - Notiere dir an Midsummer 21.06. um 14 Uhr - Es ist reine Zeitverschwendung, etwas Mittelm\xE4\xDFiges zu tun! Sch\xF6ne Gr\xFC\xDFe! \u2014 Madonna - Tr\xE4ume beginnt mit einem positiven Mindset.";function _seedFinderDefaultText(voiceId){if(window._appSettings&&window._appSettings.seed_finder_text)return window._appSettings.seed_finder_text;const v=(window._voices||[]).find(x=>x.id===voiceId);if(v&&v.transcript&&v.transcript.trim())return v.transcript.trim();const lc=(voiceId||"").toLowerCase();return lc.startsWith("de_")?SEED_FINDER_TEXT_DE:lc.startsWith("en_")||lc.startsWith("gb_")?SEED_FINDER_TEXT_EN:SEED_FINDER_TEXT_DE}const SF_DB="seed-finder",SF_STORE="samples",SF_CACHE_VERSION="wav-seed-v2";function _sfHash(s){let h=0;for(let i=0;i<s.length;i++)h=h*31+s.charCodeAt(i)>>>0;return h.toString(36)}function _sfDbOpen(){return new Promise((res,rej)=>{const r=indexedDB.open(SF_DB,1);r.onupgradeneeded=e=>{const db=e.target.result;db.objectStoreNames.contains(SF_STORE)||db.createObjectStore(SF_STORE,{keyPath:"key"})},r.onsuccess=e=>res(e.target.result),r.onerror=e=>rej(e.target.error)})}async function _sfDbGet(key){try{const db=await _sfDbOpen();return await new Promise((res,rej)=>{const r=db.transaction(SF_STORE,"readonly").objectStore(SF_STORE).get(key);r.onsuccess=e=>res(e.target.result||null),r.onerror=e=>rej(e.target.error)})}catch{return null}}async function _sfDbPut(rec){try{const db=await _sfDbOpen();await new Promise((res,rej)=>{const r=db.transaction(SF_STORE,"readwrite").objectStore(SF_STORE).put(rec);r.onsuccess=()=>res(),r.onerror=e=>rej(e.target.error)})}catch{}}async function _sfDbAllForVoice(voiceId){try{const db=await _sfDbOpen();return await new Promise((res,rej)=>{const out=[],cur=db.transaction(SF_STORE,"readonly").objectStore(SF_STORE).openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&out.push(c.value),c.continue()):res(out)},cur.onerror=e=>rej(e.target.error)})}catch{return[]}}async function _sfDbClearVoice(voiceId){try{const db=await _sfDbOpen();await new Promise((res,rej)=>{const store=db.transaction(SF_STORE,"readwrite").objectStore(SF_STORE),cur=store.openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&store.delete(c.primaryKey),c.continue()):res()},cur.onerror=e=>rej(e.target.error)})}catch{}}function attachSeedFinder2(voiceId,body){const panel=document.createElement("div");panel.className="opt-group seed-finder-group",panel.innerHTML=`
<div class="opt-group-title">
<span class="opt-chevron"></span>
<span class="opt-title-text">\u{1F3B2} Seed Finder</span>
<span class="opt-group-meta">Find the most natural-sounding voice</span>
</div>
<div class="opt-group-subtitle">
Generates audio for each seed so you can listen and pick the best one.
Click "Use this seed" to save it to the TTS server.
</div>
<div class="opt-group-body seed-finder-body">
<div class="seed-finder-config">
<div class="seed-finder-row">
<label class="seed-finder-label">Test sentence</label>
<textarea class="seed-finder-text" rows="3" spellcheck="false"></textarea>
</div>
<div class="seed-finder-row seed-finder-params">
<div class="seed-finder-field">
<label>Seeds: from</label>
<input class="seed-finder-from" type="number" min="0" max="9999" value="1" step="1">
<label>to</label>
<input class="seed-finder-to" type="number" min="1" max="9999" value="15" step="1">
</div>
<div class="seed-finder-field">
<label>Backend</label>
<select class="seed-finder-backend">
<option value="voice_clone" selected>Voice Clone</option>
<option value="streaming">Streaming</option>
<option value="voice_design">Voice Design</option>
</select>
</div>
</div>
<div class="seed-finder-actions">
<button class="btn-primary seed-finder-run-btn">\u25B6 Run Seed Finder</button>
<button class="btn-secondary seed-finder-cancel-btn" style="display:none">\u2715 Cancel</button>
<button class="btn-secondary seed-finder-clear-btn" style="display:none" title="Delete the saved samples cached for this voice">\u{1F5D1} Clear saved</button>
<span class="seed-finder-status"></span>
</div>
<div class="seed-finder-row seed-finder-pin">
<label class="seed-finder-label">Or fix this voice to a specific seed</label>
<div class="seed-finder-pin-controls">
<input class="seed-finder-pin-input" type="number" min="0" max="9999" step="1" placeholder="seed #">
<button class="btn-secondary seed-finder-pin-btn">\u2605 Pin seed</button>
<button class="btn-secondary seed-finder-pin-clear" title="Use a random seed each time (unpin)">Unpin</button>
<span class="seed-finder-pin-status"></span>
</div>
</div>
</div>
<div class="seed-finder-progress" style="display:none">
<div class="seed-finder-progress-head">
<span class="seed-finder-progress-label">Generating\u2026</span>
<span class="seed-finder-progress-count">0 / 0</span>
</div>
<div class="benchmark-progress-track">
<div class="seed-finder-bar" style="width:0%"></div>
</div>
</div>
<div class="seed-finder-results"></div>
</div>
`,body.appendChild(panel);let _batchLoaded=!1;panel.querySelector(".opt-group-title").addEventListener("click",()=>{panel.classList.toggle("open"),panel.classList.contains("open")&&!_batchLoaded&&(_batchLoaded=!0,_loadBatchSamples())});const textEl=panel.querySelector(".seed-finder-text"),fromEl=panel.querySelector(".seed-finder-from"),toEl=panel.querySelector(".seed-finder-to"),backendEl=panel.querySelector(".seed-finder-backend"),runBtn=panel.querySelector(".seed-finder-run-btn"),cancelBtn=panel.querySelector(".seed-finder-cancel-btn"),clearBtn=panel.querySelector(".seed-finder-clear-btn"),statusEl=panel.querySelector(".seed-finder-status"),progressEl=panel.querySelector(".seed-finder-progress"),progLabel=panel.querySelector(".seed-finder-progress-label"),progCount=panel.querySelector(".seed-finder-progress-count"),progBar=panel.querySelector(".seed-finder-bar"),resultsEl=panel.querySelector(".seed-finder-results");textEl.value=_seedFinderDefaultText(voiceId);const pinInput=panel.querySelector(".seed-finder-pin-input"),pinBtn=panel.querySelector(".seed-finder-pin-btn"),pinClear=panel.querySelector(".seed-finder-pin-clear"),pinStatus=panel.querySelector(".seed-finder-pin-status"),voiceObj=(window._voices||[]).find(v=>v.id===voiceId)||{};voiceObj.seed!==void 0&&voiceObj.seed!==null&&(pinInput.value=voiceObj.seed,pinStatus.textContent=`\u2713 Pinned seed ${voiceObj.seed}`),voiceObj.has_ref||(backendEl.value="voice_design");let _cancelled=!1,_currentAudio=null;async function _loadBatchSamples(){try{const resp=await fetch(`/api/seed-samples/${encodeURIComponent(voiceId)}`);if(!resp.ok)return;const{seeds}=await resp.json();if(!seeds||seeds.length===0)return;resultsEl.innerHTML="";const header=document.createElement("div");header.className="sfr-batch-header",header.textContent=`${seeds.length} pre-generated sample${seeds.length!==1?"s":""} from batch run \u2014 click Play to listen`,resultsEl.appendChild(header);for(const seed of seeds){const row=document.createElement("div");row.className="seed-finder-result-row sfr-batch",row.dataset.seed=seed,row.innerHTML=`
<span class="sfr-seed">Seed ${seed}</span>
<span class="sfr-dur sfr-batch-tag">pre-generated</span>
<button class="btn-secondary sfr-play-btn"><span class="mdi mdi-play"></span> Play</button>
<button class="btn-primary sfr-use-btn">\u2605 Use seed ${seed}</button>
<span class="sfr-saved"></span>
`;let _cachedBlob=null;row.querySelector(".sfr-play-btn").addEventListener("click",async()=>{if(!_cachedBlob){const r=await fetch(`/api/seed-sample/${encodeURIComponent(voiceId)}/${seed}`);if(!r.ok){typeof toast=="function"&&toast("Failed to load sample","error");return}_cachedBlob=await r.blob()}_playBlob(_cachedBlob)}),row.querySelector(".sfr-use-btn").addEventListener("click",async()=>{try{await _saveSeed(seed),resultsEl.querySelectorAll(".sfr-saved").forEach(el=>el.textContent=""),resultsEl.querySelectorAll(".seed-finder-result-row").forEach(r=>r.classList.remove("sfr-active")),row.classList.add("sfr-active"),row.querySelector(".sfr-saved").textContent="\u2713 Saved",pinInput&&(pinInput.value=seed),typeof toast=="function"&&toast(`Seed ${seed} saved for ${voiceId}`,"success")}catch(e){typeof toast=="function"&&toast("Save failed: "+e.message,"error")}}),resultsEl.appendChild(row)}}catch{}}async function _saveSeed(seed){const resp=await fetch("/api/tts-voice-seed",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice:voiceId,seed})});if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.detail||resp.statusText)}const v=(window._voices||[]).find(vv=>vv.id===voiceId);v&&(v.seed=seed);const pinnedSeedEl=document.querySelector(".insp-pinned-seed");pinnedSeedEl&&(seed!=null?(pinnedSeedEl.textContent=`Pin Seed # ${seed}`,pinnedSeedEl.style.display=""):(pinnedSeedEl.style.display="none",pinnedSeedEl.textContent=""))}async function _fetchWithRetry(fn,tries=2,delay=1200){let lastErr;for(let t=0;t<=tries;t++){if(_cancelled)throw new Error("cancelled");try{return await fn()}catch(e){lastErr=e,t<tries&&await new Promise(r=>setTimeout(r,delay))}}throw lastErr}function _playBlob(blob){_currentAudio&&(_currentAudio.pause(),_currentAudio=null);const url=URL.createObjectURL(blob),audio=new Audio(url);_currentAudio=audio,audio.play().catch(()=>{}),audio.addEventListener("ended",()=>{URL.revokeObjectURL(url),_currentAudio=null})}const seedRows=new Map,_keyFor=(seed,text,backend)=>`${SF_CACHE_VERSION}|${voiceId}|${backend}|${_sfHash(text)}|${seed}`;async function _useSeed(seed,row){_currentAudio&&(_currentAudio.pause(),_currentAudio=null);try{await _saveSeed(seed),resultsEl.querySelectorAll(".sfr-saved").forEach(el=>el.textContent=""),resultsEl.querySelectorAll(".seed-finder-result-row").forEach(r=>r.classList.remove("sfr-active")),row.classList.add("sfr-active"),row.querySelector(".sfr-saved").textContent="\u2713 Saved",pinInput&&(pinInput.value=seed),typeof toast=="function"&&toast(`Seed ${seed} saved for ${voiceId}`,"success")}catch(e){typeof toast=="function"&&toast("Save failed: "+e.message,"error")}}function _seedMetricText(data){const parts=[];return data.dur&&parts.push(`audio ${data.dur.toFixed(1)}s${data.clipped?" clipped":""}`),data.genSec&&parts.push(`gen ${data.genSec.toFixed(1)}s`),data.rtf&&parts.push(`${data.rtf.toFixed(2)}x`),data.cached&&parts.push("saved"),parts.join(" \xB7 ")}function _renderSeed(seed,data){const row=document.createElement("div");if(row.className="seed-finder-result-row"+(data.err?" seed-finder-result-error":""),row.dataset.seed=seed,data.err)row.innerHTML=`<span class="sfr-seed">Seed ${seed}</span>
<span class="sfr-error">Failed: ${escHtml(String(data.err))}</span>
<button class="btn-secondary sfr-retry-btn"><span class="mdi mdi-refresh"></span> Retry</button>`,row.querySelector(".sfr-retry-btn").addEventListener("click",()=>_retrySeed(seed));else{const durStr=_seedMetricText(data);row.innerHTML=`<span class="sfr-seed">Seed ${seed}</span>
<span class="sfr-dur">${escHtml(durStr)}</span>
<button class="btn-secondary sfr-play-btn"><span class="mdi mdi-play"></span> Play</button>
<button class="btn-primary sfr-use-btn">\u2605 Use seed ${seed}</button>
<span class="sfr-saved"></span>`,row.querySelector(".sfr-play-btn").addEventListener("click",()=>_playBlob(data.blob)),row.querySelector(".sfr-use-btn").addEventListener("click",()=>_useSeed(seed,row))}const existing=seedRows.get(seed);existing?existing.replaceWith(row):resultsEl.appendChild(row),seedRows.set(seed,row)}async function _fetchSeedSample(seed,text,backend){const body2={text,voice:voiceId,response_format:"wav",instruct:"",backend,seed,seed_finder:!0},t0=performance.now(),r=await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body2)}),genSec=Math.max(0,(performance.now()-t0)/1e3);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const blob=await r.blob(),headerDur=parseFloat(r.headers.get("X-TTS-Audio-Duration")||""),dur=Number.isFinite(headerDur)&&headerDur>0?headerDur:Math.max(0,(blob.size-44)/(24e3*2)),clipped=String(r.headers.get("X-TTS-Audio-Clipped")||"").toLowerCase()==="true",rtf=dur>0?genSec/dur:0;return{blob,dur,genSec,rtf,clipped}}async function _generateSeed(seed,text,backend,force){const key=_keyFor(seed,text,backend);if(!force){const c=await _sfDbGet(key);if(c&&c.blob)return{blob:c.blob,dur:c.dur||0,genSec:c.genSec||0,rtf:c.rtf||0,clipped:!!c.clipped,cached:!0}}const data=await _fetchWithRetry(()=>_fetchSeedSample(seed,text,backend));return _sfDbPut({key,cacheVersion:SF_CACHE_VERSION,voiceId,backend,textHash:_sfHash(text),seed,...data,ts:Date.now()}),{...data,cached:!1}}async function _retrySeed(seed){const text=textEl.value.trim(),backend=backendEl.value,row=seedRows.get(seed),err=row&&row.querySelector(".sfr-error");err&&(err.textContent="Retrying\u2026");try{_renderSeed(seed,await _generateSeed(seed,text,backend,!0))}catch(e){_renderSeed(seed,{err:e.message||String(e)})}}async function _loadCached(){const text=textEl.value.trim(),backend=backendEl.value,th=_sfHash(text),all=(await _sfDbAllForVoice(voiceId)).filter(r=>r.cacheVersion===SF_CACHE_VERSION&&r.backend===backend&&r.textHash===th&&r.blob);if(!all.length){clearBtn.style.display="none";return}all.sort((a,b)=>a.seed-b.seed),resultsEl.innerHTML="",seedRows.clear(),all.forEach(r=>_renderSeed(r.seed,{blob:r.blob,dur:r.dur||0,genSec:r.genSec||0,rtf:r.rtf||0,clipped:!!r.clipped,cached:!0})),clearBtn.style.display="",statusEl.textContent=`${all.length} saved sample${all.length!==1?"s":""} loaded \u2014 Run to fill the rest.`}async function _runFinder(){const text=textEl.value.trim();if(!text){statusEl.textContent="Enter a test sentence.";return}const seedFrom=Math.max(0,parseInt(fromEl.value,10)||1),seedTo=Math.max(seedFrom,parseInt(toEl.value,10)||15),backend=backendEl.value,seeds=[];for(let s=seedFrom;s<=seedTo;s++)seeds.push(s);const total=seeds.length;_cancelled=!1,resultsEl.innerHTML="",seedRows.clear(),progressEl.style.display="",runBtn.style.display="none",cancelBtn.style.display="",clearBtn.style.display="none",statusEl.textContent="",progBar.style.width="0%",progCount.textContent=`0 / ${total}`,progLabel.textContent="Generating\u2026";let i=0,cached=0;for(const seed of seeds){if(_cancelled)break;progLabel.textContent=`Seed ${seed}\u2026`,progCount.textContent=`${i} / ${total}`,progBar.style.width=`${Math.round(i/total*100)}%`;try{const d=await _generateSeed(seed,text,backend,!1);d.cached&&cached++,_renderSeed(seed,d)}catch(e){if(_cancelled)break;_renderSeed(seed,{err:e.message||String(e)})}i++}progBar.style.width="100%",progCount.textContent=`${i} / ${total}`,progLabel.textContent=_cancelled?"Cancelled.":`Done \u2014 listen and pick your seed.${cached?` (${cached} from cache)`:""}`,runBtn.style.display="",cancelBtn.style.display="none",seedRows.size&&(clearBtn.style.display="")}runBtn.addEventListener("click",_runFinder),cancelBtn.addEventListener("click",()=>{_cancelled=!0}),clearBtn.addEventListener("click",async()=>{confirm("Delete the cached seed samples for this voice?")&&(await _sfDbClearVoice(voiceId),resultsEl.innerHTML="",seedRows.clear(),clearBtn.style.display="none",statusEl.textContent="Saved samples cleared.")}),_loadCached(),pinBtn.addEventListener("click",async()=>{const seed=parseInt(pinInput.value,10);if(isNaN(seed)||seed<0){pinStatus.textContent="Enter a seed number.";return}pinBtn.disabled=!0,pinStatus.textContent="Saving\u2026";try{await _saveSeed(seed),pinStatus.textContent=`\u2713 Pinned seed ${seed}`,typeof toast=="function"&&toast(`Voice ${voiceId} fixed to seed ${seed}`,"success")}catch(e){pinStatus.textContent="",typeof toast=="function"&&toast("Pin failed: "+e.message,"error")}finally{pinBtn.disabled=!1}}),pinClear.addEventListener("click",async()=>{pinClear.disabled=!0,pinStatus.textContent="Clearing\u2026";try{await _saveSeed(null),pinInput.value="",pinStatus.textContent="\u2713 Unpinned (random seed)",resultsEl.querySelectorAll(".sfr-saved").forEach(el=>el.textContent=""),resultsEl.querySelectorAll(".seed-finder-result-row").forEach(r=>r.classList.remove("sfr-active")),typeof toast=="function"&&toast(`Voice ${voiceId} unpinned \u2014 random seed`,"success")}catch(e){pinStatus.textContent="",typeof toast=="function"&&toast("Unpin failed: "+e.message,"error")}finally{pinClear.disabled=!1}})}function seedFinderBatchAll(){var _a2;const _sfUseSelected=typeof _bulkSelected!="undefined"&&_bulkSelected.size>0,ids=_sfUseSelected?[..._bulkSelected]:typeof activeVoiceIds=="function"?activeVoiceIds():(window._voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id);if(!ids.length){typeof toast=="function"&&toast("No voices selected","error");return}(_a2=document.getElementById("seedbatch-overlay"))==null||_a2.remove();const ov=document.createElement("div");ov.className="audiobook-overlay",ov.id="seedbatch-overlay",ov.innerHTML=`<div class="audiobook-box">
<div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div>
<div class="audiobook-msg">Generate &amp; cache Seed Finder samples for <b>${ids.length}</b> ${_sfUseSelected?"selected":"active"} voice${ids.length!==1?"s":""}. Already-cached seeds are skipped, so it's resumable; you can cancel anytime.</div>
<div class="seed-finder-row seed-finder-params" style="margin:12px 0">
<div class="seed-finder-field">
<label>Seeds: from</label>
<input class="sb-from" type="number" min="0" max="9999" value="1" step="1">
<label>to</label>
<input class="sb-to" type="number" min="1" max="9999" value="8" step="1">
</div>
<span class="sb-estimate seed-finder-status"></span>
</div>
<div class="audiobook-actions">
<button class="btn-secondary btn-sm" id="sb-cancel-cfg">Cancel</button>
<button class="btn-primary btn-sm" id="sb-start"><span class="mdi mdi-play"></span> Start</button>
</div>
</div>`,document.body.appendChild(ov);const box=ov.querySelector(".audiobook-box"),fromI=ov.querySelector(".sb-from"),toI=ov.querySelector(".sb-to"),est=ov.querySelector(".sb-estimate"),updEst=()=>{const f=Math.max(0,parseInt(fromI.value,10)||0),t=Math.max(f,parseInt(toI.value,10)||f);est.textContent=`${ids.length} voices \xD7 ${t-f+1} seeds = up to ${ids.length*(t-f+1)} samples`};fromI.addEventListener("input",updEst),toI.addEventListener("input",updEst),updEst(),ov.querySelector("#sb-cancel-cfg").addEventListener("click",()=>ov.remove()),ov.querySelector("#sb-start").addEventListener("click",()=>{const from=Math.max(0,parseInt(fromI.value,10)||1),to=Math.max(from,parseInt(toI.value,10)||8);_seedBatchRun(ids,from,to,box,ov)})}async function _seedBatchRun(ids,from,to,box,ov){const totalJobs=ids.length*(to-from+1);box.innerHTML=`<div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div>
<div class="audiobook-msg" id="sb-msg">Starting\u2026</div>
<div class="reader-synth-track"><div class="reader-synth-fill" id="sb-fill"></div></div>
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="sb-cancel">Cancel</button></div>`;let cancel=!1;box.querySelector("#sb-cancel").addEventListener("click",()=>{cancel=!0});const fill=box.querySelector("#sb-fill"),msg=box.querySelector("#sb-msg");let done=0,made=0,cached=0,failed=0;for(let vi=0;vi<ids.length&&!cancel;vi++){const voiceId=ids[vi],text=_seedFinderDefaultText(voiceId),th=_sfHash(text),backend=typeof _ttsBackendForVoice=="function"?_ttsBackendForVoice(voiceId,"voice_clone"):"voice_clone";for(let seed=from;seed<=to&&!cancel;seed++){msg.textContent=`Voice ${vi+1}/${ids.length} \xB7 seed ${seed} \u2014 ${voiceId}`;const key=`${SF_CACHE_VERSION}|${voiceId}|${backend}|${th}|${seed}`,existing=await _sfDbGet(key);if(existing&&existing.blob){cached++,done++,fill.style.width=done/totalJobs*100+"%";continue}try{const body={text,voice:voiceId,response_format:"wav",instruct:"",backend,seed,seed_finder:!0},t0=performance.now(),resp=await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)}),genSec=Math.max(0,(performance.now()-t0)/1e3);if(!resp.ok)throw new Error(resp.statusText);const blob=await resp.blob(),headerDur=parseFloat(resp.headers.get("X-TTS-Audio-Duration")||""),dur=Number.isFinite(headerDur)&&headerDur>0?headerDur:Math.max(0,(blob.size-44)/(24e3*2)),clipped=String(resp.headers.get("X-TTS-Audio-Clipped")||"").toLowerCase()==="true",rtf=dur>0?genSec/dur:0;await _sfDbPut({key,cacheVersion:SF_CACHE_VERSION,voiceId,backend,textHash:th,seed,blob,dur,genSec,rtf,clipped,ts:Date.now()}),made++}catch{failed++}done++,fill.style.width=done/totalJobs*100+"%"}}ov.remove(),typeof toast=="function"&&toast(cancel?`Stopped \u2014 ${made} generated, ${cached} already cached`:`Batch done \u2014 ${made} generated, ${cached} cached${failed?`, ${failed} failed`:""}`,cancel?"error":"success")}typeof $=="function"&&((_a=$("seed-batch-all-btn"))==null||_a.addEventListener("click",seedFinderBatchAll)),Object.assign(root,{attachSeedFinder:attachSeedFinder2,seedFinderBatchAll}),root.TTVC=root.TTVC||{},root.TTVC.seedFinder=root.TTVC.seedFinder||{},Object.assign(root.TTVC.seedFinder,{attach:attachSeedFinder2,batchAll:seedFinderBatchAll})}(window),function(root){var _a,_b,_c;const DEFAULT_VOICE_SOURCE_URLS=["https://aiartes.com/voiceai","https://sample-files.com/downloads/audio/wav/voice-sample.wav","https://freesound.org/people/Scott%20Simpson/","https://lanceblairvo.com/raw-voiceover-samples/","https://github.com/yaph/tts-samples/tree/main/mp3","https://github.com/jim-schwoebel/voice_datasets"],VOICE_SOURCE_STORAGE_KEY="ttsvc-getvoices-sources";let _voiceSourcePayload=null,_voiceSourceItems=[];function sourceTextareaValueFromDefaults(){return DEFAULT_VOICE_SOURCE_URLS.join(`
`)}function initGetVoiceSourcesEditor(){const box=$("getvoices-sources");!box||box.dataset.ready||(box.value=localStorage.getItem(VOICE_SOURCE_STORAGE_KEY)||sourceTextareaValueFromDefaults(),box.dataset.ready="1",box.addEventListener("input",()=>{localStorage.setItem(VOICE_SOURCE_STORAGE_KEY,box.value),_voiceSourcePayload=null,$("getvoices-status").textContent="Source list changed."}))}function getEditableVoiceSourceUrls(){var _a2;return initGetVoiceSourcesEditor(),(((_a2=$("getvoices-sources"))==null?void 0:_a2.value)||"").split(/\r?\n/).map(line=>line.trim()).filter(line=>line&&!line.startsWith("#"))}function getVoiceSourceItems(){return(_voiceSourcePayload&&_voiceSourcePayload.sources||[]).flatMap(src=>(src.items||[]).map(item=>({...item,_sourceName:src.name,_sourceHomepage:src.homepage})))}function voiceSourceSearchText(item){return[item.name,item.kind,item.category,item.language,item.gender,item.description,item.source,item._sourceName].join(" ").toLowerCase()}function setOptions(selectId,values,allLabel){const sel=$(selectId);if(!sel)return;const current=sel.value||"all";sel.innerHTML=`<option value="all">${escHtml(allLabel)}</option>`+values.map(value=>`<option value="${escHtml(value)}">${escHtml(value)}</option>`).join(""),sel.value=values.includes(current)?current:"all"}function renderGetVoices(){var _a2,_b2,_c2,_d,_e,_f;initGetVoiceSourcesEditor();const list=$("getvoices-list"),summary=$("getvoices-summary");if(!list||!summary)return;const payload=_voiceSourcePayload||{sources:[],total:0,direct_audio:0,errors:[]},sources=payload.sources||[],sourceFilter=((_a2=$("getvoices-source-filter"))==null?void 0:_a2.value)||"all",languageFilter=((_b2=$("getvoices-language-filter"))==null?void 0:_b2.value)||"all",genderFilter=((_c2=$("getvoices-gender-filter"))==null?void 0:_c2.value)||"all",filetypeFilter=((_d=$("getvoices-filetype-filter"))==null?void 0:_d.value)||"all",q=(((_e=$("getvoices-search"))==null?void 0:_e.value)||"").trim().toLowerCase(),directOnly=!!((_f=$("getvoices-direct-only"))!=null&&_f.checked);_voiceSourceItems=getVoiceSourceItems(),summary.innerHTML=[[`${payload.total||0}`,"Items found"],[`${payload.direct_audio||0}`,"Direct audio"],[`${sources.length}`,"Sources OK"],[`${(payload.errors||[]).length}`,"Errors"]].map(([value,label])=>`<div class="insight"><strong>${escHtml(value)}</strong><span>${escHtml(label)}</span></div>`).join(""),setOptions("getvoices-source-filter",sources.map(src=>src.id).filter(Boolean),"Source: all");const sourceSelect=$("getvoices-source-filter");sourceSelect&&[...sourceSelect.options].forEach(option=>{if(option.value==="all")return;const src=sources.find(s=>s.id===option.value);src&&(option.textContent=src.name||src.id)});const languages=[...new Set(_voiceSourceItems.map(item=>item.language||"Unknown"))].sort((a,b)=>a.localeCompare(b)),genders=[...new Set(_voiceSourceItems.map(item=>item.gender||"Unknown"))].sort((a,b)=>a.localeCompare(b)),filetypes=[...new Set(_voiceSourceItems.map(item=>(item.file_type||(item.direct_audio?"audio":"page")).toUpperCase()))].sort((a,b)=>a.localeCompare(b));setOptions("getvoices-language-filter",languages,"Language: all"),setOptions("getvoices-gender-filter",genders,"Sex: all"),setOptions("getvoices-filetype-filter",filetypes,"Filetype: all");let items=_voiceSourceItems.filter(item=>{if(sourceFilter!=="all"&&item.source_id!==sourceFilter||languageFilter!=="all"&&(item.language||"Unknown")!==languageFilter||genderFilter!=="all"&&(item.gender||"Unknown")!==genderFilter)return!1;const itemFiletype=(item.file_type||(item.direct_audio?"audio":"page")).toUpperCase();return!(filetypeFilter!=="all"&&itemFiletype!==filetypeFilter||directOnly&&!item.direct_audio||q&&!voiceSourceSearchText(item).includes(q))});const shown=items.slice(0,240),more=items.length-shown.length;if(!shown.length){const errorText=(payload.errors||[]).map(e=>`${e.source}: ${e.detail}`).join(" | ");list.innerHTML=`<div class="card"><p class="note">No matching sources found.${errorText?" Source errors: "+escHtml(errorText):""}</p></div>`;return}list.innerHTML=shown.map(item=>{const thumb=item.image_url?`<img class="voice-source-thumb" src="${escHtml(item.image_url)}" alt="" loading="lazy" decoding="async">`:'<div class="voice-source-thumb"></div>',audio=item.audio_url?`<audio controls preload="none" src="${escHtml(item.audio_url)}"></audio>`:"",audioLink=item.audio_url?`<a class="btn-secondary" href="${escHtml(item.audio_url)}" target="_blank" rel="noopener">Open audio</a>`:"",canGetVoice=!!(item.import_url||item.audio_url),importUrl=item.import_url||item.audio_url,getVoice=canGetVoice?`<button class="btn-primary get-source-voice" data-url="${escHtml(importUrl)}" data-name="${escHtml(item.name||"")}" data-image="${escHtml(item.image_url||"")}" data-language="${escHtml(item.language||"")}" data-gender="${escHtml(item.gender||"")}" data-kind="${escHtml(item.kind||"")}" data-description="${escHtml(item.description||"")}" data-page="${escHtml(item.page_url||item._sourceHomepage||"")}">Import this voice</button>`:"",type=item.file_type?`<span class="voice-source-pill">${escHtml(String(item.file_type).toUpperCase())}</span>`:"",language=item.language?`<span class="voice-source-pill">${escHtml(item.language)}</span>`:"",gender=item.gender?`<span class="voice-source-pill">${escHtml(item.gender)}</span>`:"";return`<div class="voice-source-card">
<div class="voice-source-head">
${thumb}
<div class="voice-source-title">
<strong title="${escHtml(item.name||"")}">${escHtml(item.name||"Untitled")}</strong>
<span>${escHtml(item._sourceName||item.source||"")}${item.category?" \xB7 "+escHtml(item.category):""}</span>
</div>
</div>
<div class="voice-source-desc">${escHtml(item.kind||"")}${item.description?" - "+escHtml(item.description):""}</div>
${audio}
<div class="voice-source-actions">
${type}${language}${gender}
${audioLink}
<a class="btn-secondary" href="${escHtml(item.page_url||item._sourceHomepage||"#")}" target="_blank" rel="noopener">Source page</a>
<button class="btn-secondary copy-source-url" data-url="${escHtml(item.audio_url||item.page_url||"")}">Copy URL</button>
</div>
${getVoice?`<div class="vs-import-footer">${getVoice}</div>`:""}
</div>`}).join("")+(more>0?`<div class="card"><p class="note">${more} more matches. Narrow the search or filters to see them.</p></div>`:"")}async function loadGetVoices(force=!1){if(initGetVoiceSourcesEditor(),_voiceSourcePayload&&!force){renderGetVoices();return}const urls=getEditableVoiceSourceUrls(),list=$("getvoices-list");if(!urls.length){list&&(list.innerHTML='<div class="card"><p class="note">Add at least one source URL, then scrape again.</p></div>'),$("getvoices-status").textContent="No sources.";return}list&&(list.innerHTML=loadingMarkup("Scraping voice sources",`Fetching ${urls.length} source${urls.length===1?"":"s"} from the editable list.`,6)),$("getvoices-status").textContent="Scraping...",$("getvoices-refresh-btn").disabled=!0;try{const r=await fetch("/api/voice-sources",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({urls})});if(!r.ok){const text=await r.text().catch(()=>"");let message=r.statusText||`HTTP ${r.status}`;try{message=JSON.parse(text).detail||message}catch{text&&(message=text.slice(0,160))}throw new Error(message)}_voiceSourcePayload=await r.json(),renderGetVoices();const errors=(_voiceSourcePayload.errors||[]).length;$("getvoices-status").textContent=`${_voiceSourcePayload.total||0} found${errors?`, ${errors} source errors`:""}`}catch(e){list&&(list.innerHTML=`<div class="card"><p style="color:var(--red)">Scrape failed: ${escHtml(e.message)}</p><p class="note">Check that the TTS Voice Creator backend is restarted and that at least one source URL is reachable.</p></div>`),$("getvoices-status").textContent="Scrape failed",toast("Voice source scrape failed: "+e.message,"error")}finally{$("getvoices-refresh-btn").disabled=!1}}["getvoices-search","getvoices-source-filter","getvoices-language-filter","getvoices-gender-filter","getvoices-filetype-filter","getvoices-direct-only"].forEach(id=>{const el=$(id);el&&el.addEventListener(id==="getvoices-search"?"input":"change",renderGetVoices)}),(_a=$("getvoices-refresh-btn"))==null||_a.addEventListener("click",()=>loadGetVoices(!0)),(_b=$("getvoices-reset-sources-btn"))==null||_b.addEventListener("click",()=>{const box=$("getvoices-sources");box&&(box.value=sourceTextareaValueFromDefaults(),localStorage.setItem(VOICE_SOURCE_STORAGE_KEY,box.value),_voiceSourcePayload=null,renderGetVoices(),$("getvoices-status").textContent="Source list reset.")});const SOURCE_LANGUAGE_CODES={english:"EN",german:"DE",deutsch:"DE",french:"FR",spanish:"ES",japanese:"JA",korean:"KO",italian:"IT",portuguese:"PT",russian:"RU",arabic:"AR",polish:"PL",dutch:"NL",swedish:"SV",turkish:"TR",hindi:"HI",chinese:"ZH"};function sourceLanguageCode(language){const raw=String(language||"").trim();return/^[A-Za-z]{2}$/.test(raw)?raw.toUpperCase():SOURCE_LANGUAGE_CODES[raw.toLowerCase()]||"EN"}function sourceGenderCode(gender,name=""){const text=`${gender||""} ${name||""}`.toLowerCase();return/female|woman|girl|\bf\b/.test(text)?"F":/male|man|boy|\bm\b/.test(text)?"M":"N"}function suggestedVoiceIdFromSourceName(name,language="EN",gender="N"){const base=String(name||"SourceVoice").normalize("NFKD").replace(/[\u0300-\u036f]/g,"").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,48)||"SourceVoice";return`${language||"EN"}_${gender||"N"}_${base}`}function setLibAddSourcePreview2(meta={}){const sourceBox=document.querySelector(".lib-add-source-box");if(!sourceBox)return;let preview=$("lib-add-source-preview");if(preview||(preview=document.createElement("div"),preview.id="lib-add-source-preview",preview.className="lib-add-source-preview",sourceBox.appendChild(preview)),!meta.name&&!meta.imageUrl){preview.classList.remove("open"),preview.innerHTML="";return}const image=meta.imageUrl?`<img src="${escHtml(meta.imageUrl)}" alt="">`:'<div class="voice-source-thumb"></div>';preview.innerHTML=`${image}<div style="min-width:0"><strong>${escHtml(meta.name||"Source voice")}</strong><span>${escHtml([meta.language,meta.gender,meta.kind].filter(Boolean).join(" \xB7 ")||"Source metadata will be saved with the voice")}</span></div>`,preview.classList.add("open")}async function getSourceVoiceInLibrary(meta){if(!meta.url){toast("This source has no direct audio URL","error");return}const lang=sourceLanguageCode(meta.language),gender=sourceGenderCode(meta.gender,meta.name),voiceId=suggestedVoiceIdFromSourceName(meta.name,lang,gender);toast(`Importing ${meta.name||"voice"}\u2026`,"info");try{const r=await fetch("/api/quick-import-voice",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({audio_url:meta.url,voice_id:voiceId,transcript:""})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();let srcTag="online";try{meta.pageUrl&&(srcTag=new URL(meta.pageUrl).hostname.replace(/^www\./,"").split(".")[0]||"online")}catch{}typeof saveMeta=="function"&&await saveMeta(d.voice_id,{name:meta.name||d.voice_id,tag:srcTag,group:srcTag,origin:"cloned",gender:(gender||"").toUpperCase(),note:(meta.description||"").slice(0,180)}).catch(()=>{}),meta.imageUrl&&await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:d.voice_id,image_url:meta.imageUrl})}).catch(()=>{}),_pendingSelectId=d.voice_id,navTo("s-voices"),_libraryLoadPromise=null,await loadVoiceLibrary(),toast(`Saved as ${d.voice_id}`,"success")}catch(e){toast("Import failed: "+e.message,"error")}}(_c=$("getvoices-list"))==null||_c.addEventListener("click",async e=>{const getBtn=e.target.closest(".get-source-voice");if(getBtn){await getSourceVoiceInLibrary({url:getBtn.dataset.url||"",name:getBtn.dataset.name||"",imageUrl:getBtn.dataset.image||"",language:getBtn.dataset.language||"",gender:getBtn.dataset.gender||"",kind:getBtn.dataset.kind||"",description:getBtn.dataset.description||"",pageUrl:getBtn.dataset.page||""});return}const btn=e.target.closest(".copy-source-url");btn&&(await copyText(btn.dataset.url||""),toast("Source URL copied","success"))}),Object.assign(root,{DEFAULT_VOICE_SOURCE_URLS,VOICE_SOURCE_STORAGE_KEY,_voiceSourcePayload,_voiceSourceItems,sourceTextareaValueFromDefaults,initGetVoiceSourcesEditor,getEditableVoiceSourceUrls,getVoiceSourceItems,voiceSourceSearchText,setOptions,renderGetVoices,loadGetVoices,SOURCE_LANGUAGE_CODES,sourceLanguageCode,sourceGenderCode,suggestedVoiceIdFromSourceName,setLibAddSourcePreview:setLibAddSourcePreview2,getSourceVoiceInLibrary}),root.TTVC=root.TTVC||{},root.TTVC.voiceSources=root.TTVC.voiceSources||{},Object.assign(root.TTVC.voiceSources,{list:getVoiceSourceItems,load:loadGetVoices,import:getSourceVoiceInLibrary,sourceLanguageCode,sourceGenderCode})}(window),function(){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l;const $2=id=>document.getElementById(id),grid=$2("fa-grid");if(!grid)return;let _page=1,_audio=null,_playingCard=null;const esc=s=>String(s==null?"":s).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");function fmtCount(n){return n=+n||0,n>=1e6?(n/1e6).toFixed(1).replace(/\.0$/,"")+"M":n>=1e3?(n/1e3).toFixed(1).replace(/\.0$/,"")+"K":String(n)}const FLAG={EN:"\u{1F1EC}\u{1F1E7}",DE:"\u{1F1E9}\u{1F1EA}",FR:"\u{1F1EB}\u{1F1F7}",ES:"\u{1F1EA}\u{1F1F8}",IT:"\u{1F1EE}\u{1F1F9}",PT:"\u{1F1F5}\u{1F1F9}",NL:"\u{1F1F3}\u{1F1F1}",JA:"\u{1F1EF}\u{1F1F5}",ZH:"\u{1F1E8}\u{1F1F3}",KO:"\u{1F1F0}\u{1F1F7}",RU:"\u{1F1F7}\u{1F1FA}",AR:"\u{1F1F8}\u{1F1E6}",PL:"\u{1F1F5}\u{1F1F1}"};function status2(msg,show=!0){const bar=$2("fa-status-bar"),txt=$2("fa-status-text");bar&&(bar.hidden=!show),txt&&(txt.textContent=msg||"")}function activeVals(group){return[...document.querySelectorAll(`#fa-filter-pop .fa-pchips[data-group="${group}"] .fa-pchip.active`)].map(c=>c.dataset.val).filter(Boolean)}function filterCount(){var _a2;return activeVals("gender").length+activeVals("age").length+activeVals("tag").length+((_a2=$2("fa-tag"))!=null&&_a2.value.trim()?1:0)}function updateFilterBadge(){const n=filterCount(),b=$2("fa-filter-badge"),btn=$2("fa-filter-btn");b&&(b.textContent=String(n),b.hidden=n===0),btn==null||btn.classList.toggle("active",n>0)}function params(){var _a2,_b2,_c2,_d2;const p=new URLSearchParams({page:String(_page),page_size:"24"}),s=(_a2=$2("fa-search"))==null?void 0:_a2.value.trim();s&&p.set("search",s);const l=(_b2=$2("fa-lang"))==null?void 0:_b2.value;l&&p.set("language",l);const sort=(_c2=$2("fa-sort"))==null?void 0:_c2.value;sort&&p.set("sort_by",sort);const g=activeVals("gender")[0];g&&p.set("gender",g);const a=activeVals("age")[0];a&&p.set("age",a),activeVals("tag").forEach(t2=>p.append("tag",t2));const t=(_d2=$2("fa-tag"))==null?void 0:_d2.value.trim();return t&&p.append("tag",t),p}function card(v){const can=!!v.sample_audio,meta=[v.language?`<span class="fa-meta-chip">${FLAG[v.language]||"\u{1F310}"} ${esc(v.language)}</span>`:"",v.gender?`<span class="fa-meta-chip">${esc(v.gender)}</span>`:"",v.age?`<span class="fa-meta-chip">${esc(v.age)}</span>`:""].join(""),avatar=v.image?`<img class="fa-avatar" src="${esc(v.image)}" alt="" loading="lazy" onerror="this.parentElement.classList.add('fa-avatar-fallback');this.remove();">`:"";return`<div class="fa-card${can?"":" fa-card-disabled"}" data-id="${esc(v.id)}">
<div class="fa-card-avatar${v.image?"":" fa-avatar-fallback"}">
${avatar}
${can?'<button class="fa-play" title="Preview"><span class="mdi mdi-play"></span></button>':""}
</div>
<div class="fa-card-body">
<div class="fa-card-top">
<span class="fa-card-name" title="${esc(v.title)}">${esc(v.title)||"Untitled"}</span>
${v.author?`<span class="fa-card-author">\xB7 ${esc(v.author)}</span>`:""}
</div>
<div class="fa-card-desc">${esc(v.description||v.sample_text||"")}</div>
<div class="fa-card-meta">${meta}</div>
<div class="fa-card-foot">
<span class="fa-stat" title="Generations"><span class="mdi mdi-poll"></span> ${fmtCount(v.plays)}</span>
<span class="fa-stat" title="Likes"><span class="mdi mdi-heart-outline"></span> ${fmtCount(v.likes)}</span>
${can?'<button class="btn-primary btn-sm fa-import" title="Import as a clonable voice"><span class="mdi mdi-download"></span> Use</button>':""}
</div>
</div>
</div>`}function render(data){const items=data.items||[];items.length?(grid.innerHTML=items.map(card).join(""),grid.querySelectorAll(".fa-card").forEach((el,i)=>{var _a2,_b2;const v=items[i];(_a2=el.querySelector(".fa-play"))==null||_a2.addEventListener("click",e=>{e.stopPropagation(),preview(v,el)}),(_b2=el.querySelector(".fa-import"))==null||_b2.addEventListener("click",e=>{e.stopPropagation(),importVoice(v,el)})})):grid.innerHTML='<div class="el-idle">No voices found \u2014 try a different search, language or filter.</div>';const pager=$2("fa-pager");if(pager){pager.hidden=!1,$2("fa-prev").disabled=_page<=1;const pages=Math.max(1,Math.ceil((data.total||0)/(data.page_size||24)));$2("fa-next").disabled=_page>=pages,$2("fa-pager-info").textContent=`Page ${_page} \xB7 ${(data.total||0).toLocaleString()} voices`}}async function browse(){status2("Loading voices from fish.audio\u2026"),grid.innerHTML='<div class="el-idle"><span class="reh-imsdb-spinner"></span> Loading\u2026</div>';try{const d=await fetch("/api/fishaudio/voices?"+params().toString()).then(r=>{if(!r.ok)throw new Error("HTTP "+r.status);return r.json()});render(d),status2(d.offline?"\u26A0 fish.audio unreachable \u2014 showing cached results":`${(d.total||0).toLocaleString()} matching voices`,!!d.offline)}catch(e){grid.innerHTML=`<div class="el-idle">Failed to load: ${esc(e.message)}</div>`,status2("")}}function stopPreview(){_audio&&(_audio.pause(),_audio=null),_playingCard==null||_playingCard.classList.remove("fa-card-playing"),_playingCard=null}function preview(v,el){if(_playingCard===el){stopPreview();return}stopPreview(),v.sample_audio&&(_audio=new Audio(v.sample_audio),_playingCard=el,el.classList.add("fa-card-playing"),_audio.addEventListener("ended",stopPreview),_audio.play().catch(()=>{stopPreview(),typeof toast=="function"&&toast("Could not play preview","error")}))}async function importVoice(v,el){const btn=el.querySelector(".fa-import"),orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML='<span class="reh-imsdb-spinner"></span>';const lang=(v.language||"EN").slice(0,2).toUpperCase(),base=(typeof _umlautSafe=="function"?_umlautSafe(v.title||"fishaudio"):v.title||"fishaudio").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,40)||"Voice",voiceId=`${lang}_${base}`;try{const r=await fetch("/api/quick-import-voice",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,audio_url:v.sample_audio,transcript:v.sample_text||v.default_text||""})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();typeof saveMeta=="function"&&await saveMeta(d.voice_id,{name:v.title||base,tag:"fish-audio",group:"fish-audio",origin:"cloned",gender:(v.gender||"").charAt(0).toUpperCase(),note:(v.description||"").slice(0,180)}).catch(()=>{}),v.image&&await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:d.voice_id,image_url:v.image})}).catch(()=>{}),btn.innerHTML='<span class="mdi mdi-check"></span> Imported',el.classList.add("fa-card-imported"),typeof toast=="function"&&toast(`Imported "${v.title}" as ${d.voice_id}`,"success"),typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().catch(()=>{})}catch(e){btn.disabled=!1,btn.innerHTML=orig,typeof toast=="function"&&toast("Import failed: "+e.message,"error")}}(_a=$2("fa-fetch"))==null||_a.addEventListener("click",()=>{_page=1,browse()}),(_b=$2("fa-search"))==null||_b.addEventListener("keydown",e=>{e.key==="Enter"&&(_page=1,browse())}),(_c=$2("fa-tag"))==null||_c.addEventListener("keydown",e=>{e.key==="Enter"&&(_page=1,browse(),applyFilters())}),(_d=$2("fa-lang"))==null||_d.addEventListener("change",()=>{_page=1,browse()}),(_e=$2("fa-sort"))==null||_e.addEventListener("change",()=>{_page=1,browse()}),(_f=$2("fa-prev"))==null||_f.addEventListener("click",()=>{_page>1&&(_page--,browse())}),(_g=$2("fa-next"))==null||_g.addEventListener("click",()=>{_page++,browse()});const pop=$2("fa-filter-pop");function positionPop(){const btn=$2("fa-filter-btn");if(!pop||!btn||pop.hidden)return;const r=btn.getBoundingClientRect(),w=pop.offsetWidth||320,margin=8;let left=Math.min(r.right-w,window.innerWidth-w-margin);left<margin&&(left=margin),pop.style.position="fixed",pop.style.right="auto",pop.style.left=left+"px",pop.style.top=r.bottom+8+"px",pop.style.maxHeight=window.innerHeight-r.bottom-16+"px"}function openPop(show){pop&&(pop.hidden=show===void 0?!pop.hidden:!show,pop.hidden||positionPop())}(_h=$2("fa-filter-btn"))==null||_h.addEventListener("click",e=>{e.stopPropagation(),openPop()}),(_i=$2("fa-filter-close"))==null||_i.addEventListener("click",e=>{e.stopPropagation(),openPop(!1)}),pop==null||pop.addEventListener("click",e=>e.stopPropagation()),document.addEventListener("click",()=>openPop(!1)),document.addEventListener("keydown",e=>{e.key==="Escape"&&openPop(!1)}),window.addEventListener("resize",positionPop),window.addEventListener("scroll",positionPop,!0),document.querySelectorAll("#fa-filter-pop .fa-pchip").forEach(chip=>{chip.addEventListener("click",()=>{const wrap=chip.closest(".fa-pchips");wrap.classList.contains("fa-pchips-multi")?chip.classList.toggle("active"):(wrap.querySelectorAll(".fa-pchip").forEach(c=>c.classList.remove("active")),chip.classList.add("active")),updateFilterBadge()})}),(_j=$2("fa-tag"))==null||_j.addEventListener("input",updateFilterBadge);function applyFilters(){_page=1,openPop(!1),browse()}(_k=$2("fa-filter-apply"))==null||_k.addEventListener("click",applyFilters),(_l=$2("fa-filter-reset"))==null||_l.addEventListener("click",()=>{document.querySelectorAll('#fa-filter-pop .fa-pchips[data-group="tag"] .fa-pchip').forEach(c=>c.classList.remove("active")),["gender","age"].forEach(g=>{const wrap=document.querySelector(`#fa-filter-pop .fa-pchips[data-group="${g}"]`);wrap==null||wrap.querySelectorAll(".fa-pchip").forEach((c,i)=>c.classList.toggle("active",i===0))}),$2("fa-tag")&&($2("fa-tag").value=""),updateFilterBadge(),applyFilters()})}(),function(){const tabs=document.getElementById("gvo-tabs");if(!tabs)return;const map={direct:"tab-getvoices",fish:"fa-browser-card",eleven:"el-browser-card"};function show(src){map[src]||(src="direct"),Object.entries(map).forEach(([k,id])=>{const el=document.getElementById(id);el&&(k===src?el.style.removeProperty("display"):el.style.setProperty("display","none","important"))}),tabs.querySelectorAll(".gvo-tab").forEach(t=>t.classList.toggle("active",t.dataset.src===src));try{localStorage.setItem("gvo-src",src)}catch{}}tabs.querySelectorAll(".gvo-tab").forEach(t=>t.addEventListener("click",()=>show(t.dataset.src)));let saved="direct";try{saved=localStorage.getItem("gvo-src")||"direct"}catch{}show(saved)}(),function(){const KEY="ttsvc_voice_library_cache_v2";let inFlight=null;function read(){try{const value=JSON.parse(sessionStorage.getItem(KEY)||"null");return!value||!Array.isArray(value.voices)||!Number.isFinite(value.savedAt)?null:Date.now()-value.savedAt<=3e4?value.voices:null}catch{return null}}function write(voices){try{sessionStorage.setItem(KEY,JSON.stringify({savedAt:Date.now(),voices}))}catch{}}function invalidate(){try{sessionStorage.removeItem(KEY)}catch{}}async function fetchVoices(options={}){const refresh=options===!0||options.refresh===!0;if(!refresh){const cached=read();if(cached)return cached}return inFlight||(inFlight=fetch("/api/voices"+(refresh?"?refresh=1":""),{cache:"no-store"}).then(async response=>{if(!response.ok){const error=await response.json().catch(()=>({}));throw new Error(error.detail||response.statusText)}const voices=await response.json();if(!Array.isArray(voices))throw new Error("Voice library response was not an array");return write(voices),voices}).finally(()=>{inFlight=null}),inFlight)}window.fetchVoiceLibrary=fetchVoices,window.invalidateVoiceLibraryCache=invalidate}(),function(){const list=document.getElementById("background-jobs-list"),refresh=document.getElementById("background-jobs-refresh"),diagnostics=document.getElementById("resource-diagnostics"),diagnosticsRefresh=document.getElementById("resource-diagnostics-refresh");if(!list||!refresh)return;const labels={transcription:"Transcription",voice_design:"Voice design",casting:"Voice casting",consistent_images:"Consistent images"};let loading=!1,diagnosticsLoading=!1;function node(value,fallback="\u2014"){const item=document.createElement("span");return item.textContent=value==null||value===""?fallback:String(value),item}function statusClass(job){return job.cancelled||job.stage==="cancelled"?"is-cancelled":job.error||job.stage==="error"||job.stage==="failed"?"is-error":job.done?"is-done":"is-active"}function statusLabel(job){return job.cancelled||job.stage==="cancelled"?"Cancelled":job.error||job.stage==="error"||job.stage==="failed"?"Failed":job.done?"Completed":job.stage==="queued"?"Queued":"Running"}function cancelEndpoint(kind,id){const paths={transcription:"/api/transcribe/cancel/",voice_design:"/api/voice-design/cancel/",casting:"/api/match-characters-voices/cancel/",consistent_images:"/api/character-generate-consistent-images/cancel/"};return paths[kind]?paths[kind]+encodeURIComponent(id):""}function retryEndpoint(kind,id){const paths={transcription:"/api/transcribe/retry/",voice_design:"/api/voice-design/retry/",casting:"/api/match-characters-voices/retry/",consistent_images:"/api/character-generate-consistent-images/retry/"};return paths[kind]&&id?paths[kind]+encodeURIComponent(id):""}function render(jobs){if(list.replaceChildren(),!jobs.length){const empty=node("No background jobs yet. Start a transcription, voice design, cast, or image workflow to see it here.");empty.className="perf-history-empty",list.appendChild(empty);return}jobs.forEach(job=>{const row=document.createElement("div");row.className="background-job-row "+statusClass(job);const main=document.createElement("div");main.className="background-job-main";const title=document.createElement("strong");title.textContent=labels[job.kind]||job.kind||"Background job",main.appendChild(title);const id=document.createElement("code");id.textContent=String(job.job_id||"").slice(0,12),main.appendChild(id);const detail=node(job.error||job.detail||job.stage);detail.className="background-job-detail",main.appendChild(detail);const meta=node(`${statusLabel(job)} \xB7 ${Number(job.elapsed||0).toFixed(1)}s`);meta.className="background-job-meta",main.appendChild(meta);const storedResult=job.result&&typeof job.result=="object"?job.result:{},artifactList=[];storedResult.artifact&&artifactList.push(storedResult.artifact),Array.isArray(storedResult.artifacts)&&artifactList.push(...storedResult.artifacts),artifactList.filter(item=>item&&typeof item.url=="string"&&item.url.startsWith("/api/jobs/")).forEach(item=>{const link=document.createElement("a");link.className="background-job-artifact",link.href=item.url,link.target="_blank",link.rel="noopener",link.textContent=`Download ${item.kind||"result"}`,main.appendChild(link)}),row.appendChild(main);const actions=document.createElement("div");if(actions.className="background-job-actions",job.done&&job.error&&job.retry_available){const button=document.createElement("button");button.type="button",button.className="btn-secondary btn-sm",button.textContent="Retry",button.addEventListener("click",async()=>{button.disabled=!0;try{const response=await fetch(retryEndpoint(job.kind,job.job_id),{method:"POST"});if(!response.ok){const detail2=await response.json().catch(()=>({}));throw new Error(detail2.detail||`HTTP ${response.status}`)}await load()}catch(error){button.disabled=!1,typeof toast=="function"&&toast("Could not retry job: "+error.message,"error")}}),actions.appendChild(button)}if(!job.done&&!job.cancelled&&job.job_id){const endpoint=cancelEndpoint(job.kind,job.job_id);if(endpoint){const button=document.createElement("button");button.type="button",button.className="btn-secondary btn-sm",button.textContent="Cancel",button.addEventListener("click",async()=>{button.disabled=!0;try{const response=await fetch(endpoint,{method:"POST"});if(!response.ok)throw new Error(`HTTP ${response.status}`);await load()}catch(error){button.disabled=!1,typeof toast=="function"&&toast("Could not cancel job: "+error.message,"error")}}),actions.appendChild(button)}}const note=node("Reopen source to retry");note.className="background-job-retry-note",note.title="Inputs are not retained for replay, by design.",(!job.done||!job.error||!job.retry_available)&&actions.appendChild(note),row.appendChild(actions),list.appendChild(row)})}async function load(){if(!loading){loading=!0,refresh.disabled=!0;try{const response=await fetch("/api/jobs?limit=50",{cache:"no-store"});if(!response.ok)throw new Error(`HTTP ${response.status}`);const payload=await response.json();render(Array.isArray(payload.jobs)?payload.jobs:[])}catch(error){list.replaceChildren();const empty=node("Background job history unavailable: "+error.message);empty.className="perf-history-empty",list.appendChild(empty)}finally{loading=!1,refresh.disabled=!1}}}function formatBytes(value){const bytes=Number(value);return Number.isFinite(bytes)?bytes<1024*1024?`${(bytes/1024).toFixed(1)} KiB`:`${(bytes/1024/1024).toFixed(1)} MiB`:"\u2014"}async function loadDiagnostics(){var _a,_b,_c,_d,_e,_f;if(!(!diagnostics||diagnosticsLoading)){diagnosticsLoading=!0,diagnosticsRefresh&&(diagnosticsRefresh.disabled=!0);try{const response=await fetch("/api/diagnostics",{cache:"no-store"});if(!response.ok)throw new Error(`HTTP ${response.status}`);const server=await response.json(),browser=await new Promise(resolve=>requestAnimationFrame(()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g,_h;return resolve({domNodes:document.getElementsByTagName("*").length,audioElements:document.querySelectorAll("audio").length,jsHeapBytes:((_a2=performance.memory)==null?void 0:_a2.usedJSHeapSize)||null,imageCacheEntries:(_e2=(_d2=(_c2=(_b2=window.TTVC)==null?void 0:_b2.imageCache)==null?void 0:_c2.size)==null?void 0:_d2.call(_c2))!=null?_e2:null,rehearserAudioEntries:(_h=(_g=(_f2=window.rehState)==null?void 0:_f2.synthCache)==null?void 0:_g.size)!=null?_h:null,voiceRecords:Array.isArray(window._voices)?window._voices.length:null})}));diagnostics.replaceChildren(),[["Server RSS",`${server.server_rss_mb} MiB`],["Config data",formatBytes((_a=server.config_disk)==null?void 0:_a.used_bytes)],["Config free",formatBytes((_b=server.config_disk)==null?void 0:_b.free_bytes)],["Temp data",formatBytes((_c=server.temp_disk)==null?void 0:_c.used_bytes)],["Container memory",`${formatBytes((_d=server.container_memory)==null?void 0:_d.current_bytes)} / ${formatBytes((_e=server.container_memory)==null?void 0:_e.max_bytes)}`],["Artifact cache",formatBytes((_f=server.cache_storage)==null?void 0:_f.background_artifacts_bytes)],["Persisted jobs",server.background_jobs],["Image cache entries",browser.imageCacheEntries],["Rehearser audio entries",browser.rehearserAudioEntries],["Voice records",browser.voiceRecords],["DOM nodes",browser.domNodes],["Audio elements",browser.audioElements],["JS heap",formatBytes(browser.jsHeapBytes)]].forEach(([label,value])=>{const item=document.createElement("div");item.className="resource-diagnostic-item";const name=document.createElement("span");name.textContent=label;const valueNode=document.createElement("strong");valueNode.textContent=value==null?"\u2014":String(value),item.append(name,valueNode),diagnostics.appendChild(item)});const operations=Array.isArray(server.largest_operations)?server.largest_operations:[];if(operations.length){const heading=document.createElement("div");heading.className="resource-diagnostic-subhead",heading.textContent="Largest recent operations",diagnostics.appendChild(heading),operations.forEach(operation=>{const item=document.createElement("div");item.className="resource-diagnostic-item";const name=document.createElement("span");name.textContent=`${operation.kind} \xB7 ${operation.status}`;const valueNode=document.createElement("strong");valueNode.textContent=`${Number(operation.elapsed||0).toFixed(1)}s`,item.append(name,valueNode),diagnostics.appendChild(item)})}}catch(error){diagnostics.replaceChildren();const item=node("Diagnostics unavailable: "+error.message);item.className="perf-history-empty",diagnostics.appendChild(item)}finally{diagnosticsLoading=!1,diagnosticsRefresh&&(diagnosticsRefresh.disabled=!1)}}}refresh.addEventListener("click",load),diagnosticsRefresh==null||diagnosticsRefresh.addEventListener("click",loadDiagnostics),load(),loadDiagnostics(),setInterval(()=>{var _a;(_a=document.getElementById("s-performance"))!=null&&_a.classList.contains("is-active")&&(load(),loadDiagnostics())},1e4)}(),function(){"use strict";var root=window.TTVC=window.TTVC||{},entries=new Map,visible=new Set,observer="IntersectionObserver"in window?new IntersectionObserver(function(records){records.forEach(function(record){var img=record.target;if(record.isIntersecting){visible.add(img);var source=img.dataset.ttvcSrc;source&&!img.getAttribute("src")&&img.setAttribute("src",source),entries.delete(img),entries.set(img,Date.now())}else visible.delete(img)}),evict()},{rootMargin:"160px"}):null;function limit(){return typeof window.ttvcIsLowMemoryDevice=="function"&&window.ttvcIsLowMemoryDevice()?24:64}function evict(){for(entries.forEach(function(_,img2){img2.isConnected||entries.delete(img2)});entries.size>limit();){var candidate=null;for(var img of entries.keys())if(!visible.has(img)){candidate=img;break}if(!candidate)break;entries.delete(candidate),candidate.getAttribute("src")&&(candidate.dataset.ttvcSrc=candidate.getAttribute("src")),candidate.removeAttribute("src")}}function observeImages(container){container&&(container.querySelectorAll("img").forEach(function(img){!img.dataset.ttvcSrc&&img.getAttribute("src")&&(img.dataset.ttvcSrc=img.getAttribute("src")),observer&&!img.dataset.ttvcObserved&&(img.dataset.ttvcObserved="1",observer.observe(img)),entries.delete(img),entries.set(img,Date.now())}),evict())}root.imageCache={observe:observeImages,evict,size:function(){return entries.size}},window.ttvcObserveImages=observeImages}(),function(root){var _a,_b,_c;function cleanBaseUrl(url){return String(url||"").trim().replace(/\/+$/,"")}function getTtsBaseUrl(){var _a2;return cleanBaseUrl((_a2=$("s-tts-url"))==null?void 0:_a2.value)||"http://localhost:8020"}function getTtsV1Url(){const base=getTtsBaseUrl();return base.endsWith("/v1")?base:base+"/v1"}function getTtsStreamBaseUrl(){var _a2;return cleanBaseUrl(((_a2=$("s-tts-stream-url"))==null?void 0:_a2.value)||_appSettings.tts_stream_url)||"http://localhost:8023"}function getTtsStreamV1Url(){const base=getTtsStreamBaseUrl();return base.endsWith("/v1")?base:base+"/v1"}function getCreatorV1Url2(){const loc=window.location,protocol=loc.protocol||"http:",port=loc.port?":"+loc.port:"",host=loc.hostname==="0.0.0.0"?"localhost":loc.hostname;return`${protocol}//${host}${port}/v1`}function updateCreatorUrlHints2(){const warning=$("routing-url-warning"),badBindHost=window.location.hostname==="0.0.0.0";warning&&warning.classList.toggle("show",badBindHost)}function activeVoiceIds2(){return(_voices||[]).filter(v=>v.enabled!==!1).slice().sort((a,b)=>a.id.localeCompare(b.id)).map(v=>v.id)}function integrationVoiceExample(){return activeVoiceIds2()[0]||"EN_F_ExampleVoice"}function integrationVoiceList(){const ids=activeVoiceIds2();return ids.length?ids.join(", "):"EN_F_ExampleVoice, DE_M_ExampleVoice"}function virtualDesignVoiceIds2(){return Object.keys(loadDesignPresets?loadDesignPresets():{}).sort((a,b)=>a.localeCompare(b)).map(name=>"vd_"+(typeof _umlautSafe=="function"?_umlautSafe(name):name).replace(/[^A-Za-z0-9_.-]+/g,"_").replace(/^_+|_+$/g,""))}function renderIntegrationSnippets2(){if(!$("snippet-sillytavern"))return;const base=getTtsBaseUrl(),v1=getTtsV1Url(),streamV1=getTtsStreamV1Url(),proxyV1=getCreatorV1Url2(),proxyBase=proxyV1.replace(/\/v1$/,"");updateCreatorUrlHints2();const voice=integrationVoiceExample(),voices=integrationVoiceList(),vdVoices=virtualDesignVoiceIds2(),vdVoice=vdVoices[0]||"vd_EN_F_Warm_Narrator";$("integration-url-label").textContent="TTS backend: "+base,$("snippet-sillytavern").textContent=`Provider: OpenAI compatible TTS
API base URL: ${v1}
API key: dummy
Model: qwen3-tts
Voice: ${voice}
Custom voices: ${voices}
Streaming backend, if your SillyTavern TTS extension supports progressive playback:
API base URL: ${streamV1}
Endpoint: /audio/speech
Format: wav`,$("snippet-streaming-howto").textContent=`Direct streaming backend, no creator routing:
API base URL: ${streamV1}
Endpoint: /audio/speech
Model: tts-1
Voice: ${voice}
Response format: wav
Requirement: the app must start playback while the HTTP response is still arriving.
Current 8023 streaming service ignores per-request instruct/style text.
Streaming through TTS Voice Creator routing:
API base URL: ${proxyV1}
Voice: default or another incoming route voice
Routing tab: set Backend = Streaming for the matching rule
Response format: wav
Avoid before/after sounds for true streaming; route sounds and MP3 require buffering.
SillyTavern note:
Use OpenAI-compatible TTS if your extension supports progressive audio responses. If it waits for the whole file before playing, streaming works technically but will feel like buffered TTS.`,$("snippet-open-webui").textContent=`Admin settings -> Audio -> Text-to-Speech
Engine/provider: OpenAI compatible
API base URL: ${proxyV1}
API key: dummy
Model: tts-1
Voice: default
Routing tab example:
default + EN -> ${voice}
default + DE -> DE_M_YourGermanVoice`,$("snippet-home-assistant").textContent=`Home Assistant OpenAI TTS agent:
Base URL: ${proxyV1}
API key: dummy
Model: tts-1
Voice: default
Extra JSON payload: {"app":"Home Assistant"}
Audio format: mp3 or wav
REST command example using routing:
rest_command:
routed_tts:
url: "${proxyV1}/audio/speech"
method: POST
content_type: "application/json"
headers:
Authorization: "Bearer dummy"
payload: >
{"model":"tts-1","voice":"default","input":"{{ text }}","response_format":"mp3","app":"Home Assistant"}
Important: use this Creator proxy URL, not the direct Qwen3 backend URL ${v1}. Routing only runs through the Creator proxy.`,$("snippet-curl").textContent=`curl -s "${v1}/models"
curl -s "${v1}/audio/speech" \\
-H "Authorization: Bearer dummy" \\
-H "Content-Type: application/json" \\
-d '{"model":"qwen3-tts","voice":"${voice}","input":"Hello from Qwen3 TTS","response_format":"wav"}' \\
--output qwen3-tts-test.wav`,$("snippet-voice-design-proxy").textContent=`Virtual VoiceDesign mode - no WAV export needed
Use this app as the OpenAI-compatible TTS endpoint:
API base URL: ${proxyV1}
API key: dummy
Model: tts-1
Voice: ${vdVoice}
Available virtual voices:
${vdVoices.length?vdVoices.join(", "):"Save a Voice Design prompt preset first."}
curl -s "${proxyV1}/models"
curl -s "${proxyV1}/audio/speech" \\
-H "Authorization: Bearer dummy" \\
-H "Content-Type: application/json" \\
-d '{"model":"tts-1","voice":"${vdVoice}","input":"This line is generated through the VoiceDesign container.","response_format":"wav"}' \\
--output voicedesign-virtual.wav`,$("snippet-mcp-claude-cmd")&&($("snippet-mcp-claude-cmd").textContent=`claude mcp add voice-creator \\
--transport http \\
--url ${proxyBase}/mcp \\
--header "X-Voice-Creator-Client-Id: claude-code"`),$("snippet-mcp-claude-config")&&($("snippet-mcp-claude-config").textContent=`{
"mcpServers": {
"voice-creator": {
"url": "${proxyBase}/mcp",
"headers": { "X-Voice-Creator-Client-Id": "my-agent" }
}
}
}`),$("snippet-hotkey")&&($("snippet-hotkey").textContent=`pip install pynput sounddevice soundfile pyperclip requests
python hotkey_daemon.py --server ${proxyBase}`),$("snippet-speak")&&($("snippet-speak").textContent=`# Bind your client ID to a voice once (persisted in Settings)
curl -X PUT ${proxyBase}/speak/bindings/my-script \\
-H "Content-Type: application/json" \\
-d '{"voice":"${voice}"}'
# Generate speech \u2014 voice from binding, persona rewrite optional
curl -X POST ${proxyBase}/speak \\
-H "Content-Type: application/json" \\
-H "X-Voice-Creator-Client-Id: my-script" \\
-d '{"text":"Hello world","apply_persona":true}' \\
--output speech.wav
# Or specify voice directly
curl -X POST ${proxyBase}/speak \\
-H "Content-Type: application/json" \\
-d '{"text":"Hello world","voice":"${voice}"}' \\
--output speech.wav`)}document.querySelectorAll(".copy-snippet").forEach(btn=>btn.addEventListener("click",async()=>{const el=$(btn.dataset.snippet);el&&(await copyText(el.textContent),toast("Snippet copied","success"))})),(_a=$("show-api-btn"))==null||_a.addEventListener("click",()=>{window.open("/docs","_blank","noopener")}),(_b=$("integration-refresh-btn"))==null||_b.addEventListener("click",()=>{renderIntegrationSnippets2(),toast("Integration examples refreshed","success")}),(_c=$("copy-active-voices-btn-integrations"))==null||_c.addEventListener("click",async()=>{const active=activeVoiceIds2();if(!active.length){toast("No active voices to copy","error");return}await copyText(active.join(", ")),toast("Copied "+active.length+" active voices","success"),status("Copied active voices to clipboard")}),Object.assign(root,{cleanBaseUrl,getTtsBaseUrl,getTtsV1Url,getTtsStreamBaseUrl,getTtsStreamV1Url,getCreatorV1Url:getCreatorV1Url2,updateCreatorUrlHints:updateCreatorUrlHints2,activeVoiceIds:activeVoiceIds2,integrationVoiceExample,integrationVoiceList,virtualDesignVoiceIds:virtualDesignVoiceIds2,renderIntegrationSnippets:renderIntegrationSnippets2}),root.TTVC=root.TTVC||{},root.TTVC.integrations=root.TTVC.integrations||{},Object.assign(root.TTVC.integrations,{endpoints:{tts:getTtsV1Url,stream:getTtsStreamV1Url,creator:getCreatorV1Url2},voices:{active:activeVoiceIds2,list:integrationVoiceList},render:renderIntegrationSnippets2})}(window),function(root){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p;let _ttsRoutes=[];const ROUTE_LANGS=[["*","Any"],["AUTO","Auto"],...typeof LANG_CODE_OPTIONS!="undefined"&&Array.isArray(LANG_CODE_OPTIONS)?LANG_CODE_OPTIONS:[["EN","English"],["DE","German"],["FR","French"],["ES","Spanish"],["IT","Italian"],["PT","Portuguese"],["NL","Dutch"],["PL","Polish"]]];function _routingSetStatus(text){["routing-status","routing-status-head"].forEach(id=>{const el=document.getElementById(id);el&&(el.textContent=text)})}function routeSelectOptions(value){return ROUTE_LANGS.map(([code,label])=>`<option value="${code}" ${code===value?"selected":""}>${escHtml(label)}</option>`).join("")}function enhanceRouteLangSelects(){typeof enhanceLangSelect=="function"&&document.querySelectorAll("#routing-list .route-lang").forEach(enhanceLangSelect)}const ROUTE_BACKENDS=[["voice_clone","Voice Clone"],["streaming","Streaming"],["voice_design","Voice Design"],["nvidia_magpie","NVIDIA Magpie"],["nvidia_zeroshot","NVIDIA Zeroshot"],["nvidia_flow","NVIDIA Flow"]];let _routeSounds=[];function routeSoundOptions(value=""){const current=String(value||""),listed=new Set(_routeSounds.map(s=>String(s.path||""))),options=['<option value="">Browse sounds</option>'];return current&&!listed.has(current)&&options.push(`<option value="${escHtml(current)}" selected>${escHtml(current)}</option>`),_routeSounds.forEach(sound=>{const path=String(sound.path||"");if(!path)return;const duration=sound.duration!=null?` \xB7 ${Number(sound.duration).toFixed(1)}s`:"";options.push(`<option value="${escHtml(path)}" ${path===current?"selected":""}>${escHtml(path+duration)}</option>`)}),options.join("")}async function loadRouteSounds(){try{const r=await fetch("/api/route-sounds");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_routeSounds=Array.isArray(d.sounds)?d.sounds:[]}catch{_routeSounds=[]}}let _routeSoundPickerTarget=null,_routeSoundPlayingButton=null;function routeSoundUrl(path){return"/api/route-sounds/file/"+String(path||"").split("/").map(encodeURIComponent).join("/")}function setRouteSoundField(row,target,path){const field=row==null?void 0:row.querySelector(target==="before"?".route-before-sound":".route-after-sound");field&&(field.value=path||"",readRoutingForm())}function renderRouteSoundBrowser(){var _a2;const list=$("routing-sound-list");if(!list)return;const query=String(((_a2=$("routing-sound-search"))==null?void 0:_a2.value)||"").trim().toLowerCase(),sounds=query?_routeSounds.filter(sound=>String(sound.path||"").toLowerCase().includes(query)||String(sound.name||"").toLowerCase().includes(query)):_routeSounds;if(!_routeSounds.length){list.innerHTML='<div class="routing-log-empty">No sounds found yet. Upload one in a route row; only the selected file is imported.</div>';return}if(!sounds.length){list.innerHTML='<div class="routing-log-empty">No sounds match this search.</div>';return}list.innerHTML=sounds.map(sound=>{const path=String(sound.path||""),size=sound.size!=null?Math.max(1,Math.round(Number(sound.size)/1024))+" KB":"",duration=sound.duration!=null?Number(sound.duration).toFixed(1)+"s":size,type=sound.type?String(sound.type).toUpperCase():"",targetLabel=(_routeSoundPickerTarget==null?void 0:_routeSoundPickerTarget.target)==="after"?"Use after":"Use before";return`
<div class="routing-sound-item" data-path="${escHtml(path)}">
<button class="btn-secondary sound-play" type="button" title="Preview sound">\u25B6</button>
<div class="sound-path" title="${escHtml(path)}">${escHtml(path)}</div>
<div class="sound-meta">${escHtml(duration||"-")} ${type?"\xB7 "+escHtml(type):""}</div>
<button class="btn-secondary sound-use-current" type="button">${escHtml(targetLabel)}</button>
</div>`}).join("")}async function openRouteSoundBrowser(row,target){_routeSoundPickerTarget={row,target},await loadRouteSounds(),renderRouteSoundBrowser();const panel=$("routing-sound-browser");panel&&(panel.hidden=!1,panel.scrollIntoView({block:"nearest",behavior:"smooth"}));const label=target==="before"?"before sound":"after sound",note=$("routing-sound-browser-note");note&&(note.textContent=`Preview uploaded route sounds, then choose one for this ${label}. Upload imports only the selected file; this list also shows sounds already present in the sounds folders. ${_routeSounds.length} sounds available.`)}function closeRouteSoundBrowser(){const panel=$("routing-sound-browser");panel&&(panel.hidden=!0);const audio=$("routing-sound-preview");audio&&(audio.pause(),audio.hidden=!0,audio.removeAttribute("src")),_routeSoundPlayingButton&&(_routeSoundPlayingButton.textContent="\u25B6"),_routeSoundPlayingButton=null,_routeSoundPickerTarget=null}function playRouteSound(path,btn){const audio=$("routing-sound-preview");!audio||!path||(_routeSoundPlayingButton&&_routeSoundPlayingButton!==btn&&(_routeSoundPlayingButton.textContent="\u25B6"),_routeSoundPlayingButton=btn,btn.textContent="\u275A\u275A",audio.hidden=!1,audio.playbackRate=1,audio.src=routeSoundUrl(path),audio.onended=()=>{btn.textContent="\u25B6"},audio.onpause=()=>{_routeSoundPlayingButton===btn&&(btn.textContent="\u25B6")},audio.onplay=()=>{btn.textContent="\u275A\u275A"},audio.play().catch(e=>{btn.textContent="\u25B6",toast("Sound preview failed: "+e.message,"error")}))}const ROUTE_SPEED_PREVIEW_TEXT={EN:"This is a quick playback speed test.",DE:"Das ist ein kurzer Test der Wiedergabegeschwindigkeit.",FR:"Ceci est un test rapide de la vitesse de lecture.",ES:"Esta es una prueba r\xE1pida de la velocidad de reproducci\xF3n.",IT:"Questo \xE8 un rapido test della velocit\xE0 di riproduzione.",PT:"Este \xE9 um teste r\xE1pido da velocidade de reprodu\xE7\xE3o.",NL:"Dit is een snelle test van de afspeelsnelheid.",PL:"To jest kr\xF3tki test szybko\u015Bci odtwarzania."};async function previewRouteSpeed(row,btn){var _a2,_b2,_c2,_d2,_e2;const outputVoice=(_a2=row.querySelector(".route-output"))==null?void 0:_a2.value.trim();if(!outputVoice){toast("Set an output voice first","error");return}const backend=((_b2=row.querySelector(".route-backend"))==null?void 0:_b2.value)||"voice_clone",speed=Math.max(.5,Math.min(2,parseFloat((_c2=row.querySelector(".route-speed"))==null?void 0:_c2.value)||1)),lang=(((_d2=row.querySelector(".route-lang"))==null?void 0:_d2.value)||"EN").toUpperCase(),text=ROUTE_SPEED_PREVIEW_TEXT[lang]||ROUTE_SPEED_PREVIEW_TEXT.EN,audio=$("routing-sound-preview");if(!audio)return;_routeSoundPlayingButton&&_routeSoundPlayingButton!==btn&&(_routeSoundPlayingButton.textContent="\u25B6"),_routeSpeedPreviewButton&&_routeSpeedPreviewButton!==btn&&((_e2=_routeSpeedPreviewButton.querySelector(".mdi"))==null||_e2.classList.replace("mdi-pause","mdi-play")),_routeSpeedPreviewButton=btn;const icon=btn.querySelector(".mdi");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(outputVoice,text,"wav","",backend);audio.hidden=!1,audio.src=URL.createObjectURL(blob),audio.playbackRate=speed,icon==null||icon.classList.replace("mdi-play","mdi-pause"),audio.onended=()=>icon==null?void 0:icon.classList.replace("mdi-pause","mdi-play"),audio.onpause=()=>{_routeSpeedPreviewButton===btn&&(icon==null||icon.classList.replace("mdi-pause","mdi-play"))},await audio.play()}catch(e){icon==null||icon.classList.replace("mdi-pause","mdi-play"),toast("Preview failed: "+e.message,"error")}finally{btn.disabled=!1}}let _routeSpeedPreviewButton=null;function useRouteSound(path,target){const selected=_routeSoundPickerTarget||{},row=selected.row||document.querySelector(".routing-row"),useTarget=target||selected.target||"before";setRouteSoundField(row,useTarget,path),toast(`${useTarget==="before"?"Before":"After"} sound selected`,"success")}function routeBackendOptions(value){const current=value||"voice_clone";return ROUTE_BACKENDS.map(([code,label])=>`<option value="${code}" ${code===current?"selected":""}>${escHtml(label)}</option>`).join("")}function refreshRoutingVoiceOptions(){const dl=$("routing-voice-options");if(dl){const ids=[...activeVoiceIds(),...virtualDesignVoiceIds()];dl.innerHTML=[...new Set(ids)].map(id=>`<option value="${escHtml(id)}"></option>`).join("")}const soundsDl=$("routing-sound-options");soundsDl&&(soundsDl.innerHTML=_routeSounds.map(sound=>`<option value="${escHtml(sound.path||"")}"></option>`).join(""))}function newRoute(app="Open WebUI",inputVoice="default",language="*",outputVoice=""){return{id:"route_"+Date.now().toString(36)+"_"+Math.random().toString(36).slice(2,6),enabled:!0,app,input_voice:inputVoice,language,backend:"voice_clone",output_voice:outputVoice,before_sound:"",after_sound:""}}function renderRoutingList(){if($("routing-list")){if(refreshRoutingVoiceOptions(),$("routing-proxy-url").textContent=getCreatorV1Url(),updateCreatorUrlHints(),_routingSetStatus(_ttsRoutes.length?`${_ttsRoutes.length} route${_ttsRoutes.length===1?"":"s"}`:"No routes yet."),!_ttsRoutes.length){$("routing-list").innerHTML='<div class="note" style="padding:10px">No routing rules yet. Add a route or add the Open WebUI default examples.</div>';return}$("routing-list").innerHTML=_ttsRoutes.map((r,i)=>`
<div class="routing-grid routing-row" data-index="${i}">
<label class="toggle" title="Enable route">
<input type="checkbox" class="route-enabled" aria-label="Enable route" ${r.enabled!==!1?"checked":""}>
<span class="t-slider"></span>
</label>
<input class="route-app" aria-label="App / client" value="${escHtml(r.app||"Open WebUI")}" placeholder="Open WebUI, SillyTavern, Home Assistant, or *" title="Matches app/client JSON, X-TTS-App header, or detected client name. Use * for any app.">
<input class="route-input" aria-label="Input voice" value="${escHtml(r.input_voice||"default")}" placeholder="default">
<select class="route-lang" aria-label="Route language">${routeSelectOptions(String(r.language||"*").toUpperCase())}</select>
<select class="route-backend" aria-label="Route backend" title="Voice Clone uses the normal TTS URL, Streaming uses the streaming URL, Voice Design uses vd_ presets, NVIDIA Magpie uses fixed NVIDIA voices, NVIDIA Zeroshot/Flow use saved library WAVs as audio prompts.">${routeBackendOptions(r.backend||"voice_clone")}</select>
<span class="route-output-wrap"><input class="route-output" value="${escHtml(r.output_voice||"")}" list="routing-voice-options" placeholder="EN_F_VoiceName or vd_Preset"></span>
<div class="route-speed-cell">
<input class="route-speed" type="number" min="0.5" max="2" step="0.05" value="${Number(r.speed)>0?r.speed:1}" title="Playback speed multiplier (0.5x-2x). 1 = normal speed.">
<button type="button" class="btn-secondary route-speed-preview" title="Preview how this voice sounds at this speed"><span class="mdi mdi-play"></span></button>
</div>
<div class="route-sound-cell">
<input class="route-before-sound" value="${escHtml(r.before_sound||"")}" placeholder="sounds/start.wav" list="routing-sound-options">
<button class="btn-secondary route-sound-pick" data-target="before" title="Browse and preview uploaded sounds">Pick</button>
<button class="btn-secondary route-sound-upload" data-target="before" title="Upload one before sound">Upload</button>
</div>
<div class="route-sound-cell">
<input class="route-after-sound" value="${escHtml(r.after_sound||"")}" placeholder="sounds/end.wav" list="routing-sound-options">
<button class="btn-secondary route-sound-pick" data-target="after" title="Browse and preview uploaded sounds">Pick</button>
<button class="btn-secondary route-sound-upload" data-target="after" title="Upload one after sound">Upload</button>
</div>
<button class="btn-secondary routing-delete" title="Delete route">\xD7</button>
</div>
`).join(""),enhanceRouteLangSelects(),typeof VoicePicker!="undefined"&&document.querySelectorAll(".route-output").forEach(inp=>VoicePicker.attachTextPicker(inp))}}function readRoutingForm(){_ttsRoutes=[...document.querySelectorAll(".routing-row")].map((row,i)=>({id:(_ttsRoutes[Number(row.dataset.index)]||{}).id||`route_${i+1}`,enabled:row.querySelector(".route-enabled").checked,app:row.querySelector(".route-app").value.trim()||"*",input_voice:row.querySelector(".route-input").value.trim()||"default",language:row.querySelector(".route-lang").value||"*",backend:row.querySelector(".route-backend").value||"voice_clone",output_voice:row.querySelector(".route-output").value.trim(),speed:Math.max(.5,Math.min(2,parseFloat(row.querySelector(".route-speed").value)||1)),before_sound:row.querySelector(".route-before-sound").value.trim(),after_sound:row.querySelector(".route-after-sound").value.trim()}))}async function loadRoutingTab(){if($("routing-list")){$("routing-proxy-url").textContent=getCreatorV1Url(),updateCreatorUrlHints(),_routingSetStatus("Loading routing\u2026"),$("routing-list").innerHTML=loadingMarkup("Loading routing","Loading active voices and routing rules for the proxy.",5),setBusyButton("routing-refresh-btn",!0),await Promise.allSettled([_voices.length?Promise.resolve():loadVoiceLibrary().catch(()=>{}),loadRouteSounds()]);try{const r=await fetch("/api/tts-routes");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_ttsRoutes=Array.isArray(d.routes)?d.routes:[],renderRoutingList(),status("Routing loaded"),loadRoutingLog()}catch(e){_routingSetStatus("Load failed"),$("routing-list").innerHTML=`<div style="color:var(--red);padding:10px">Failed to load routes: ${escHtml(e.message)}</div>`,status("Routing load failed")}finally{setBusyButton("routing-refresh-btn",!1)}}}async function saveRoutingTab(){readRoutingForm(),["routing-save-btn","routing-save-btn-head"].forEach(id=>{const b=$(id);b&&(b.disabled=!0)});try{const r=await fetch("/api/tts-routes",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({routes:_ttsRoutes})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}_ttsRoutes=(await r.json()).routes||_ttsRoutes,renderRoutingList(),toast("Routing saved","success"),status("TTS routing saved")}catch(e){toast("Save routes failed: "+e.message,"error")}finally{["routing-save-btn","routing-save-btn-head"].forEach(id=>{const b=$(id);b&&(b.disabled=!1)})}}function renderRouteTestResult(d){const el=$("routing-test-result");if(!el)return;const health=d.voice_health||{},warnings=Array.isArray(health.warnings)?health.warnings:[];el.className="routing-test-result "+(warnings.length?"warn":"ok");const parts=[`${escHtml(d.requested_voice||"")} \u2192 ${escHtml(d.routed_voice||"")}`,`app ${escHtml(d.app||"-")}`,`backend ${escHtml(d.backend||"voice_clone")}`,`language ${escHtml(d.detected_language||"-")}`,d.matched?"matched route":"no route matched"];health.duration&&parts.push(`reference ${health.duration}s`),health.word_count!=null&&parts.push(`${health.word_count} words`),health.words_per_sec&&parts.push(`${health.words_per_sec} words/s`),warnings.length&&parts.push("Warning: "+warnings.map(escHtml).join("; "));const sounds=d.sounds||{};for(const[key,sound]of Object.entries(sounds)){const label=key==="before_sound"?"before sound":"after sound";parts.push(sound.ok?`${label} OK`:`${label}: ${escHtml(sound.error||"not found")}`),sound.ok||(el.className="routing-test-result warn")}el.innerHTML=parts.join(" \xB7 ")}function routingLogTime(ts){if(!ts)return"--:--:--";const d=new Date(ts);return Number.isNaN(d.getTime())?String(ts).slice(11,19)||"--:--:--":d.toLocaleTimeString([],{hour:"2-digit",minute:"2-digit",second:"2-digit"})}function routingLogBadge(item){const status2=String(item.status||item.kind||"log");return item.kind==="test"&&status2==="matched"?"test ok":item.kind==="test"&&status2==="no_match"?"test miss":status2.replace(/_/g," ")}function routingLogMeta(item){const parts=[];return item.backend&&parts.push(item.backend),item.language&&parts.push("lang "+item.language),item.response_format&&parts.push(item.response_format),item.duration!=null&&parts.push(Number(item.duration).toFixed(2)+"s"),item.bytes!=null&&parts.push(Math.round(Number(item.bytes)/1024)+" KB"),item.sounds&&Array.isArray(item.sounds)&&item.sounds.length&&parts.push("sounds "+item.sounds.join(",")),item.route_id&&parts.push("route "+item.route_id),item.client&&parts.push(item.client),parts.join(" \xB7 ")}let _routingLogItems=[],_routingLogFilter="all";function routingLogPassesFilter(item){const status2=String((item==null?void 0:item.status)||"").toLowerCase(),isError=status2==="error"||!!(item!=null&&item.error),isNoMatch=status2==="no_match"||(item==null?void 0:item.matched)===!1;return _routingLogFilter==="error"?isError:_routingLogFilter==="no_match"?isNoMatch:_routingLogFilter==="attention"?isError||isNoMatch:!0}function renderCurrentRoutingLog(){renderRoutingLog(_routingLogItems.filter(routingLogPassesFilter))}function renderRoutingLog(items=[]){const el=$("routing-log-list");if(el){if(!items.length){const filtered=_routingLogItems.length&&_routingLogFilter!=="all";el.innerHTML=`<div class="routing-log-empty">${filtered?"No routing log entries match this filter.":"No routing log entries yet. Test a route or send a TTS request through the Creator proxy."}</div>`;return}el.innerHTML=items.map(item=>{const status2=String(item.status||"log").replace(/[^a-z0-9_-]/gi,"_"),requested=item.requested_voice||"-",routed=item.routed_voice||"-",voice=requested===routed?requested:`${requested} \u2192 ${routed}`,text=item.error?`Error: ${item.error}`:item.text_preview||"";return`
<div class="routing-log-entry status-${escHtml(status2)}">
<div class="log-time">${escHtml(routingLogTime(item.ts))}</div>
<div class="routing-log-badge">${escHtml(routingLogBadge(item))}</div>
<div class="log-app" title="${escHtml(item.app||"-")}">${escHtml(item.app||"-")}</div>
<div class="log-voice" title="${escHtml(voice)}">${escHtml(voice)}</div>
<div class="log-meta" title="${escHtml(routingLogMeta(item))}">${escHtml(routingLogMeta(item)||"-")}</div>
<div class="log-text" title="${escHtml(text)}">${escHtml(text||"-")}</div>
</div>`}).join("")}}async function loadRoutingLog(){const el=$("routing-log-list");if(el)try{const r=await fetch("/api/tts-routing-log?limit=80");if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_routingLogItems=Array.isArray(d.items)?d.items:[],renderCurrentRoutingLog()}catch(e){el.innerHTML=`<div class="routing-log-empty" style="color:var(--red)">Routing log unavailable: ${escHtml(e.message)}</div>`}}async function clearRoutingLog(){const btn=$("routing-log-clear-btn");btn&&(btn.disabled=!0);try{const r=await fetch("/api/tts-routing-log",{method:"DELETE"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}_routingLogItems=[],renderRoutingLog([]),toast("Routing log cleared","success")}catch(e){toast("Clear log failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}async function testRouting(){readRoutingForm();const btn=$("routing-test-btn"),el=$("routing-test-result");btn.disabled=!0,el.className="routing-test-result",el.textContent="Testing route...";try{const r=await fetch("/api/tts-route-test",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({app:$("routing-test-app").value.trim()||"Open WebUI",voice:$("routing-test-voice").value.trim()||"default",input:$("routing-test-text").value.trim()})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}renderRouteTestResult(await r.json()),loadRoutingLog()}catch(e){el.className="routing-test-result warn",el.textContent="Route test failed: "+e.message}finally{btn.disabled=!1}}async function uploadRouteSoundForRow(row,target){const input=document.createElement("input");input.type="file",input.accept="audio/*",input.multiple=!1,input.onchange=async()=>{if(!input.files||!input.files.length)return;const btn=row.querySelector(`.route-sound-upload[data-target="${target}"]`),field=row.querySelector(target==="before"?".route-before-sound":".route-after-sound");btn&&(btn.disabled=!0);try{const fd=new FormData;fd.append("file",input.files[0]);const r=await fetch("/api/route-sounds/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();field.value=d.path||"",await loadRouteSounds(),readRoutingForm(),renderRoutingList(),toast(`${target==="before"?"Before":"After"} sound uploaded`,"success"),status(`Uploaded route sound: ${d.path}`)}catch(e){toast("Sound upload failed: "+e.message,"error"),status("Sound upload failed")}finally{btn&&(btn.disabled=!1)}},input.click()}(_a=$("routing-refresh-btn"))==null||_a.addEventListener("click",loadRoutingTab),(_b=$("routing-add-btn"))==null||_b.addEventListener("click",()=>{readRoutingForm(),_ttsRoutes.push(newRoute()),renderRoutingList()}),(_c=$("routing-add-openwebui-btn"))==null||_c.addEventListener("click",()=>{readRoutingForm();const voices=activeVoiceIds(),firstByLang=lang=>voices.find(v=>v.toUpperCase().startsWith(lang+"_"))||"";_ttsRoutes.push(newRoute("Open WebUI","default","EN",firstByLang("EN"))),_ttsRoutes.push(newRoute("Open WebUI","default","DE",firstByLang("DE"))),renderRoutingList()}),(_d=$("routing-save-btn"))==null||_d.addEventListener("click",saveRoutingTab),(_e=$("routing-save-btn-head"))==null||_e.addEventListener("click",saveRoutingTab),(_f=$("routing-add-btn-head"))==null||_f.addEventListener("click",()=>{var _a2;return(_a2=$("routing-add-btn"))==null?void 0:_a2.click()}),(_g=$("routing-test-btn"))==null||_g.addEventListener("click",testRouting),(_h=$("routing-log-refresh-btn"))==null||_h.addEventListener("click",loadRoutingLog),(_i=$("routing-log-clear-btn"))==null||_i.addEventListener("click",clearRoutingLog),(_j=$("routing-log-filter"))==null||_j.addEventListener("change",e=>{_routingLogFilter=e.target.value||"all",renderCurrentRoutingLog()}),(_k=$("routing-list"))==null||_k.addEventListener("change",e=>{const picker=e.target.closest(".route-sound-picker");if(!picker)return;const field=picker.closest(".routing-row").querySelector(picker.dataset.target==="before"?".route-before-sound":".route-after-sound");field&&(field.value=picker.value||""),readRoutingForm()}),(_l=$("routing-list"))==null||_l.addEventListener("click",e=>{const speedPreviewBtn=e.target.closest(".route-speed-preview");if(speedPreviewBtn){previewRouteSpeed(speedPreviewBtn.closest(".routing-row"),speedPreviewBtn);return}const pickBtn=e.target.closest(".route-sound-pick");if(pickBtn){const row2=pickBtn.closest(".routing-row");openRouteSoundBrowser(row2,pickBtn.dataset.target);return}const uploadBtn=e.target.closest(".route-sound-upload");if(uploadBtn){const row2=uploadBtn.closest(".routing-row");uploadRouteSoundForRow(row2,uploadBtn.dataset.target);return}const btn=e.target.closest(".routing-delete");if(!btn)return;readRoutingForm();const row=btn.closest(".routing-row");_ttsRoutes.splice(Number(row.dataset.index),1),renderRoutingList()}),(_m=$("routing-sound-search"))==null||_m.addEventListener("input",debounce(renderRouteSoundBrowser,120)),(_n=$("routing-sound-refresh-btn"))==null||_n.addEventListener("click",async()=>{await loadRouteSounds(),renderRouteSoundBrowser()}),(_o=$("routing-sound-close-btn"))==null||_o.addEventListener("click",closeRouteSoundBrowser),(_p=$("routing-sound-list"))==null||_p.addEventListener("click",e=>{const item=e.target.closest(".routing-sound-item");if(!item)return;const path=item.dataset.path||"",playBtn=e.target.closest(".sound-play");if(playBtn){playRouteSound(path,playBtn);return}e.target.closest(".sound-use-current")&&useRouteSound(path,(_routeSoundPickerTarget==null?void 0:_routeSoundPickerTarget.target)||"before")}),Object.assign(root,{ROUTE_LANGS,_routingSetStatus,routeSelectOptions,enhanceRouteLangSelects,ROUTE_BACKENDS,_routeSounds,routeSoundOptions,loadRouteSounds,_routeSoundPickerTarget,_routeSoundPlayingButton,routeSoundUrl,setRouteSoundField,renderRouteSoundBrowser,openRouteSoundBrowser,closeRouteSoundBrowser,playRouteSound,ROUTE_SPEED_PREVIEW_TEXT,previewRouteSpeed,_routeSpeedPreviewButton,useRouteSound,routeBackendOptions,refreshRoutingVoiceOptions,newRoute,renderRoutingList,readRoutingForm,loadRoutingTab,saveRoutingTab,renderRouteTestResult,routingLogTime,routingLogBadge,routingLogMeta,_routingLogItems,_routingLogFilter,routingLogPassesFilter,renderCurrentRoutingLog,renderRoutingLog,loadRoutingLog,clearRoutingLog,testRouting,uploadRouteSoundForRow}),Object.defineProperty(root,"_ttsRoutes",{configurable:!0,get:()=>_ttsRoutes,set:v=>{_ttsRoutes=v}}),root.TTVC=root.TTVC||{},root.TTVC.routing=root.TTVC.routing||{},Object.assign(root.TTVC.routing,{get routes(){return _ttsRoutes},get sounds(){return _routeSounds},load:loadRoutingTab,save:saveRoutingTab,test:testRouting,render:renderRoutingList})}(window),function(root){var _a,_b,_c,_d;const CLONE_SAMPLE_TEXTS={EN:"Hello! My name is Sam, and this is my voice. I can speak softly or with great strength. The crisp winter air, warm firelight, and the gentle sound of rain \u2014 these are the things I love. Can you hear how clearly I speak?",DE:"Hallo! Ich hei\xDFe Alex und das ist meine Stimme. Ich kann leise fl\xFCstern oder mit voller Kraft sprechen. Klare Winterluft, warmes Kerzenlicht und der Klang des Regens am Fenster \u2014 das liebe ich. H\xF6rst du, wie deutlich ich spreche?",IT:"Ciao! Mi chiamo Marco e questa \xE8 la mia voce. Posso parlare dolcemente o con grande forza. L'aria fresca d'inverno, la luce calda del fuoco e il suono della pioggia \u2014 queste sono le cose che amo. Senti come parlo chiaramente?",ES:"\xA1Hola! Me llamo Carlos y esta es mi voz. Puedo hablar suavemente o con gran fuerza. El aire fr\xEDo del invierno, la c\xE1lida luz del fuego y el suave sonido de la lluvia \u2014 estas son las cosas que amo. \xBFPuedes o\xEDr lo claramente que hablo?",FR:"Bonjour ! Je m'appelle Sophie et voici ma voix. Je peux parler doucement ou avec grande force. L'air vif de l'hiver, la douce lumi\xE8re du feu et le son de la pluie \u2014 voil\xE0 ce que j'aime. Entends-tu comme je parle clairement ?",PT:"Ol\xE1! Meu nome \xE9 Ana e esta \xE9 a minha voz. Posso falar suavemente ou com grande for\xE7a. O ar fresco do inverno, a luz quente do fogo e o som da chuva \u2014 estas s\xE3o as coisas que amo. Consegues ouvir como falo claramente?",NL:"Hoi! Mijn naam is Laura en dit is mijn stem. Ik kan zacht fluisteren of met volle kracht spreken. De frisse winterlucht, het warme kaarslicht en het geluid van de regen \u2014 dat zijn de dingen die ik liefheb. Hoor je hoe helder ik spreek?",PL:"Cze\u015B\u0107! Mam na imi\u0119 Anna i to jest m\xF3j g\u0142os. Mog\u0119 m\xF3wi\u0107 cicho lub z ca\u0142\u0105 moc\u0105. Mro\u017Ane zimowe powietrze, ciep\u0142e \u015Bwiat\u0142o ognia i d\u017Awi\u0119k deszczu za oknem \u2014 to s\u0105 rzeczy, kt\xF3re kocham. Czy s\u0142yszysz, jak wyra\u017Anie m\xF3wi\u0119?"},CLONE_SAMPLE_NAMES={EN:"Sam",DE:"Alex",IT:"Marco",ES:"Carlos",FR:"Sophie",PT:"Ana",NL:"Laura",PL:"Anna"};function cloneSampleForLang(lang){var _a2;const txt=$("clone-sample-text"),def=CLONE_SAMPLE_NAMES[lang]||"";let t=CLONE_SAMPLE_TEXTS[lang]||CLONE_SAMPLE_TEXTS.EN;const name=(((_a2=$("clone-your-name"))==null?void 0:_a2.value)||"").trim();name&&def&&(t=t.replace(new RegExp("\\b"+def.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b"),name)),txt&&(txt.value=t,txt.dataset.sampleName=name||def)}window.initCloneSampleText=function(){const sel=$("clone-sample-lang"),txt=$("clone-sample-text");!sel||!txt||(cloneSampleForLang(sel.value),sel._cloneSampleBound||(sel.addEventListener("change",()=>cloneSampleForLang(sel.value)),sel._cloneSampleBound=!0))},initCloneSampleText();let ws=null,wsRegions=null,currentFileId2=null,trimmedFileId2=null,designedFileId2=null,editingVoiceId2=null,editingVoicePath2=null;function initWaveSurfer(){ws&&(ws.destroy(),ws=null,wsRegions=null),wsRegions=WaveSurfer.Regions.create(),ws=WaveSurfer.create({container:"#waveform",waveColor:"#45475a",progressColor:"#89b4fa",cursorColor:"#cba6f7",height:90,normalize:!0,plugins:[wsRegions]}),ws.on("ready",()=>{const dur=ws.getDuration();$("trim-end").value=dur.toFixed(2),$("trim-end").max=dur.toFixed(2),$("trim-start").max=dur.toFixed(2),updateRegion()}),wsRegions.on("region-updated",r=>{$("trim-start").value=r.start.toFixed(2),$("trim-end").value=r.end.toFixed(2),updateDurationLabel()})}function updateRegion(){wsRegions.clearRegions();const s=parseFloat($("trim-start").value)||0,e=parseFloat($("trim-end").value)||(ws?ws.getDuration():0);wsRegions.addRegion({start:s,end:e,color:"rgba(137,180,250,0.25)",drag:!0,resize:!0}),updateDurationLabel()}function updateDurationLabel(){const d=Math.max(0,(parseFloat($("trim-end").value)||0)-(parseFloat($("trim-start").value)||0)),el=$("trim-duration");el.textContent=d.toFixed(1)+" s",el.className=d>=5&&d<=20?"dur-ok":d>20?"dur-warn":"dur-bad"}["trim-start","trim-end"].forEach(id=>$(id).addEventListener("input",()=>{ws&&updateRegion()})),$("play-btn").addEventListener("click",()=>{ws&&ws.playPause()}),$("play-selection-btn").addEventListener("click",()=>{ws&&ws.play(parseFloat($("trim-start").value)||0,parseFloat($("trim-end").value)||ws.getDuration())}),$("auto-trim-btn").addEventListener("click",async()=>{if(!currentFileId2){toast("No audio loaded","error");return}$("auto-trim-btn").disabled=!0,status("Finding best TTS reference segment\u2026");try{const r=await fetch("/api/auto-trim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:currentFileId2})});let d;if(r.ok)d=await r.json();else if(r.status===404||r.status===405)status("Backend auto trim unavailable; analysing audio in browser\u2026"),d=await clientAutoTrimBounds(currentFileId2);else{const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText||"Auto trim failed")}$("trim-start").value=Number(d.start).toFixed(2),$("trim-end").value=Number(d.end).toFixed(2),ws&&updateRegion(),toast("Auto trim set: "+Number(d.duration).toFixed(1)+" s","success"),status(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error"),status("Auto trim failed")}finally{$("auto-trim-btn").disabled=!1}});function loadAudioId(id,dur,opts={}){currentFileId2=id,trimmedFileId2=null,designedFileId2=null,editingVoiceId2=opts.editingVoiceId||null,editingVoicePath2=opts.editingVoicePath||null,$("trim-start").value="0",$("trim-end").value=dur.toFixed(2),$("waveform-card").style.display="",initWaveSurfer(),ws.load("/api/audio/"+id),$("save-result").style.display="none",$("trim-audio").style.display="none",$("no-audio-hint").style.display="",editingVoiceId2&&($("voice-id-input").value=editingVoiceId2,$("voice-id-input").dispatchEvent(new Event("input")),$("transcript-area").value=opts.transcript||"",status("Editing existing voice: "+editingVoiceId2))}const dropZone=$("drop-zone"),fileInput=$("file-input");dropZone.addEventListener("click",()=>fileInput.click()),dropZone.addEventListener("dragover",e=>{e.preventDefault(),dropZone.classList.add("drag-over")}),dropZone.addEventListener("dragleave",()=>dropZone.classList.remove("drag-over")),dropZone.addEventListener("drop",e=>{e.preventDefault(),dropZone.classList.remove("drag-over"),e.dataTransfer.files.length&&uploadFile(e.dataTransfer.files[0])}),fileInput.addEventListener("change",()=>{fileInput.files.length&&uploadFile(fileInput.files[0])});async function uploadFile(file){status("Uploading "+file.name+"\u2026");const fd=new FormData;fd.append("file",file);try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();loadAudioId(d.id,d.duration),status("Loaded: "+file.name+" ("+d.duration.toFixed(1)+" s)"),toast("File loaded","success")}catch(e){toast("Upload failed: "+e.message,"error"),status("Upload failed")}}async function loadLibraryVoiceAudio2(v){const audioResp=await fetch(voiceFileUrl(v),{cache:"no-store"});if(!audioResp.ok){const e=await audioResp.json().catch(()=>({}));throw new Error(e.detail||audioResp.statusText)}const blob=await audioResp.blob(),ext=(v.file_type||"wav").toLowerCase(),fd=new FormData;fd.append("file",new File([blob],`${v.id}.${ext}`,{type:blob.type||"audio/wav"}));const upload=await fetch("/api/upload",{method:"POST",body:fd});if(!upload.ok){const e=await upload.json().catch(()=>({}));throw new Error(e.detail||upload.statusText)}const d=await upload.json();return{id:d.id,voice_id:v.id,duration:d.duration,transcript:v.transcript||"",file_type:ext,path:v.path}}$("yt-btn").addEventListener("click",()=>{const url=$("yt-url").value.trim();if(!url)return;$("yt-btn").disabled=!0,$("yt-progress").textContent="Starting download\u2026";const es=new EventSource("/api/download-yt?url="+encodeURIComponent(url));es.onmessage=e=>{const d=JSON.parse(e.data);d.error?(toast("Download failed: "+d.error,"error"),$("yt-progress").textContent=d.error,$("yt-btn").disabled=!1,es.close()):d.done?(es.close(),$("yt-btn").disabled=!1,$("yt-progress").textContent="Done!",loadAudioId(d.id,d.duration),toast("YouTube audio loaded","success")):($("yt-progress").textContent=d.msg||"",d.pct&&status("Downloading\u2026 "+d.pct+"%"))},es.onerror=()=>{es.close(),$("yt-btn").disabled=!1}});const RAW_MIC_CONSTRAINTS={echoCancellation:!1,noiseSuppression:!1,autoGainControl:!1};async function visibleMicrophoneCount(){var _a2;if(!((_a2=navigator.mediaDevices)!=null&&_a2.enumerateDevices))return null;try{return(await navigator.mediaDevices.enumerateDevices()).filter(device=>device.kind==="audioinput").length}catch{return null}}async function microphoneErrorMessage2(error){const name=(error==null?void 0:error.name)||"",message=(error==null?void 0:error.message)||"",lowerMessage=message.toLowerCase(),micCount=await visibleMicrophoneCount();return name==="NotFoundError"||lowerMessage.includes("requested device not found")?micCount===0?"No microphone is visible to this browser. Connect or enable an input device in your OS/browser settings, then reload.":"The browser can see a microphone, but cannot open the selected/default input. Check the site permission and OS input selection, then reload.":name==="NotAllowedError"||name==="PermissionDeniedError"?"Microphone permission is blocked for this site. Allow microphone access in the address bar, then reload.":name==="NotReadableError"?"The microphone is busy or unavailable. Close other apps using it, then try again.":name==="SecurityError"?"Microphone access requires localhost or HTTPS.":message||"Microphone failed."}async function requestMicrophoneStream2(options={}){var _a2;if(!((_a2=navigator.mediaDevices)!=null&&_a2.getUserMedia))throw new Error("Microphone requires HTTPS. Open the app via https://... or access it on localhost.");if(!options.raw)return navigator.mediaDevices.getUserMedia({audio:!0});try{return await navigator.mediaDevices.getUserMedia({audio:RAW_MIC_CONSTRAINTS})}catch(e){if((e==null?void 0:e.name)==="OverconstrainedError"||(e==null?void 0:e.name)==="NotFoundError")return navigator.mediaDevices.getUserMedia({audio:!0});throw e}}let _cloneMonState={stream:null,recordStream:null,audioCtx:null,sourceNode:null,gainNode:null,analyser:null,meterRaf:null,waveRing:null,monitoring:!1};function _cloneRenderMeter(level=0,db=-1/0,clipped=!1){const meter=$("clone-mic-meter");if(!meter)return;if(!meter.children.length)for(let i=0;i<18;i++){const b=document.createElement("div");b.className="bar",meter.appendChild(b)}const active=Math.round(Math.max(0,Math.min(1,level))*meter.children.length);[...meter.children].forEach((bar,i)=>{bar.className="bar",bar.style.height=7+Math.min(i,active)*1.55+"px",i<active&&(bar.classList.add("on"),db>-12&&i>11&&bar.classList.add("hot"),clipped&&i>14&&bar.classList.add("clip"))});const el=$("clone-db-readout");el&&(el.textContent=Number.isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB")}function _cloneStartMeter(){if(!_cloneMonState.analyser)return;_cloneMonState.meterRaf&&cancelAnimationFrame(_cloneMonState.meterRaf);const data=new Float32Array(_cloneMonState.analyser.fftSize),canvas=$("clone-live-wave"),RING=300,ADD=10;_cloneMonState.waveRing=new Float32Array(RING);const tick=()=>{_cloneMonState.analyser.getFloatTimeDomainData(data);let sum=0,peak=0;for(const s of data)sum+=s*s,peak=Math.max(peak,Math.abs(s));const rms=Math.sqrt(sum/data.length),db=rms>0?20*Math.log10(rms):-1/0,level=Number.isFinite(db)?(db+60)/60:0;if(_cloneRenderMeter(level,db,peak>.98),canvas&&_cloneMonState.waveRing){const ring=_cloneMonState.waveRing;ring.copyWithin(0,ADD);for(let i=0;i<ADD;i++)ring[RING-ADD+i]=data[Math.floor(i*data.length/ADD)];const ctx=canvas.getContext("2d"),w=canvas.width,h=canvas.height;ctx.clearRect(0,0,w,h),ctx.beginPath(),ctx.strokeStyle=peak>.98?"#f38ba8":db>-12?"#f9e2af":"#a6e3a1",ctx.lineWidth=1.5;const mid=h/2;for(let i=0;i<RING;i++){const x=i/RING*w,y=mid-ring[i]*mid*.85;i===0?ctx.moveTo(x,y):ctx.lineTo(x,y)}ctx.stroke()}_cloneMonState.meterRaf=requestAnimationFrame(tick)};tick()}async function _cloneStartMonitor(){var _a2;if(_cloneMonState.recordStream)return;const AudioCtx=window.AudioContext||window.webkitAudioContext;if(_cloneMonState.stream=await requestMicrophoneStream2({raw:!0}),AudioCtx){_cloneMonState.audioCtx=new AudioCtx,_cloneMonState.sourceNode=_cloneMonState.audioCtx.createMediaStreamSource(_cloneMonState.stream),_cloneMonState.gainNode=_cloneMonState.audioCtx.createGain(),_cloneMonState.analyser=_cloneMonState.audioCtx.createAnalyser(),_cloneMonState.analyser.fftSize=1024;const dest=_cloneMonState.audioCtx.createMediaStreamDestination(),gainVal=parseFloat((_a2=$("clone-mic-gain"))==null?void 0:_a2.value)||1;_cloneMonState.gainNode.gain.value=gainVal,_cloneMonState.sourceNode.connect(_cloneMonState.gainNode),_cloneMonState.gainNode.connect(_cloneMonState.analyser),_cloneMonState.gainNode.connect(dest),_cloneMonState.recordStream=dest.stream,_cloneStartMeter()}else _cloneMonState.recordStream=_cloneMonState.stream;_cloneMonState.monitoring=!0,$("clone-monitor-btn")&&($("clone-monitor-btn").disabled=!0),$("clone-monitor-stop")&&($("clone-monitor-stop").disabled=!1)}function _cloneStopMonitor(){_cloneMonState.meterRaf&&cancelAnimationFrame(_cloneMonState.meterRaf),_cloneMonState.meterRaf=null,[_cloneMonState.sourceNode,_cloneMonState.gainNode,_cloneMonState.analyser].forEach(n=>{try{n&&n.disconnect()}catch{}}),_cloneMonState.stream&&_cloneMonState.stream.getTracks().forEach(t=>t.stop()),_cloneMonState.recordStream&&_cloneMonState.recordStream.getTracks().forEach(t=>t.stop()),_cloneMonState.audioCtx&&_cloneMonState.audioCtx.close().catch(()=>{}),Object.assign(_cloneMonState,{stream:null,recordStream:null,sourceNode:null,gainNode:null,analyser:null,audioCtx:null,monitoring:!1,waveRing:null}),_cloneRenderMeter(0,-1/0,!1);const wc=$("clone-live-wave");wc&&wc.getContext("2d").clearRect(0,0,wc.width,wc.height),$("clone-monitor-btn")&&($("clone-monitor-btn").disabled=!1),$("clone-monitor-stop")&&($("clone-monitor-stop").disabled=!0)}(_a=$("clone-monitor-btn"))==null||_a.addEventListener("click",async()=>{try{await _cloneStartMonitor(),status("Mic level monitor active")}catch(e){const m=await microphoneErrorMessage2(e);toast(m,"error")}}),(_b=$("clone-monitor-stop"))==null||_b.addEventListener("click",()=>{_cloneStopMonitor(),status("Mic level monitor stopped")}),(_c=$("clone-mic-gain"))==null||_c.addEventListener("input",()=>{const v=parseFloat($("clone-mic-gain").value)||0;$("clone-mic-gain-value")&&($("clone-mic-gain-value").textContent=v.toFixed(2)+"x"),_cloneMonState.gainNode&&(_cloneMonState.gainNode.gain.value=v)}),_cloneRenderMeter();let mediaRec=null,recChunks=[],recTimer=null,recSecs=0;$("rec-start-btn").addEventListener("click",async()=>{try{await _cloneStartMonitor(),recChunks=[],recSecs=0,$("rec-time").textContent="0:00",$("rec-indicator").classList.add("active"),$("rec-start-btn").disabled=!0,$("rec-stop-btn").disabled=!1,recTimer=setInterval(()=>{recSecs++,$("rec-time").textContent=Math.floor(recSecs/60)+":"+String(recSecs%60).padStart(2,"0")},1e3),mediaRec=new MediaRecorder(_cloneMonState.recordStream||_cloneMonState.stream,{audioBitsPerSecond:256e3}),mediaRec.ondataavailable=e=>{e.data.size&&recChunks.push(e.data)},mediaRec.onstop=async()=>{clearInterval(recTimer),$("rec-indicator").classList.remove("active");const blob=new Blob(recChunks,{type:mediaRec.mimeType||"audio/webm"}),ext=(mediaRec.mimeType||"").includes("ogg")?".ogg":".webm";_cloneStopMonitor(),await uploadFile(new File([blob],"recording"+ext,{type:blob.type}))},mediaRec.start(100),status("Recording\u2026")}catch(e){_cloneStopMonitor(),toast(await microphoneErrorMessage2(e),"error")}}),$("rec-stop-btn").addEventListener("click",()=>{mediaRec&&mediaRec.state!=="inactive"&&mediaRec.stop(),$("rec-start-btn").disabled=!1,$("rec-stop-btn").disabled=!0}),$("trim-btn").addEventListener("click",async()=>{var _a2,_b2,_c2;if(!currentFileId2){toast("No audio loaded","error");return}try{const r=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:currentFileId2,start:parseFloat($("trim-start").value)||0,end:parseFloat($("trim-end").value)||0})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();trimmedFileId2=d.id,designedFileId2=null,$("trim-audio").src="/api/audio/"+d.id,$("trim-audio").style.display="",$("no-audio-hint").style.display="none",switchTab("save"),toast("Trim done","success"),(_b2=(_a2=$("transcript-area"))==null?void 0:_a2.closest(".card"))==null||_b2.scrollIntoView({behavior:"smooth",block:"center"}),(_c2=window._cloneAutoTranscribe)==null||_c2.call(window)}catch(e){toast("Trim failed: "+e.message,"error")}});const DESIGN_LANG_CODE={Auto:"EN",English:"EN",Chinese:"ZH",Japanese:"JA",Korean:"KO",German:"DE",French:"FR",Spanish:"ES",Italian:"IT",Portuguese:"PT",Russian:"RU"},DESIGN_GENDER_WORD={F:"female",M:"male",N:"neutral"},DESIGN_PRESET_KEY="vcf-design-presets",DESIGN_PRESET_SEEDED_KEY="vcf-design-presets-seeded-v2",DEFAULT_DESIGN_PRESETS={EN_M_Young_Energetic:{description:"Young adult male voice, clear English, bright and energetic, moderately high pitch, quick but controlled speaking rate, confident and friendly, suitable for tutorials or streaming.",sample_text:"Hey everyone, welcome back. Today we are going to move quickly, keep it clear, and make this setup feel easy.",language:"English",gender:"M"},EN_F_Warm_Narrator:{description:"Adult female English narrator, warm and smooth, medium pitch, calm pace, gentle emotion, clear articulation, suited for audiobooks and voice assistant responses.",sample_text:"The room grew quiet as the morning light touched the window, and for a moment everything felt simple and kind.",language:"English",gender:"F"},DE_M_Elderly_Documentary:{description:"Aeltere maennliche deutsche Stimme, tief und resonant, langsam und gelassen, klar artikuliert, ruhig und dokumentarisch, mit serioeser und vertrauensvoller Praesenz.",sample_text:"Seit vielen Jahren beobachten wir diesen Ort, seine Geschichte und die Menschen, die ihn mit Leben fuellen.",language:"German",gender:"M"},DE_F_Young_Friendly:{description:"Junge weibliche deutsche Stimme, hell und freundlich, natuerliche Sprechgeschwindigkeit, klare Aussprache, leicht optimistisch und nahbar, passend fuer Assistenten und kurze Erklaerungen.",sample_text:"Hallo, schoen dass du da bist. Ich zeige dir kurz, wie alles funktioniert, Schritt fuer Schritt.",language:"German",gender:"F"},EN_N_Old_Wise_Assistant:{description:"Older neutral English voice, gentle and wise, slightly low pitch, slow measured pace, soothing tone, very clear pronunciation, calm personality for guidance and reflective narration.",sample_text:"Take a slow breath. We will look at the facts carefully, choose the next step, and keep moving.",language:"English",gender:"N"}},QWEN_DESIGN_SAMPLES={"qwen-timbre-reuse":{title:"Qwen Timbre Reuse",summary:"Reference clip for designing a reusable teen character timbre.",description:"Male, 17 years old, tenor range, gaining confidence - deeper breath support now, though vowels still tighten when nervous",text:"H-hey! You dropped your... uh... calculus notebook? I mean, I think it's yours? Maybe?",language:"English",gender:"M"},"acoustic-sausage-announcer":{title:"Acoustic Attribute Control - British announcer",summary:"Fast, loud, articulate British male delivery with excitement and performative authority.",description:`gender: Male.
pitch: Low male pitch with significant upward inflections for emphasis and excitement.
speed: Fast-paced delivery with deliberate pauses for dramatic effect.
volume: Loud and projecting, increasing notably during moments of praise and announcements.
age: Young adult to middle-aged adult.
clarity: Highly articulate and distinct pronunciation.
fluency: Very fluent speech with no hesitations.
accent: British English.
texture: Bright and clear vocal texture.
emotion: Enthusiastic and excited, especially when complimenting.
tone: Upbeat, authoritative, and performative.
personality: Confident, extroverted, and engaging.`,text:"Nine different, exciting ways of cooking sausage. Incredible. There were three outstanding deliveries in terms of the sausage being the hero. The first dish that we want to dissect, this individual smartly combined different proteins in their sausage. Great seasoning. The blend was absolutely spot on. Congratulations. Please step forward. Natasha.",language:"English",gender:"M"},"acoustic-character-laugh":{title:"Acoustic Attribute Control - theatrical character",summary:"Artificially high male character voice shifting from loud forced amusement to deliberate resignation.",description:`gender: Male.
pitch: Artificially high-pitched, slightly lowering after the initial laugh.
speed: Rapid during the laugh, then slowing to a deliberate pace.
volume: Loud laugh transitioning to a standard conversational level.
age: Young adult to middle-aged, performing a character voice.
clarity: Clear and distinct articulation.
fluency: Fluent delivery without hesitation.
accent: American English.
texture: Slightly strained and somewhat nasal quality.
emotion: Forced amusement shifting to feigned resignation.
tone: Initially playful, then shifts to a slightly put-upon tone.
personality: Theatrical and expressive.`,text:"Good one. Okay, fine, I'm just gonna leave this sock monkey here. Goodbye.",language:"English",gender:"M"},"age-control-surly-elvis":{title:"Age Control - middle-aged gravel",summary:"Low, resonant, slightly gravelly American male voice with a commanding opening.",description:`gender: Male.
pitch: Low male pitch, generally stable.
speed: Deliberate pace, slowing slightly after the initial exclamation.
volume: Starts loud, then transitions to a projected conversational volume.
age: Middle-aged adult.
clarity: High clarity with distinct pronunciation.
fluency: Highly fluent.
accent: American English.
texture: Resonant and slightly gravelly.
emotion: Initially commanding, shifting to narrative amusement.
tone: Authoritative start, moving to an engaging, descriptive tone.
personality: Confident and performative.`,text:"Older gentleman, 110, maybe 111 years old, sort of a surly Elvis thing happening with him. He smiles like this. Seen him around?",language:"English",gender:"M"},"gradual-control-anger":{title:"Gradual Control - emotional escalation",summary:"Female voice that begins neutral and quickly escalates into sharp anger and accusation.",description:`gender: Female.
pitch: Mid-range female pitch, rising sharply with frustration.
speed: Starts measured, then accelerates rapidly during emotional outburst.
volume: Begins conversational, escalates quickly to loud and forceful.
age: Young adult to middle-aged.
clarity: High clarity and distinct articulation throughout.
fluency: Highly fluent with no significant pauses or fillers.
accent: General American English.
texture: Bright and clear vocal quality.
emotion: Shifts abruptly from neutral acceptance to intense resentment and anger.
tone: Initially accepting, becomes sharply accusatory and confrontational.
personality: Assertive and emotionally expressive when provoked.`,text:"Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you-",language:"English",gender:"F"},"human-likeness-digital-nomad":{title:"Human-likeness - casual self-aware monologue",summary:"Warm male conversational voice with natural laughter, hesitations, and self-deprecating humor.",description:"A relaxed, naturally expressive male voice in his late twenties to early thirties, with a moderately low pitch, casual speaking rate, and conversational volume; deliver lines with a light, self-deprecating tone, breaking into genuine, easygoing laughter at moments of embarrassment, while maintaining clear articulation and an overall warm, approachable clarity.",text:`Yeah, so--uh--I'm a digital nomad, right? So... pretty much all my communication is just, like, texts and messages. And now, you know, there's these AI agents that can, uh... reply for you? Which is--heh--convenient, sure, I guess? But also... kinda delicate, you know?
Like, you'll type something super short--like, "Yep, sounds good"--and it'll turn that into this whole... warm, polished paragraph. Like, way nicer than I'd ever write myself. huh... ha Seriously, I sound like a Hallmark card all of a sudden.
But then... once you outsource that... what's the other person actually hearing? Are they hearing me... or just some... generic, friendly-bot voice? Man, that's weird to even say out loud.`,language:"English",gender:"M"},"background-marcus-cole":{title:"Background Information - Marcus Cole",summary:"Broadcast booth announcer profile with bright, agile, urgent delivery.",description:`Character Name: Marcus Cole
Voice Profile: A bright, agile male voice with a natural upward lift, delivering lines at a brisk, energetic pace. Pitch leans high with spark, volume projects clearly--near-shouting at peaks--to convey urgency and excitement. Speech flows seamlessly, fluently, each word sharply defined, riding a current of dynamic rhythm.
Background: Longtime broadcast booth announcer for national television, specializing in live interstitials and public engagement spots. His voice bridges segments, rallies action, and keeps momentum alive--from voter drives to entertainment news.
Presence: Late 50s, neatly groomed, dressed in a crisp shirt under studio lights. Moves with practiced ease, eyes locked on the script, energy coiled and ready.
Personality: Energetic, precise, inherently engaging. He doesn't just read--he propels. Behind the speed is intent: to inform fast, to move people to act. Whether it's "text VOTE to 5703" or a star-studded tease, he makes it feel immediate, vital.`,text:"Lot being you watching. 1-866-IDLE-03 for JPL. That's 1-866-436-5703. Or text the word VOTE to 5703. Diana DeGarmo's next with more from the movies right after this brief intermission on American Idol.",language:"English",gender:"M"},"timbre-reuse-lucas-mia":{title:"Timbre Reuse - Lucas and Mia",summary:"Two-character teen dialogue using native VoiceDesign speaker-profile switching.",description:`"Lucas": "Male, 17 years old, tenor range, gaining confidence - deeper breath support now, though vowels still tighten when nervous"
"Mia": "Female, 16 years old, mezzo-soprano range, softening - lowering register to intimate speaking voice, consonants softening"`,text:`Lucas:H-hey! You dropped your... uh... calculus notebook? I mean, I think it's yours? Maybe?
Mia:Oh wow, my mortal enemy - Mr. Thompson's problem sets. Thanks for rescuing me from that F.
Lucas:No problem! I actually... kinda finished those already? If you want to compare answers or something...
Mia:Is this your sneaky way of saying you want to study together, Lucas? Because I saw you staring during lab partners sign-up.
Lucas:What? No! I mean yes but not like... I just think you're... your titration technique is really precise!
Mia:That's the nerdiest compliment I've ever gotten. Tell you what - help me survive pre-calc and I'll teach you how to actually flirt.
Lucas:Wow, harsh. And here I thought my titration line was smooth.
Mia:It was adorable. Like when you tripped over your shoelaces in the hall yesterday. Or that time you-
Lucas:Okay okay! I get it, I'm a disaster. So... library after school? I'll bring the graphing calculators?
Mia:Only if you promise not to spill coffee on my notes again... though I guess watching you panic-clean was pretty cute.`,language:"English",gender:"N",dialogue:!0}};let currentDesignSource=null;function loadDesignPresets2(){try{return JSON.parse(localStorage.getItem(DESIGN_PRESET_KEY)||"{}")}catch{return{}}}function saveDesignPresets(presets){localStorage.setItem(DESIGN_PRESET_KEY,JSON.stringify(presets))}async function syncDesignPresetsToServer(){try{await fetch("/api/voice-design-presets",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(loadDesignPresets2())}),renderIntegrationSnippets()}catch(e){status("Voice Design preset sync failed: "+e.message)}}function seedDesignPresets(){const presets=loadDesignPresets2();let changed=!1;Object.entries(DEFAULT_DESIGN_PRESETS).forEach(([name,preset])=>{if(!presets[name]){presets[name]=preset,changed=!0;return}["description","sample_text","language","gender"].forEach(key=>{!presets[name][key]&&preset[key]&&(presets[name][key]=preset[key],changed=!0)})}),(changed||!localStorage.getItem(DESIGN_PRESET_SEEDED_KEY))&&saveDesignPresets(presets),localStorage.setItem(DESIGN_PRESET_SEEDED_KEY,"1"),changed&&syncDesignPresetsToServer()}function refreshDesignPresetSelect(){const presets=loadDesignPresets2(),sel=$("design-preset-select"),prev=sel.value;sel.innerHTML='<option value="">\u2014 preset \u2014</option>',Object.keys(presets).sort((a,b)=>a.localeCompare(b)).forEach(name=>{const opt=document.createElement("option");opt.value=opt.textContent=name,sel.appendChild(opt)}),presets[prev]&&(sel.value=prev),renderDesignPresetLibrary()}function applyDesignPreset(name){const preset=loadDesignPresets2()[name];if(!preset){toast("Preset not found","error");return}$("design-instruct").value=preset.description||"",$("design-sample-text").value=preset.sample_text||preset.text||$("design-sample-text").value||"",$("design-language").value=preset.language||"Auto",$("design-gender").value=preset.gender||"N",$("design-preset-name").value=name,$("design-preset-select").value=name,currentDesignSource={name,gender:preset.gender||"N",language:preset.language||"Auto",text:$("design-sample-text").value||"",description:preset.description||""},toast("Preset loaded: "+name,"success")}function renderDesignPresetLibrary(){const lib=$("design-preset-library");if(!lib)return;const presets=loadDesignPresets2(),names=Object.keys(presets).sort((a,b)=>a.localeCompare(b));if(!names.length){lib.innerHTML='<div class="note">No saved prompt presets yet.</div>';return}lib.innerHTML="",names.forEach(name=>{const p=presets[name],row=document.createElement("div");row.className="design-preset-row",row.innerHTML=`
<strong>${escHtml(name)}</strong>
<span>${escHtml(p.gender||"N")}</span>
<span>${escHtml(p.language||"Auto")}</span>
<span class="preset-desc" title="${escHtml(p.description||"")}">${escHtml(p.description||"")}</span>
<span class="preset-transcript" title="${escHtml(p.sample_text||p.text||"")}">${escHtml(p.sample_text||p.text||"")}</span>
<span class="preset-actions">
<button class="btn-secondary preset-use">Use</button>
<button class="btn-primary preset-preview">Preview</button>
<button class="btn-secondary preset-delete">Delete</button>
</span>
`,row.querySelector(".preset-use").addEventListener("click",()=>applyDesignPreset(name)),row.querySelector(".preset-delete").addEventListener("click",()=>{const all=loadDesignPresets2();delete all[name],saveDesignPresets(all),syncDesignPresetsToServer(),refreshDesignPresetSelect(),toast("Preset deleted: "+name,"success")}),row.querySelector(".preset-preview").addEventListener("click",async e=>{e.currentTarget.disabled=!0;try{const sampleText=p.sample_text||p.text||$("design-sample-text").value,r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(p.description||"",sampleText,p.language||"Auto",p))});if(!r.ok){const err=await r.json().catch(()=>({}));throw new Error(err.detail||r.statusText)}const d=await r.json();$("design-audio").src="/api/audio/"+d.id,$("design-result").style.display="flex",$("design-audio").play().catch(()=>{})}catch(err){toast("Preset preview failed: "+err.message,"error")}finally{e.currentTarget.disabled=!1}}),lib.appendChild(row)})}function renderQwenSampleCards(){const list=$("qwen-sample-list");list&&(list.innerHTML="",Object.entries(QWEN_DESIGN_SAMPLES).forEach(([key,sample])=>{const card=document.createElement("div");card.className="qwen-sample design-sample-grid",card.dataset.qwenSample=key,card.innerHTML=`
<strong title="${escHtml(sample.summary||sample.title||key)}">${escHtml(sample.title||key)}</strong>
<span class="sample-sex">${escHtml(sample.gender||"N")}</span>
<span class="sample-language">${escHtml(sample.language||"Auto")}</span>
<span class="sample-desc" title="${escHtml(sample.description||"")}">${escHtml(sample.description||"")}</span>
<span class="sample-text" title="${escHtml(sample.text||"")}">${escHtml(sample.text||"")}</span>
<div class="qwen-sample-actions">
<button class="btn-primary qwen-preview">Preview</button>
<button class="btn-secondary qwen-use">Use</button>
<span class="note qwen-state"></span>
</div>
<audio controls></audio>
`,list.appendChild(card)}))}function applyQwenSample(sample){$("design-instruct").value=sample.description,$("design-sample-text").value=sample.text,$("design-language").value=sample.language,$("design-gender").value=sample.gender,currentDesignSource=sample,$("design-result").style.display="none",$("design-save-result").style.display="none",$("design-instruct").scrollIntoView({behavior:"smooth",block:"nearest"})}function isDialogueDesign(instruct,text,source=null){if(source&&source.dialogue)return!0;const speakers=new Set;if(String(instruct||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^"?([^":]+)"?\s*:\s*"?(.+?)"?$/);match&&speakers.add(match[1].trim())}),speakers.size<2)return!1;const turnSpeakers=new Set;return String(text||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^([^:]{1,40}):\s*(.+)$/);match&&speakers.has(match[1].trim())&&turnSpeakers.add(match[1].trim())}),turnSpeakers.size>=2}function voiceDesignPayload(instruct,sampleText,language,source=null,gender=null){var _a2;return{instruct,sample_text:sampleText,language,gender:gender||(source==null?void 0:source.gender)||((_a2=$("design-gender"))==null?void 0:_a2.value)||"",dialogue:isDialogueDesign(instruct,sampleText,source)}}let _dVoiceIdManual=!1;function designSafeName(name){const base=name||"VoiceDesign";return(typeof _umlautSafe=="function"?_umlautSafe(base):String(base)).replace(/^[A-Z]{2}_[FMN]_/,"").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,42)||"VoiceDesign"}function voiceIdSafePart(value,fallback="style"){return(typeof _umlautSafe=="function"?_umlautSafe(value||fallback):String(value||fallback)).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,32)||fallback}function 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)})),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='<span class="reh-imsdb-spinner"></span> Transcribing\u2026',status2&&(status2.className="clone-tr-status working",status2.innerHTML='<span class="reh-imsdb-spinner"></span> Listening to your recording\u2026'),area&&(area.classList.add("transcribing"),area.placeholder="Transcribing your audio \u2014 please wait\u2026");try{const backend=((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)||"configured",r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,backend})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();area&&(area.value=d.text),status2&&(status2.className="clone-tr-status done",status2.innerHTML='<span class="mdi mdi-check-circle"></span> Transcribed'),toast("Transcription complete","success"),(_b2=window._cloneScheduleAutoSave)==null||_b2.call(window)}catch(e){status2&&(status2.className="clone-tr-status error",status2.innerHTML=`<span class="mdi mdi-alert-circle"></span> Failed: ${escHtml(e.message||String(e))}`),toast("Transcription failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,area&&(area.classList.remove("transcribing"),area.placeholder="Type or auto-transcribe the spoken text\u2026")}}),function(){var _a2,_b2,_c2;const g=id=>document.getElementById(id);let _idManual=!1,_prevName="Sam",_autoSaveTimer=null,_lastAutoSaved="";function buildVoiceId(){var _a3,_b3,_c3;if(_idManual){scheduleAutoSave();return}const lang=((_a3=g("lang-select"))==null?void 0:_a3.value)||"EN",gender=((_b3=g("gender-select"))==null?void 0:_b3.value)||"N",name=(((_c3=g("name-input"))==null?void 0:_c3.value)||"").trim().replace(/\s+/g,""),vid=g("voice-id-input");vid&&name&&(vid.value=`${lang}_${gender}_${name}`,vid.dispatchEvent(new Event("input"))),scheduleAutoSave()}const nameField=g("clone-your-name");nameField==null||nameField.addEventListener("input",()=>{const name=nameField.value.trim();if(!name)return;const sample=g("clone-sample-text");if(sample){const prev=sample.dataset.sampleName||_prevName,re=new RegExp("\\b"+prev.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b");re.test(sample.value)&&(sample.value=sample.value.replace(re,name)),sample.dataset.sampleName=name}_prevName=name;const ni=g("name-input");ni&&(ni.value=name.replace(/\s+/g,"")),buildVoiceId()}),["lang-select","gender-select"].forEach(id=>["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,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:loadAudioId,upload:uploadFile},design:{run:runVoiceDesign,buildPayload:voiceDesignPayload}})}(window),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='<div class="inspector-placeholder"><span><span class="mdi mdi-table"></span></span><p>Table View Mode<br>Click a row to exit table view and edit.</p></div>')}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='<option value="">All languages</option>'+langs.map(x=>`<option value="${escHtml(x)}">${escHtml(libraryLanguageLabel(x))} (${escHtml(x)})</option>`).join(""),sexSel.innerHTML='<option value="">All</option>'+sexes.map(x=>`<option value="${escHtml(x)}">${escHtml(SEX_FILTER_LABELS[x]||x)}</option>`).join(""),typeSel.innerHTML='<option value="">All</option>'+types.map(x=>`<option value="${escHtml(x)}">${escHtml(x.toUpperCase())}</option>`).join(""),tagSel&&(tagSel.innerHTML='<option value="">All tags</option>'+tags.map(x=>`<option value="${escHtml(x)}">${escHtml(x)}</option>`).join("")),groupSel&&(groupSel.innerHTML='<option value="">All groups</option>'+groups.map(x=>`<option value="${escHtml(x)}">${escHtml(x)}</option>`).join("")),langSel.value=langs.includes(keep.lang)?keep.lang:"",sexSel.value=sexes.includes(keep.sex)?keep.sex:"",typeSel.value=types.includes(keep.type)?keep.type:"",tagSel&&(tagSel.value=tags.includes(keep.tag)?keep.tag:""),groupSel&&(groupSel.value=groups.includes(keep.group)?keep.group:"")}function readLibraryFilters(){var _a2,_b2,_c2,_d2,_e2;_libraryFilters.text=(((_a2=$("library-filter-text"))==null?void 0:_a2.value)||"").trim().toLowerCase(),_libraryFilters.lang=((_b2=$("library-filter-lang"))==null?void 0:_b2.value)||"",_libraryFilters.sex=((_c2=$("library-filter-sex"))==null?void 0:_c2.value)||"",_libraryFilters.type=((_d2=$("library-filter-type"))==null?void 0:_d2.value)||"",_libraryFilters.rating=((_e2=$("library-filter-rating"))==null?void 0:_e2.value)||""}function libraryFilterMatch(v){const f=_libraryFilters;if(f.lang&&libraryVoiceLang(v)!==f.lang||f.sex&&(v.gender||"")!==f.sex||f.type&&voiceFileType(v)!==f.type)return!1;if(f.rating){const r=Number(v.rating||0),wanted=Number(f.rating);if(wanted===0&&r!==0||wanted===1&&r<1||wanted>1&&r<wanted)return!1}if(f.text){const tags=Array.isArray(v.tags)?v.tags.join(" "):v.tags||v.tag||"";if(![v.id,v.name,v.transcript,v.note,v.file_type,v.flag,v.gender,v.group,v.origin,tags].map(x=>String(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);return f!=null?f.toFixed(2)+"x":"-"}function fmtElapsed(v){const e=voiceBenchmarkElapsed(v),b=voiceBenchmark(v);return!b||!b.ok?b&&!b.ok?"ERR":"-":e!=null?e.toFixed(1)+"s":"-"}function 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;ch<buffer.numberOfChannels;ch++){const data=buffer.getChannelData(ch);count+=data.length;for(let i=0;i<data.length;i++){const sample=data[i];sum+=sample*sample,peak=Math.max(peak,Math.abs(sample))}}const rms=Math.sqrt(sum/Math.max(1,count)),dbfs=rms>0?20*Math.log10(rms):null,peakDbfs=peak>0?20*Math.log10(peak):null;return{dbfs:dbfs==null?null:Number(dbfs.toFixed(2)),peak_dbfs:peakDbfs==null?null:Number(peakDbfs.toFixed(2))}}async function clientCalculateVoiceDb(){const voices=_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])=>`<div class="insight"><strong>${value}</strong><span>${label}</span></div>`).join("");return}if(state==="error"){el.innerHTML='<div class="insight"><strong>Failed</strong><span>Library load</span></div>';return}const total=_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`<div class="insight${activeCls}"${filter}${title}><strong>${escHtml(item.value)}</strong><span>${escHtml(item.label)}</span></div>`}).join(""),el.querySelectorAll("[data-filter]").forEach(tile=>{const activate=()=>setLibraryIssueFilter(tile.dataset.filter||"");tile.addEventListener("click",activate),tile.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),activate())})})}function shouldRenderVoiceLibrary(){const section=$("s-voices");return!section||section.classList.contains("is-active")}async function 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='<div style="color:var(--red);padding:8px;font-size:13px">Failed to load voices: '+(e.message||String(e))+"</div>"),$("voice-count").textContent="Load failed",updateLibraryInsights("error")),status("Voice library load failed: "+e.message),!silent)throw e}finally{setBusyButton("refresh-voices-btn",!1),_libraryLoadPromise2=null}})(),_libraryLoadPromise2)}$("refresh-voices-btn").addEventListener("click",()=>{_vlCacheClear(),loadVoiceLibrary2({refresh:!0})}),$("sync-voice-folders-btn").addEventListener("click",async()=>{$("sync-voice-folders-btn").disabled=!0,status("Syncing active_voices and hidden_voices\u2026");try{const r=await fetch("/api/voices/sync-folders",{method:"POST"});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();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=>`<span>${escHtml(x)}</span>`).join("")}const last=$("benchmark-live-last");last&&stats.last&&(last.textContent=stats.last)}function hideBenchmarkProgress(){$("benchmark-progress").hidden=!0,$("benchmark-progress-bar").style.width="0%",$("benchmark-live-last")&&($("benchmark-live-last").textContent="")}async function 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);for(let i=0;i<voices.length;i++){const voice=voices[i],row=document.querySelector(`.vl-row[data-id="${CSS.escape(voice.id)}"]`);row&&(row.classList.remove("benchmarking-pending"),row.classList.add("benchmarking-active"),row.scrollIntoView({behavior:"smooth",block:"center"})),setBenchmarkProgress(i,total,`Benchmarking ${voice.id}`,stats),status(`Benchmarking ${voice.id} (${i+1} / ${total})...`);try{const d=await runVoiceBenchmark(voice.id,{text});if(d){aggregate.benchmarked+=Number(d.benchmarked||0),aggregate.errors.push(...d.errors||[]),aggregate.voices.push(...d.voices||[]),mergeBenchmarkResults(d);const hit=(d.voices||[]).find(x=>x.voice_id===voice.id),b=hit&&hit.benchmark;if(b&&b.ok){if(stats.ok++,b.realtime_ok===!1&&stats.slow++,stats.last=`${voice.id}: ${Number(b.elapsed_sec||0).toFixed(1)}s${b.speed!=null?` \xB7 ${Number(b.speed).toFixed(2)}x`:""}${b.realtime_ok===!1?" \xB7 slow":""}`,row){const bc=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:""}`}}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=`<div class="audiobook-box"><div class="audiobook-title"><span class="mdi mdi-check-decagram-outline"></span> Verifying voices</div>
<div class="audiobook-msg" id="vv-msg">0 / ${ids.length}</div>
<div class="reader-synth-track"><div class="reader-synth-fill" id="vv-fill"></div></div>
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="vv-cancel">Cancel</button></div></div>`,document.body.appendChild(ov);let cancelled=!1;ov.querySelector("#vv-cancel").addEventListener("click",()=>{cancelled=!0});const fill=ov.querySelector("#vv-fill"),msg=ov.querySelector("#vv-msg"),results=await _verifyVoicesRoundtrip(ids,{onProgress:(done,total,id)=>{if(fill&&(fill.style.width=done/total*100+"%"),msg&&(msg.textContent=`${done} / ${total} \xB7 ${id}`),cancelled)throw new Error("cancelled")}}).catch(()=>[]);ov.remove(),btn&&(btn.disabled=!1);const failed=results.filter(r=>!r.ok),resOv=document.createElement("div");resOv.className="audiobook-overlay",resOv.innerHTML=`<div class="audiobook-box" style="max-width:640px;max-height:80vh;overflow:auto;">
<div class="audiobook-title"><span class="mdi mdi-check-decagram-outline"></span> Voice verification results</div>
<p class="card-subtitle" style="margin:8px 0 12px;">${results.length-failed.length} / ${results.length} passed.${failed.length?" Failed voices likely need a redesign.":""}</p>
<div style="display:flex;flex-direction:column;gap:6px;">
${failed.map(r=>`<div style="border:1px solid var(--red);border-radius:6px;padding:8px;font-size:13px;">
<b>${escHtml(r.id)}</b> \u2014 ${r.error?escHtml(r.error):"score "+r.score.toFixed(2)}
${r.transcript!==void 0?`<br><span style="color:var(--text-2)">Expected: ${escHtml(r.text.slice(0,120))}</span><br><span style="color:var(--red)">Heard: ${escHtml((r.transcript||"(nothing)").slice(0,120))}</span>`:""}
</div>`).join("")||'<div style="color:var(--green)">All voices passed.</div>'}
</div>
<div style="display:flex; justify-content:flex-end; margin-top:14px;">
<button type="button" class="btn-primary btn-sm" id="vv-close">Close</button>
</div>
</div>`,document.body.appendChild(resOv),resOv.querySelector("#vv-close").addEventListener("click",()=>resOv.remove()),resOv.addEventListener("click",e=>{e.target===resOv&&resOv.remove()}),toast(`Verified ${results.length} voice(s) \u2014 ${failed.length} failed`,failed.length?"error":"success")}),(_d=$("precompute-embeddings-btn"))==null||_d.addEventListener("click",async()=>{const backend=libraryTtsBackend(),_useSelected=_bulkSelected2&&_bulkSelected2.size>0,ids=_useSelected?[..._bulkSelected2]:activeVoiceIds(),_scopeLabel=_useSelected?`${ids.length} selected`:`${ids.length} active`;if(!ids.length){toast("No voices to precompute","error");return}if(!confirm(`Precompute speaker embeddings for ${_scopeLabel} voice(s) via \u201C${backend}\u201D?
This warms each voice so the engine caches its .pt and first playback is instant.`))return;const btn=$("precompute-embeddings-btn");btn&&(btn.disabled=!0);const ov=document.createElement("div");ov.className="audiobook-overlay",ov.id="precompute-overlay",ov.innerHTML=`<div class="audiobook-box"><div class="audiobook-title"><span class="mdi mdi-flash-outline"></span> Precomputing embeddings</div>
<div class="audiobook-msg" id="pc-msg">0 / ${ids.length}</div>
<div class="reader-synth-track"><div class="reader-synth-fill" id="pc-fill"></div></div>
<div class="audiobook-actions"><button class="btn-secondary btn-sm" id="pc-cancel">Cancel</button></div></div>`,document.body.appendChild(ov);let cancel=!1;ov.querySelector("#pc-cancel").addEventListener("click",()=>{cancel=!0});const fill=ov.querySelector("#pc-fill"),msg=ov.querySelector("#pc-msg");let done=0,ok=0,failed=0;const queue=ids.slice(),worker=async()=>{for(;queue.length&&!cancel;){const id=queue.shift();msg&&(msg.textContent=`${done} / ${ids.length} \xB7 ${id}`);const voiceBackend=typeof _ttsBackendForVoice=="function"?_ttsBackendForVoice(id,backend):backend;try{await fetchTtsPreviewBlob(id,"Hallo.","wav","",voiceBackend),ok++}catch{failed++}done++,fill&&(fill.style.width=done/ids.length*100+"%")}};try{await Promise.all(Array.from({length:Math.min(2,ids.length)},worker))}finally{ov.remove(),btn&&(btn.disabled=!1)}toast(cancel?`Cancelled \u2014 ${ok} warmed`:`Precomputed ${ok} embedding(s)${failed?`, ${failed} skipped/failed`:""}`,!ok&&failed?"error":"success")});const LIB_ADD_SAMPLE_TEXTS={EN:"The clear morning light warmed the quiet studio as I described a silver train, a bright red apple, and the gentle rhythm of rain on the window.",DE:"Das klare Morgenlicht waermte das ruhige Studio, waehrend ich einen silbernen Zug, einen roten Apfel und den sanften Rhythmus des Regens am Fenster beschrieb.",IT:"La luce chiara del mattino scaldava lo studio tranquillo mentre descrivevo un treno d argento, una mela rossa e il ritmo leggero della pioggia alla finestra.",ES:"La clara luz de la manana calentaba el estudio tranquilo mientras describia un tren plateado, una manzana roja y el suave ritmo de la lluvia en la ventana.",FR:"La lumiere claire du matin rechauffait le studio calme pendant que je decrivais un train argente, une pomme rouge et le doux rythme de la pluie sur la fenetre.",PT:"A luz clara da manha aquecia o estudio tranquilo enquanto eu descrevia um comboio prateado, uma maca vermelha e o ritmo suave da chuva na janela.",NL:"Het heldere ochtendlicht verwarmde de stille studio terwijl ik een zilveren trein, een rode appel en het zachte ritme van regen op het raam beschreef.",PL:"Jasne poranne swiatlo ogrzewalo ciche studio, gdy opisywalem srebrny pociag, czerwone jablko i lagodny rytm deszczu na oknie."},LIB_ADD_SAMPLE_STORAGE_KEY="vcf-lib-add-sample-texts";function libAddSampleOverrides(){try{return JSON.parse(localStorage.getItem(LIB_ADD_SAMPLE_STORAGE_KEY)||"{}")||{}}catch{return{}}}function getLibAddSampleText(code){return libAddSampleOverrides()[code]||LIB_ADD_SAMPLE_TEXTS[code]||LIB_ADD_SAMPLE_TEXTS.EN}function saveLibAddSampleText(){const code=$("lib-add-sample-lang").value,text=$("lib-add-sample-text").value.trim(),overrides=libAddSampleOverrides();text&&text!==LIB_ADD_SAMPLE_TEXTS[code]?overrides[code]=text:delete overrides[code],localStorage.setItem(LIB_ADD_SAMPLE_STORAGE_KEY,JSON.stringify(overrides)),setLibAddStatus("Sample sentence saved")}function resetLibAddSampleText(){const code=$("lib-add-sample-lang").value,overrides=libAddSampleOverrides();delete overrides[code],localStorage.setItem(LIB_ADD_SAMPLE_STORAGE_KEY,JSON.stringify(overrides)),$("lib-add-sample-text").value=LIB_ADD_SAMPLE_TEXTS[code]||LIB_ADD_SAMPLE_TEXTS.EN,setLibAddStatus("Sample sentence reset")}function updateLibAddSampleLanguage(lang){const code=LIB_ADD_SAMPLE_TEXTS[lang]?lang:"EN";$("lib-add-sample-lang").value=code,$("lib-add-lang").value=code,$("lib-add-sample-text").value=getLibAddSampleText(code);const voiceId=$("lib-add-voice-id").value.trim();voiceId&&/^[A-Z]{2}_/.test(voiceId)&&($("lib-add-voice-id").value=voiceId.replace(/^[A-Z]{2}_/,code+"_"))}function renderLibAddMeter(level=0,db=-1/0,clipped=!1){const meter=$("lib-add-mic-meter");if(!meter.children.length)for(let i=0;i<18;i++){const bar=document.createElement("div");bar.className="bar",meter.appendChild(bar)}const active=Math.round(Math.max(0,Math.min(1,level))*meter.children.length);[...meter.children].forEach((bar,i)=>{bar.className="bar",bar.style.height=7+Math.min(i,active)*1.55+"px",i<active&&(bar.classList.add("on"),db>-12&&i>11&&bar.classList.add("hot"),clipped&&i>14&&bar.classList.add("clip"))}),$("lib-add-db-readout").textContent=Number.isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB"}function syncLibAddMicGain(){const gain=parseFloat($("lib-add-mic-gain").value)||0;$("lib-add-mic-gain-value").textContent=gain.toFixed(2)+"x",libAddState.gainNode&&(libAddState.gainNode.gain.value=gain)}function startLibAddMeter(){if(!libAddState.analyser)return;libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf);const data=new Float32Array(libAddState.analyser.fftSize),tick=()=>{libAddState.analyser.getFloatTimeDomainData(data);let sum=0,peak=0;for(const sample of data)sum+=sample*sample,peak=Math.max(peak,Math.abs(sample));const rms=Math.sqrt(sum/data.length),db=rms>0?20*Math.log10(rms):-1/0,level=Number.isFinite(db)?(db+60)/60:0;renderLibAddMeter(level,db,peak>.98),libAddState.meterRaf=requestAnimationFrame(tick)};tick()}async function ensureLibAddMicMonitor(){if(libAddState.recordStream)return;const AudioCtx=window.AudioContext||window.webkitAudioContext;if(libAddState.stream=await requestMicrophoneStream({raw:!0}),AudioCtx){libAddState.audioCtx=new AudioCtx,libAddState.sourceNode=libAddState.audioCtx.createMediaStreamSource(libAddState.stream),libAddState.gainNode=libAddState.audioCtx.createGain(),libAddState.analyser=libAddState.audioCtx.createAnalyser(),libAddState.analyser.fftSize=1024;const dest=libAddState.audioCtx.createMediaStreamDestination();syncLibAddMicGain(),libAddState.sourceNode.connect(libAddState.gainNode),libAddState.gainNode.connect(libAddState.analyser),libAddState.gainNode.connect(dest),libAddState.recordStream=dest.stream,startLibAddMeter()}else libAddState.recordStream=libAddState.stream;libAddState.monitoring=!0,$("lib-add-monitor-btn").disabled=!0,$("lib-add-monitor-stop").disabled=!1}function stopLibAddMic(){libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf),libAddState.meterRaf=null,[libAddState.sourceNode,libAddState.gainNode,libAddState.analyser].forEach(node=>{try{node&&node.disconnect()}catch{}}),libAddState.stream&&libAddState.stream.getTracks().forEach(t=>t.stop()),libAddState.recordStream&&libAddState.recordStream.getTracks().forEach(t=>t.stop()),libAddState.audioCtx&&libAddState.audioCtx.close().catch(()=>{}),libAddState.stream=null,libAddState.recordStream=null,libAddState.sourceNode=null,libAddState.gainNode=null,libAddState.analyser=null,libAddState.audioCtx=null,libAddState.monitoring=!1,$("lib-add-monitor-btn").disabled=!1,$("lib-add-monitor-stop").disabled=!0,renderLibAddMeter(0,-1/0,!1)}let libAddState={id:null,duration:0,audio:null,buffer:null,recorder:null,chunks:[],pendingSource:null,stream:null,recordStream:null,timer:null,secs:0,audioCtx:null,sourceNode:null,gainNode:null,analyser:null,meterRaf:null,monitoring:!1};window.libAddState=libAddState,$("add-new-voice-btn").addEventListener("click",()=>{$("lib-add-panel").classList.toggle("open")}),$("lib-add-sample-lang").addEventListener("change",()=>updateLibAddSampleLanguage($("lib-add-sample-lang").value)),["change","input"].forEach(ev=>$("lib-add-lang").addEventListener(ev,()=>updateLibAddSampleLanguage($("lib-add-lang").value))),$("lib-add-sample-text").addEventListener("input",debounce(saveLibAddSampleText,500)),$("lib-add-use-sample").addEventListener("click",()=>{$("lib-add-transcript").value=$("lib-add-sample-text").value.trim(),setLibAddStatus("Sample sentence copied to transcript")}),$("lib-add-reset-sample").addEventListener("click",resetLibAddSampleText),$("lib-add-mic-help-btn").addEventListener("click",()=>{$("lib-add-mic-help").classList.toggle("open")}),$("lib-add-monitor-btn").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),setLibAddStatus("Mic level monitor active")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message)}}),$("lib-add-monitor-stop").addEventListener("click",()=>{stopLibAddMic(),setLibAddStatus("Mic level monitor stopped")}),$("lib-add-mic-gain").addEventListener("input",syncLibAddMicGain),renderLibAddMeter(),syncLibAddMicGain(),updateLibAddSampleLanguage("EN");function setLibAddStatus(msg){$("lib-add-status").textContent=msg,status(msg)}function suggestLibVoiceId(filename){if($("lib-add-voice-id").value.trim())return;const base=(typeof _umlautSafe=="function"?_umlautSafe(filename||"NewVoice"):String(filename||"NewVoice")).replace(/\.[^.]+$/,"").replace(/[^A-Za-z0-9_-]+/g,"_").replace(/^_+|_+$/g,"").slice(0,60)||"NewVoice";$("lib-add-voice-id").value=`${$("lib-add-lang").value||"EN"}_${$("lib-add-gender").value||"N"}_${base}`}function loadLibAddAudio(id,duration,label="Audio"){libAddState.id=id,libAddState.duration=Number(duration)||0,libAddState.buffer=null,$("lib-add-start").value="0.00",$("lib-add-end").value=libAddState.duration?Math.min(libAddState.duration,20).toFixed(2):"0.00",$("lib-add-audio").src="/api/audio/"+id,$("lib-add-audio").style.display="",$("lib-add-wave").style.display="",attachLibAddWaveSelection(),decodeTempAudio(id).then(buffer=>{libAddState.id===id&&(libAddState.buffer=buffer,drawLibAddWave())}).catch(()=>{}),setLibAddStatus(`${label} loaded${libAddState.duration?" ("+libAddState.duration.toFixed(1)+" s)":""}`)}async function decodeTempAudio(id){const resp=await fetch("/api/audio/"+encodeURIComponent(id));if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const data=await resp.arrayBuffer();return new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(data.slice(0))}function clampLibAddTime(value){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;return Math.max(0,Math.min(duration,Number(value)||0))}function setLibAddCropRange(start,end){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;let a=clampLibAddTime(start),b=clampLibAddTime(end);Math.abs(b-a)<.05&&(b=Math.min(duration,a+Math.min(1,duration||1))),b<a&&([a,b]=[b,a]),$("lib-add-start").value=a.toFixed(2),$("lib-add-end").value=b.toFixed(2),drawLibAddWave()}function libAddWaveTimeFromEvent(e){var _a2;const rect=$("lib-add-wave").getBoundingClientRect(),x=Math.max(0,Math.min(rect.width,e.clientX-rect.left)),duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;return rect.width?x/rect.width*duration:0}function updateLibAddCropHint(){const hint=$("lib-add-crop-hint");if(!hint)return;const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||0,dur=Math.max(0,end-start);hint.textContent=dur?`Selected ${dur.toFixed(1)}s. Aim for 3-20 seconds.`:"Select 3-20 seconds for best cloning.",hint.className="crop-duration-hint "+(dur>=3&&dur<=20?"ok":dur?"warn":"")}function drawLibAddWave(){libAddState.buffer&&(drawOptimizerWave($("lib-add-wave"),libAddState.buffer,parseFloat($("lib-add-start").value)||0,parseFloat($("lib-add-end").value)||libAddState.duration||libAddState.buffer.duration),updateLibAddCropHint())}function libAddWaveSelectionPixels(e){var _a2;const rect=$("lib-add-wave").getBoundingClientRect(),duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0,start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||duration),sx=duration&&rect.width?start/duration*rect.width:0,ex=duration&&rect.width?end/duration*rect.width:rect.width;return{x:Math.max(0,Math.min(rect.width,e.clientX-rect.left)),sx,ex,start,end,duration}}function libAddWaveDragMode(e){const{x,sx,ex}=libAddWaveSelectionPixels(e),hit=16;return Math.abs(x-sx)<=hit?"start":Math.abs(x-ex)<=hit?"end":"new"}function attachLibAddWaveSelection(){const canvas=$("lib-add-wave");if(!canvas||canvas.dataset.cropReady)return;canvas.dataset.cropReady="1";let drag=null;canvas.addEventListener("pointerdown",e=>{var _a2;if(!libAddState.buffer)return;e.preventDefault();const mode=libAddWaveDragMode(e),t=libAddWaveTimeFromEvent(e),currentStart=parseFloat($("lib-add-start").value)||0,currentEnd=parseFloat($("lib-add-end").value)||libAddState.duration||0;drag={mode,anchor:t,start:currentStart,end:currentEnd},(_a2=canvas.setPointerCapture)==null||_a2.call(canvas,e.pointerId),mode==="start"?setLibAddCropRange(t,currentEnd):setLibAddCropRange(mode==="end"?currentStart:t,t),setLibAddStatus(mode==="start"?"Dragging crop start handle":mode==="end"?"Dragging crop end handle":"Drag to choose a new crop range")}),canvas.addEventListener("pointermove",e=>{if(!libAddState.buffer)return;if(!drag){const mode=libAddWaveDragMode(e);canvas.style.cursor=mode==="start"||mode==="end"?"ew-resize":"crosshair";return}e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t)});const finish=e=>{if(!drag)return;e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t),drag=null;const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||0;setLibAddStatus(`Crop range ${start.toFixed(2)}s to ${end.toFixed(2)}s (${Math.max(0,end-start).toFixed(1)}s) selected`)};canvas.addEventListener("pointerup",finish),canvas.addEventListener("pointerleave",()=>{drag||(canvas.style.cursor="crosshair")}),canvas.addEventListener("pointercancel",()=>{drag=null,canvas.style.cursor="crosshair"})}async function uploadLibAddFile(file){if(!file)return;const fd=new FormData;fd.append("file",file),setLibAddStatus("Uploading audio\u2026");try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();suggestLibVoiceId(file.name),loadLibAddAudio(d.id,d.duration,file.name||"Audio"),toast("Audio loaded","success")}catch(e){toast("Load failed: "+e.message,"error"),setLibAddStatus("Load failed")}}const libAddDrop=$("lib-add-drop");libAddDrop.addEventListener("click",()=>$("lib-add-file").click()),libAddDrop.addEventListener("dragover",e=>{e.preventDefault(),libAddDrop.classList.add("drag-over")}),libAddDrop.addEventListener("dragleave",()=>libAddDrop.classList.remove("drag-over")),libAddDrop.addEventListener("drop",e=>{e.preventDefault(),libAddDrop.classList.remove("drag-over"),e.dataTransfer.files.length&&uploadLibAddFile(e.dataTransfer.files[0])}),$("lib-add-file").addEventListener("change",async()=>{$("lib-add-file").files.length&&await uploadLibAddFile($("lib-add-file").files[0]),$("lib-add-file").value=""}),$("lib-add-url-btn").addEventListener("click",()=>{const url=$("lib-add-url").value.trim();if(!url){toast("Enter a YouTube or audio URL","error");return}$("lib-add-url-btn").disabled=!0,setLibAddStatus("Starting download\u2026");const es=new EventSource("/api/download-yt?url="+encodeURIComponent(url));es.onmessage=e=>{const d=JSON.parse(e.data);d.error?(toast("Download failed: "+d.error,"error"),setLibAddStatus(d.error),$("lib-add-url-btn").disabled=!1,es.close()):d.done?(es.close(),$("lib-add-url-btn").disabled=!1,suggestLibVoiceId(url.split("/").pop()||"DownloadedVoice"),loadLibAddAudio(d.id,d.duration,"Downloaded audio"),toast("URL audio loaded","success")):setLibAddStatus(d.msg||"Downloading\u2026")},es.onerror=()=>{es.close(),$("lib-add-url-btn").disabled=!1,setLibAddStatus("Download connection closed")}}),$("lib-add-rec-start").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),libAddState.chunks=[],libAddState.secs=0,$("lib-add-rec-time").textContent="0:00",$("lib-add-rec-start").disabled=!0,$("lib-add-rec-stop").disabled=!1,$("lib-add-monitor-stop").disabled=!0,libAddState.timer=setInterval(()=>{libAddState.secs++,$("lib-add-rec-time").textContent=Math.floor(libAddState.secs/60)+":"+String(libAddState.secs%60).padStart(2,"0")},1e3),libAddState.recorder=new MediaRecorder(libAddState.recordStream),libAddState.recorder.ondataavailable=e=>{e.data.size&&libAddState.chunks.push(e.data)},libAddState.recorder.onstop=async()=>{clearInterval(libAddState.timer),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0;const blob=new Blob(libAddState.chunks,{type:libAddState.recorder.mimeType||"audio/webm"}),ext=(libAddState.recorder.mimeType||"").includes("ogg")?".ogg":".webm";stopLibAddMic(),suggestLibVoiceId("recording"),await uploadLibAddFile(new File([blob],"recording"+ext,{type:blob.type}))},libAddState.recorder.start(100),setLibAddStatus("Recording\u2026")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0}}),$("lib-add-rec-stop").addEventListener("click",()=>{libAddState.recorder&&libAddState.recorder.state!=="inactive"&&libAddState.recorder.stop()}),$("lib-add-auto-trim").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}$("lib-add-auto-trim").disabled=!0;try{const r=await fetch("/api/auto-trim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id})});let d;if(r.ok)d=await r.json();else if(r.status===404||r.status===405)d=await clientAutoTrimBounds(libAddState.id);else{const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}$("lib-add-start").value=Number(d.start).toFixed(2),$("lib-add-end").value=Number(d.end).toFixed(2),drawLibAddWave(),setLibAddStatus(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error"),setLibAddStatus("Auto trim failed")}finally{$("lib-add-auto-trim").disabled=!1}});async function transcribeLibAddCurrent(successMessage="Text recognised",audioId=libAddState.id){if(!audioId)throw new Error("Load audio first");setLibAddStatus("Recognising text...");const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:audioId})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const text=(await r.json()).text||"";return $("lib-add-transcript").value=text,setLibAddStatus(successMessage),text}function openSavedLibraryVoice(voiceId){const openRow=()=>{var _a2;const row=Array.from(document.querySelectorAll(".vl-row")).find(r=>r.dataset.id===voiceId);return row?(row.scrollIntoView({behavior:"smooth",block:"center"}),row.classList.contains("edit-open")||(_a2=row.querySelector(".edit-audio-btn"))==null||_a2.click(),!0):!1};openRow()||setTimeout(openRow,150)}async function applyLibAddCrop(){if(!libAddState.id){toast("Load audio first","error");return}const start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||libAddState.duration),duration=end-start;if(end<=start+.1){toast("Crop range is too short","error"),setLibAddStatus("Crop range is too short");return}(duration<3||duration>20)&&toast("Best clone references are 3-20 seconds; cropping anyway.","error"),["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!0)}),setLibAddStatus(`Cropping ${start.toFixed(2)}s to ${end.toFixed(2)}s...`);try{const r=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start,end})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();loadLibAddAudio(d.id,d.duration,"Cropped audio"),toast("Crop applied","success");try{await transcribeLibAddCurrent("Cropped audio loaded and text recognised",d.id)}catch(e){toast("Crop applied, but recognition failed: "+e.message,"error"),setLibAddStatus("Cropped audio loaded; recognition failed")}}catch(e){toast("Crop failed: "+e.message,"error"),setLibAddStatus("Crop failed")}finally{["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!1)})}}$("lib-add-save-crop").addEventListener("click",applyLibAddCrop),$("lib-add-save-crop-bottom").addEventListener("click",applyLibAddCrop),["lib-add-start","lib-add-end"].forEach(id=>$(id).addEventListener("input",drawLibAddWave)),$("lib-add-play").addEventListener("click",()=>{if(!libAddState.id)return;libAddState.audio&&libAddState.audio.pause(),libAddState.audio=new Audio("/api/audio/"+libAddState.id);const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||libAddState.duration;libAddState.audio.currentTime=start,libAddState.audio.ontimeupdate=()=>{libAddState.audio.currentTime>=end&&libAddState.audio.pause()},libAddState.audio.play()}),$("lib-add-recognize").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}try{await transcribeLibAddCurrent("Text recognised")}catch(e){toast("Recognition failed: "+e.message,"error"),setLibAddStatus("Recognition failed")}}),$("lib-add-save").addEventListener("click",async()=>{var _a2,_b2;if(!libAddState.id){toast("Load audio first","error");return}const voiceId=$("lib-add-voice-id").value.trim()||`${$("lib-add-lang").value}_${$("lib-add-gender").value}_NewVoice`;if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}setLibAddStatus("Saving voice..."),$("lib-add-save").disabled=!0;try{const pr=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start:parseFloat($("lib-add-start").value)||0,end:parseFloat($("lib-add-end").value)||libAddState.duration})});if(!pr.ok){const e=await pr.json().catch(()=>({}));throw new Error(e.detail||pr.statusText)}const p=await pr.json();let transcript=$("lib-add-transcript").value.trim();if(!transcript&&(transcript=await transcribeLibAddCurrent("Final clip recognised; saving voice...",p.id),!transcript.trim()))throw new Error("Recognition returned no transcript; add text or try recognising again.");const sr=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:p.id,voice_id:voiceId,transcript})});if(!sr.ok){const e=await sr.json().catch(()=>({}));throw new Error(e.detail||sr.statusText)}if((_a2=libAddState.pendingSource)!=null&&_a2.imageUrl)try{await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,image_url:libAddState.pendingSource.imageUrl})})}catch{}libAddState.pendingSource=null,setLibAddSourcePreview({}),(_b2=$("lib-add-panel"))==null||_b2.classList.remove("open"),toast("Voice saved: "+voiceId,"success"),setLibAddStatus("Voice saved"),await loadVoiceLibrary2(),openSavedLibraryVoice(voiceId)}catch(e){toast("Save failed: "+e.message,"error"),setLibAddStatus("Save failed")}finally{$("lib-add-save").disabled=!1}}),$("show-disabled-cb").addEventListener("change",()=>{$("disabled-info").style.display=$("show-disabled-cb").checked?"":"none",renderVoiceList2()}),["library-filter-lang","library-filter-sex","library-filter-type","library-filter-rating"].forEach(id=>{var _a2;(_a2=$(id))==null||_a2.addEventListener("change",()=>{readLibraryFilters(),renderVoiceList2()})}),(_e=$("library-filter-text"))==null||_e.addEventListener("input",debounce(()=>{readLibraryFilters(),renderVoiceList2()},180)),(_f=$("library-filter-tag"))==null||_f.addEventListener("change",function(){window._voiceTagFilter=this.value||null,renderVoiceList2()}),(_g=$("library-filter-group"))==null||_g.addEventListener("change",function(){window._voiceGroupFilter=this.value||null,renderVoiceList2()}),(_h=$("library-clear-filters"))==null||_h.addEventListener("click",()=>{window._voiceTagFilter=null,window._voiceGroupFilter=null;const tagSel=$("library-filter-tag"),groupSel=$("library-filter-group");tagSel&&(tagSel.value=""),groupSel&&(groupSel.value=""),clearLibraryFilters()}),(_i=$("library-tts-backend-select"))==null||_i.addEventListener("change",()=>{var _a2;status("Library TTS engine: "+(((_a2=backendById(libraryTtsBackend()))==null?void 0:_a2.label)||libraryTtsBackend()))});function renderVoiceList2(){var _a2,_b2,_c2,_d2,_e2,_f2;const showDisabled=$("show-disabled-cb").checked,list=$("voice-list");list.innerHTML="",renderVoiceGroupsBar(),populateLibraryFilters(),readLibraryFilters();const _gtBtn=$("voice-group-tag-btn");if(_gtBtn){_gtBtn.classList.toggle("active",!!window._voiceGroupByTag);const ic=_gtBtn.querySelector(".mdi");ic&&(ic.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline")}const _tvBtn=$("voice-table-view-btn");if(_tvBtn){_tvBtn.classList.toggle("active",!!window._voiceTableView),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",!!window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode))}const cat=window._voiceSidebarCat||"all",enabledOk=v=>showDisabled||v.enabled!==!1;let filtered=_voices2.filter(v=>cat==="cloned"?enabledOk(v)&&v.has_ref&&v.origin!=="designed":cat==="designed"?enabledOk(v)&&(v.origin==="designed"||!v.has_ref):cat==="favorites"?enabledOk(v)&&(v.rating||0)>=4:cat==="hidden"?v.enabled===!1:enabledOk(v));window._voiceGroupFilter&&(filtered=filtered.filter(v=>(v.group||"").trim()===window._voiceGroupFilter)),window._voiceTagFilter&&(filtered=filtered.filter(v=>String(v.tag||"").split(",").map(t=>t.trim()).includes(window._voiceTagFilter)));const visibleCount=filtered.length;filtered=filtered.filter(libraryFilterMatch);const filterCount=filtered.length;_libraryIssueFilter&&(filtered=filtered.filter(v=>libraryIssueMatch(v))),$("voice-count").textContent=filtered.length+" / "+_voices2.length+" voices",filtered=filtered.slice().sort((a,b)=>{const av=getSortValue(a,_sortField),bv=getSortValue(b,_sortField);return av<bv?-_sortDir:av>bv?_sortDir:0});const pageSignature=JSON.stringify([cat,showDisabled,window._voiceGroupFilter||"",window._voiceTagFilter||"",_libraryIssueFilter||"",_sortField,_sortDir,!!window._voiceTableView,_libraryFilters]);window._voicePageSignature!==pageSignature&&(window._voicePageSignature=pageSignature,window._voicePage=1);const totalFiltered=filtered.length,pageSize=typeof window.ttvcIsLowMemoryDevice=="function"&&window.ttvcIsLowMemoryDevice()?25:50,page=Math.max(1,Number(window._voicePage)||1),pageEnd=Math.min(totalFiltered,page*pageSize),pageItems=filtered.slice(0,pageEnd);if(updateLibraryInsights(),_libraryIssueFilter){const note=document.createElement("div");note.className="library-filter-note",note.innerHTML=`<span><strong>${escHtml(filtered.length)} / ${escHtml(filterCount)}</strong> ${escHtml(libraryIssueLabel())}: ${escHtml(describeIssueVoices())}</span><button class="btn-secondary" type="button">Clear filter</button>`,note.querySelector("button").addEventListener("click",()=>setLibraryIssueFilter("")),list.appendChild(note)}if(!filtered.length){if(_voices2.length===0){const emptyEl=document.createElement("div");emptyEl.className="voices-empty-state",emptyEl.innerHTML=`
<div class="voices-empty-cases">
<div class="voices-empty-case">
<div class="voices-empty-case-icon"><span class="mdi mdi-folder-outline"></span></div>
<h4>Wrong folder path?</h4>
<p>Set the path where your <code>.wav</code> voice files live <em>inside the container</em>.</p>
<div class="voices-folder-row">
<input id="voices-empty-scan-dir" type="text" class="voices-folder-input" placeholder="/voices">
<button class="btn-secondary" id="voices-empty-browse-btn" title="Browse container filesystem"><span class="mdi mdi-folder-outline"></span> Browse</button>
<button class="btn-primary" id="voices-empty-save-btn">Set &amp; reload</button>
</div>
<div class="vef-dir-browser" id="vef-dir-browser" hidden>
<div class="vef-breadcrumb" id="vef-breadcrumb"></div>
<div class="vef-dir-list" id="vef-dir-list"><span class="vef-loading">Loading\u2026</span></div>
<div class="vef-browser-actions">
<span class="vef-selected-path note" id="vef-selected-path"></span>
<button class="btn-primary" id="vef-select-btn">Use this folder</button>
</div>
</div>
<p class="voices-empty-hint">Map a host folder via docker-compose:<br>
<code>- /your/host/path:/voices:rw</code><br>
or set <code>VOICE_HOST_DIR=/your/host/path</code> in the stack env.</p>
</div>
<div class="voices-empty-divider">or</div>
<div class="voices-empty-case">
<div class="voices-empty-case-icon"><span class="mdi mdi-microphone-variant"></span></div>
<h4>Folder is empty?</h4>
<p>Create your first voice from a recording or download ready-made voices.</p>
<div class="voices-empty-actions">
<button class="btn-primary voices-empty-action-btn" id="voices-goto-clone"><span class="mdi mdi-lightning-bolt"></span> Clone a Voice</button>
<button class="btn-secondary voices-empty-action-btn" id="voices-goto-studio"><span class="mdi mdi-earth"></span> Get Voices Online</button>
</div>
</div>
</div>
`,list.appendChild(emptyEl),fetch("/api/settings").then(r=>r.json()).then(s=>{const inp=document.getElementById("voices-empty-scan-dir");inp&&(inp.value=s.voices_scan_dir||"/voices")}).catch(()=>{});let _vefCurrentPath="/";const vefBrowser=document.getElementById("vef-dir-browser"),vefDirList=document.getElementById("vef-dir-list"),vefCrumb=document.getElementById("vef-breadcrumb"),vefSelPath=document.getElementById("vef-selected-path");async function vefNavigate(path){_vefCurrentPath=path,vefDirList.innerHTML='<span class="vef-loading">Loading\u2026</span>',vefSelPath&&(vefSelPath.textContent=path);try{const data=await fetch("/api/browse-dirs?path="+encodeURIComponent(path)).then(r=>r.json()),parts=data.path.split("/").filter(Boolean),crumbs=[{label:"/",path:"/"}];parts.forEach((p,i)=>crumbs.push({label:p,path:"/"+parts.slice(0,i+1).join("/")})),vefCrumb.innerHTML=crumbs.map((c,i)=>i<crumbs.length-1?`<button class="vef-crumb-btn" data-path="${escHtml(c.path)}">${escHtml(c.label)}</button><span class="vef-sep">/</span>`:`<span class="vef-crumb-cur">${escHtml(c.label)}</span>`).join(""),vefCrumb.querySelectorAll(".vef-crumb-btn").forEach(b=>b.addEventListener("click",()=>vefNavigate(b.dataset.path))),data.dirs.length?(vefDirList.innerHTML=data.dirs.map(d=>`<button class="vef-dir-item" data-path="${escHtml(data.path==="/"?"/"+d:data.path+"/"+d)}"><span class="mdi mdi-folder-outline"></span> ${escHtml(d)}</button>`).join(""),vefDirList.querySelectorAll(".vef-dir-item").forEach(b=>b.addEventListener("click",()=>vefNavigate(b.dataset.path)))):vefDirList.innerHTML='<span class="vef-empty">No subdirectories here.</span>',vefSelPath&&(vefSelPath.textContent=data.path),_vefCurrentPath=data.path}catch(e){vefDirList.innerHTML=`<span class="vef-error">Error: ${escHtml(e.message)}</span>`}}(_b2=document.getElementById("voices-empty-browse-btn"))==null||_b2.addEventListener("click",()=>{var _a3,_b3;const open=vefBrowser.hidden;if(vefBrowser.hidden=!open,open){const cur=((_b3=(_a3=document.getElementById("voices-empty-scan-dir"))==null?void 0:_a3.value)==null?void 0:_b3.trim())||"/voices";vefNavigate(cur)}}),(_c2=document.getElementById("vef-select-btn"))==null||_c2.addEventListener("click",()=>{const inp=document.getElementById("voices-empty-scan-dir");inp&&(inp.value=_vefCurrentPath),vefBrowser&&(vefBrowser.hidden=!0)}),(_d2=document.getElementById("voices-empty-save-btn"))==null||_d2.addEventListener("click",async()=>{var _a3;const inp=document.getElementById("voices-empty-scan-dir"),dir=(_a3=inp==null?void 0:inp.value)==null?void 0:_a3.trim();if(!dir)return;const btn=document.getElementById("voices-empty-save-btn");btn.disabled=!0,btn.textContent="Saving\u2026";try{const s=await fetch("/api/settings").then(r2=>r2.json());s.voices_scan_dir=dir;const r=await fetch("/api/settings",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(s)});if(!r.ok)throw new Error((await r.json().catch(()=>({}))).detail||r.statusText);toast("Folder saved \u2014 reloading voices\u2026","success"),_libraryLoadPromise2=null,await loadVoiceLibrary2()}catch(e){toast("Failed: "+e.message,"error"),btn.disabled=!1,btn.textContent="Set & reload"}}),(_e2=document.getElementById("voices-goto-clone"))==null||_e2.addEventListener("click",()=>navTo("s-clone")),(_f2=document.getElementById("voices-goto-studio"))==null||_f2.addEventListener("click",()=>navTo("s-studio"))}else list.innerHTML+='<div style="padding:14px;color:var(--subtext);font-size:14px">No voices match the current filters.</div>';return}if(window._voiceGroupByTag)renderVoicesByTag(list,filtered);else{const frag=document.createDocumentFragment();if(pageItems.forEach(v=>frag.appendChild(makeVoiceRow(v))),list.appendChild(frag),pageEnd<totalFiltered){const more=document.createElement("button");more.type="button",more.className="btn-secondary vl-load-more",more.style.margin="12px 0",more.textContent=`Show more voices (${pageEnd} / ${totalFiltered})`,more.addEventListener("click",()=>{window._voicePage=page+1,renderVoiceList2()}),list.appendChild(more)}}if(_pendingSelectId2){const wrap=list.querySelector(`[data-id="${CSS.escape(_pendingSelectId2)}"]`);wrap&&(_pendingSelectId2=null,selectVoice(wrap))}syncSortHeaders(),typeof window.ttvcObserveImages=="function"&&window.ttvcObserveImages(list)}function renderVoicesByTag(list,filtered){const UNTAG="\u2014 Untagged \u2014",groups=new Map;filtered.forEach(v=>{const tags=String(v.tag||"").split(",").map(t=>t.trim()).filter(Boolean);(tags.length?tags:[UNTAG]).forEach(t=>{groups.has(t)||groups.set(t,[]),groups.get(t).push(v)})});let collapsed={};try{collapsed=JSON.parse(localStorage.getItem("vl-tag-collapsed")||"{}")}catch{}const names=[...groups.keys()].sort((a,b)=>a===UNTAG?1:b===UNTAG?-1:a.toLowerCase().localeCompare(b.toLowerCase())),outer=document.createDocumentFragment();names.forEach(tag=>{const voices=groups.get(tag),isCol=!!collapsed[tag],sec=document.createElement("div");sec.className="vl-tag-group";const head=document.createElement("button");head.className="vl-tag-head"+(isCol?" collapsed":""),head.type="button",head.innerHTML=`<span class="mdi mdi-${isCol?"chevron-right":"chevron-down"} vl-tag-caret"></span><span class="mdi mdi-folder${isCol?"":"-open"}-outline"></span><span class="vl-tag-name">${escHtml(tag)}</span><span class="vl-tag-count">${voices.length}</span>`;const body=document.createElement("div");body.className="vl-tag-body",isCol&&(body.style.display="none");const rows=document.createDocumentFragment();voices.forEach(v=>rows.appendChild(makeVoiceRow(v))),body.appendChild(rows),head.addEventListener("click",()=>{const nowCol=body.style.display!=="none";body.style.display=nowCol?"none":"",head.classList.toggle("collapsed",nowCol),head.querySelector(".vl-tag-caret").className="mdi mdi-"+(nowCol?"chevron-right":"chevron-down")+" vl-tag-caret",head.querySelector(".mdi:nth-child(2)").className="mdi mdi-folder"+(nowCol?"":"-open")+"-outline";let c={};try{c=JSON.parse(localStorage.getItem("vl-tag-collapsed")||"{}")}catch{}c[tag]=nowCol,localStorage.setItem("vl-tag-collapsed",JSON.stringify(c))}),sec.appendChild(head),sec.appendChild(body),outer.appendChild(sec)}),list.appendChild(outer)}async function decodeVoiceAudio(v){const resp=await fetch(voiceFileUrl2(v),{cache:"no-store"});if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const data=await resp.arrayBuffer();return new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(data.slice(0))}function drawOptimizerWave(canvas,buffer,start=0,end=buffer.duration){const dpr=window.devicePixelRatio||1,width=Math.max(1,canvas.clientWidth),height=Math.max(1,canvas.clientHeight);canvas.width=Math.round(width*dpr),canvas.height=Math.round(height*dpr);const ctx=canvas.getContext("2d");ctx.setTransform(dpr,0,0,dpr,0,0),ctx.clearRect(0,0,width,height),ctx.fillStyle=getComputedStyle(document.documentElement).getPropertyValue("--bg")||"#111",ctx.fillRect(0,0,width,height);const data=buffer.getChannelData(0),step=Math.max(1,Math.floor(data.length/width)),mid=height/2;ctx.strokeStyle="#89b4fa",ctx.lineWidth=1,ctx.beginPath();for(let x=0;x<width;x++){let min=1,max=-1;const base=x*step;for(let i=0;i<step&&base+i<data.length;i++){const v=data[base+i];v<min&&(min=v),v>max&&(max=v)}ctx.moveTo(x,mid+min*mid*.92),ctx.lineTo(x,mid+max*mid*.92)}ctx.stroke();const sx=Math.max(0,Math.min(width,start/buffer.duration*width)),ex=Math.max(sx,Math.min(width,end/buffer.duration*width));ctx.fillStyle="rgba(166,227,161,.18)",ctx.fillRect(sx,0,ex-sx,height),ctx.strokeStyle="#a6e3a1",ctx.lineWidth=2,ctx.strokeRect(sx+.5,.5,Math.max(1,ex-sx-1),height-1);const selectedSec=Math.max(0,end-start);if(ex-sx>54&&selectedSec>0){const label=`${selectedSec.toFixed(1)}s`;ctx.font="12px ui-monospace, Menlo, Consolas, monospace";const textW=ctx.measureText(label).width+14,tx=Math.max(sx+6,Math.min(ex-textW-6,sx+(ex-sx-textW)/2));ctx.fillStyle="rgba(30,30,46,.78)",ctx.fillRect(tx,6,textW,22),ctx.fillStyle="#ffffff",ctx.fillText(label,tx+7,21)}const handleW=12;ctx.fillStyle="#3d5ce8",ctx.strokeStyle="#ffffff",[sx,ex].forEach((x,idx)=>{const hx=Math.max(0,Math.min(width-handleW,x-handleW/2));ctx.fillRect(hx,0,handleW,height),ctx.strokeRect(hx+.5,.5,Math.max(1,handleW-1),height-1),ctx.fillStyle="#ffffff",ctx.fillRect(hx+3,Math.max(12,height/2-11),2,22),ctx.fillRect(hx+7,Math.max(12,height/2-11),2,22),ctx.fillStyle="#3d5ce8"})}function makeVoiceRow(v){var _a2;const wrap=document.createElement("div");wrap.className="vl-row"+(v.enabled===!1?" vr-disabled":""),wrap.dataset.id=v.id;const color=avatarColor(v.id);wrap.dataset.color=color,wrap.dataset.hasRef=v.has_ref?"true":"false",wrap.dataset.dbfs=fmtDbfs2(v),wrap.dataset.lang=v.lang||v.id.split("_")[0].toUpperCase();const langCode=v.lang||v.id.split("_")[0].toUpperCase(),_idParts=String(v.id).split("_"),dispName=v.name&&String(v.name).trim()?String(v.name).trim():_idParts.length>1?_idParts[_idParts.length-1]:v.id,langOpts=FLAG_OPTIONS[langCode]||[],flagOpts=langOpts.length>1?langOpts:ALL_FLAGS,currentFlag=v.flag||LANG_FLAG_DEFAULT[langCode]||"",flagEmoji=currentFlag?cc2flag(currentFlag):'<span class="mdi mdi-earth"></span>',flagCode=currentFlag?ccDisplay(currentFlag):"?";wrap.dataset.flagEmoji=flagEmoji,wrap.dataset.flagCc=currentFlag||"",wrap.dataset.rating=v.rating||0,wrap.dataset.hasPicture=v.has_picture?"true":"false";const fileType=voiceFileType(v),dbfs=fmtDbfs2(v),dbTitle=v.loudness?`avg ${dbfs} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",benchText=fmtBenchmark2(v),benchTitle=benchmarkTitle2(v),benchCls=benchmarkClass2(v),genderMap={F:"\u2640",M:"\u2642",N:"\u26A5","":"?"},genderMapLetter={F:"F",M:"M",N:"D","":"?"},genderLabel={F:"Female",M:"Male",N:"Diverse","":"\u2014"},genderClass={F:"g-f",M:"g-m",N:"g-n","":"g-n"},gender=v.gender||"",starsHtml=[1,2,3,4,5].map(i=>`<span class="star${i<=(v.rating||0)?" on":""}" data-val="${i}"><span class="mdi mdi-star"></span></span>`).join(""),picSrc=v.has_picture?`/api/voice/picture/${encodeURIComponent(v.id)}`:null,pickerHtml=flagOpts.length>1?flagOpts.map(([cc,label])=>`<span class="flag-opt${cc===currentFlag?" active":""}" data-cc="${cc}" title="${label}">${cc2flag(cc)}<span class="fo-code">${ccDisplay(cc)}</span></span>`).join(""):'<span style="font-size:12px;color:var(--subtext);padding:3px 6px">No regional variants</span>',isClone=!!v.has_ref,initial=v.id[0]?v.id[0].toUpperCase():"?",flagIconHtml=currentFlag?`<span class="fi fi-${currentFlag.toLowerCase()}" role="img" aria-label="${langCode}"></span>`:null,silHtml=`<span class="mdi ${gender==="M"?"mdi-face-man":gender==="F"?"mdi-face-woman":"mdi-account"}" style="color:#fff;font-size:20px;display:flex;align-items:center;justify-content:center;width:100%;height:100%;background:${color}"></span>`,iconHtml=window.voiceAvatarIcon?window.voiceAvatarIcon(v.avatar,34):null,avatarClass=picSrc?"vl-avatar vl-avatar-photo":iconHtml?"vl-avatar vl-avatar-icon":"vl-avatar vl-avatar-sil",avatarStyle=picSrc||iconHtml?"":`style="background:${color}"`,avatarContent=picSrc?`<img src="${picSrc}" alt="" class="vl-avatar-img" loading="lazy" decoding="async">`:iconHtml||silHtml;wrap.innerHTML=`
<div class="vl-compact">
<div class="${avatarClass}" ${avatarStyle}>${avatarContent}</div>
<div class="vl-info">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit vl-name-input" data-field="name" value="${escHtml(dispName)}" title="Display Name" placeholder="Name">`:`<span class="vl-name" title="${escHtml(v.id)}">${escHtml(dispName)}</span>`}
<div class="vl-meta">
<span class="vl-lang-label" title="${escHtml(currentFlag||langCode)}">${flagEmoji}</span>
${gender?`<span class="vl-gender-letter ${genderClass[gender]||"g-n"}" title="${genderLabel[gender]||""}">${genderMapLetter[gender]||"?"}</span>`:""}
${benchText!=="-"?`<span class="vl-bench-chip ${benchCls}" title="${escHtml(benchTitle)}"><span class="mdi mdi-timer-outline"></span> ${escHtml(benchText.split(" ")[0])}</span>`:""}
<div class="vr-rating">${starsHtml}</div>
<span class="vl-type-label ${isClone?"vl-type-clone":"vl-type-design"}">${isClone?"Clone":"Design"}</span>
</div>
</div>
<div class="vr-play-group">
<div class="vr-play vr-play-original"><button class="play-btn-orig" title="Play original recording" aria-label="Play original recording"><span class="mdi mdi-play"></span></button></div>
<div class="vr-play vr-play-synth"><button class="play-btn-synth" title="Generate and play TTS preview" aria-label="Generate and play TTS preview"><span class="mdi mdi-play"></span></button></div>
</div>
<div class="vl-tbl-lang" title="${escHtml(currentFlag||langCode)}">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit" data-field="lang" value="${escHtml(langCode)}" style="width:100%">`:`${flagEmoji} ${escHtml(langCode)}`}
</div>
<div class="vl-tbl-gender" title="${escHtml(genderLabel[gender]||"")}">
${window._voiceTableEditMode?`<select class="vl-inline-edit" data-field="gender" style="width:100%"><option value="N" ${gender==="N"?"selected":""}>N</option><option value="M" ${gender==="M"?"selected":""}>M</option><option value="F" ${gender==="F"?"selected":""}>F</option></select>`:`${genderMap[gender]||"?"} ${escHtml(genderLabel[gender]||"\u2014")}`}
</div>
<div class="vl-tbl-factor ${benchCls}" title="${escHtml(benchTitle)}">${escHtml(fmtFactor(v))}</div>
<div class="vl-tbl-wpm" title="${escHtml(voiceWpm(v)!=null?`${voiceWpm(v)} words per minute \u2014 how fast this voice speaks. 130\u2013180 wpm is natural for long listening.`:"Benchmark this voice to see speaking rate")}">${escHtml(fmtWpm(v))}</div>
<div class="vl-tbl-dbfs" title="${dbTitle}">${dbfs} dB</div>
<div class="vl-tbl-type"><span class="vl-type-label ${isClone?"vl-type-clone":"vl-type-design"}">${isClone?"Clone":"Design"}</span></div>
<div class="vl-tbl-rating">
${window._voiceTableEditMode?`<input type="number" min="0" max="5" class="vl-inline-edit" data-field="rating" value="${v.rating||0}" style="width:40px">`:starsHtml}
</div>
<div class="vl-tbl-source" title="Click to edit source">${escHtml(_displaySource(v)||"-")}</div>
<div class="vl-tbl-seed">${v.seed!=null?"#"+v.seed:"Auto"}</div>
<div class="vl-tbl-note" title="${escHtml(v.note||"")}">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit" data-field="note" value="${escHtml(v.note||"")}" style="width:100%">`:escHtml(v.note||"")}
</div>
<div class="vl-tbl-tag" title="${escHtml(v.tag||"")}">
${window._voiceTableEditMode?`<input type="text" class="vl-inline-edit" data-field="tag" value="${escHtml(v.tag||"")}" style="width:100%">`:escHtml(v.tag||"")}
</div>
<div class="vl-tbl-active">
${window._voiceTableEditMode?`<input type="checkbox" class="vl-inline-edit" data-field="enabled" ${v.enabled!==!1?"checked":""} style="width:16px;height:16px">`:`<span class="mdi ${v.enabled!==!1?"mdi-check-circle":"mdi-close-circle"}" style="color:${v.enabled!==!1?"var(--green)":"var(--subtext)"}"></span>`}
</div>
</div>
<div class="vr-main-row">
<div class="vr-photo" title="Click to upload photo">
${picSrc?`<img src="${picSrc}" alt="" loading="lazy" decoding="async">`:'<div class="ph-icon"><span class="mdi mdi-account-outline"></span></div>'}
<input type="file" class="photo-input" accept="image/*">
</div>
<div class="vr-identity">
<div class="vr-flag">
<span class="flag-emoji">${flagEmoji}</span>
<span class="flag-code">${flagCode}</span>
<div class="flag-picker">${pickerHtml}</div>
</div>
<div class="vr-gender">
<span class="gender-badge ${genderClass[gender]||"g-n"}" title="Click to cycle">
<span class="gender-sym">${genderMap[gender]||"?"}</span>
<span class="gender-txt">${genderLabel[gender]||"\u2014"}</span>
</span>
</div>
<div class="vr-name">
<span class="vr-name-text" title="${escHtml(v.id)}">${escHtml(v.id)}</span>
<span class="rename-confirm">
<input class="vr-name-input" value="${escHtml(v.id)}" spellcheck="false">
<button class="icon-btn rename-ok" title="Save"><span class="mdi mdi-check"></span></button>
<button class="icon-btn rename-cancel" title="Cancel"><span class="mdi mdi-close"></span></button>
</span>
</div>
</div>
<div class="vr-type" title="${escHtml(fileType)}">${escHtml(fileType.toUpperCase())}</div>
<div class="vr-length" title="${escHtml(String((_a2=v.duration)!=null?_a2:""))}">${fmtDuration(v.duration)}</div>
<div class="vr-db" title="${escHtml(dbTitle)}">
<span class="vr-db-value">${escHtml(dbfs)}</span>
<button class="normalize-voice-btn" title="Normalize this voice">\xB1</button>
</div>
<div class="vr-bench ${benchCls}" title="${escHtml(benchTitle)}">
<span class="vr-bench-value">${escHtml(benchText)}</span>
<button class="btn-secondary benchmark-one-btn" title="Benchmark this voice">\u23F1</button>
</div>
<div class="vr-rating">${starsHtml}</div>
<div class="vr-edit">
<button class="edit-audio-btn" title="Edit audio"><span class="mdi mdi-pencil-outline"></span></button>
</div>
<div class="vr-play-group">
<div class="vr-play vr-play-original">
<button title="Play / pause original recording"><span class="mdi mdi-play"></span></button>
</div>
<div class="vr-play vr-play-synth">
<button title="Generate and play synthesized sample"><span class="mdi mdi-play"></span></button>
</div>
</div>
</div>
<div class="vr-detail-row">
<div class="vr-ref">
<label>Reference</label>
<div class="vr-inline">
<input type="text" value="${escHtml(v.transcript||"")}" placeholder="No reference text" title="${escHtml(v.transcript||"")}">
<button class="ref-transcribe-btn" title="Recognise reference text" style="${v.transcript?"display:none":""}"><span class="mdi mdi-message-text-outline"></span></button>
</div>
</div>
<div class="vr-note">
<label>Note</label>
<div class="vr-inline">
<input type="text" placeholder="Add note..." value="${escHtml(v.note||"")}">
</div>
</div>
<div class="vr-source">
<label>Source</label>
<div class="vr-inline">
<input type="text" placeholder="e.g. fish-audio, cloned, designed\u2026" value="${escHtml(v.origin||"")}">
</div>
</div>
<div class="vr-detail-active">
<label>Active</label>
<div class="vr-active-tools">
<div class="vr-toggle">
<label class="toggle" title="${v.enabled===!1?"Disabled - click to enable":"Enabled - click to disable"}">
<input type="checkbox" ${v.enabled!==!1?"checked":""}>
<span class="t-slider"></span>
</label>
</div>
</div>
</div>
<div class="vr-detail-delete">
<button class="delete-btn" title="Delete voice permanently">Delete voice</button>
<div class="delete-confirm" role="group" aria-label="Confirm delete voice">
<strong>Delete?</strong>
<span title="${escHtml(v.id)}">${escHtml(v.id)}</span>
<button class="btn-secondary delete-confirm-cancel" type="button">Cancel</button>
<button class="delete-confirm-go" type="button">Delete</button>
</div>
</div>
</div>
<div class="vr-optimizer">
<div class="optimizer-grid">
<div class="opt-group opt-trim-panel">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Reference audio &middot; crop</span></div>
<div class="opt-group-subtitle">Crop the saved WAV to 3&ndash;20&nbsp;s of clean speech.</div>
<div class="opt-group-body">
<canvas class="opt-wave"></canvas>
<div class="opt-controls">
<div class="opt-field"><label>Start</label><input class="opt-start" type="number" step="0.01" value="0"></div>
<div class="opt-field"><label>End</label><input class="opt-end" type="number" step="0.01" value="0"></div>
<button class="btn-secondary opt-auto-trim">Auto trim</button>
<button class="btn-secondary opt-play">Play crop</button>
<button class="btn-secondary opt-undo">Undo crop</button>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-crop">Save crop</button>
</div>
</div>
</div>
<div class="opt-group opt-text-panel open">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Reference transcript</span></div>
<div class="opt-group-subtitle">The spoken text that matches this voice recording.</div>
<div class="opt-group-body">
<textarea class="opt-transcript" placeholder="Reference text">${escHtml(v.transcript||"")}</textarea>
<div class="opt-controls">
<button class="btn-secondary opt-recognize">Re-recognise text</button>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-text">Save text</button>
</div>
</div>
</div>
<div class="opt-group opt-compare-panel">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Voice match</span></div>
<div class="opt-group-subtitle">Compare the saved WAV with a fresh TTS synthesis.</div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field wide"><label>Comparison backend</label><select class="opt-compare-backend">${styleBackendOptions("voice_clone")}</select></div>
<button class="btn-secondary opt-play-reference">Play WAV file</button>
<button class="btn-primary opt-synth-reference">Synthesize reference text</button>
</div>
<p class="opt-group-note">Compare the saved reference WAV with a fresh synthesis of the same reference text. Restart TTS first after editing a voice, otherwise the backend may still use a cached version.</p>
<div class="opt-compare-grid">
<div class="opt-compare-card">
<strong>WAV file</strong>
<audio class="opt-compare-ref-audio" controls></audio>
</div>
<div class="opt-compare-card">
<strong>Synthesized reference text</strong>
<audio class="opt-compare-synth-audio" controls></audio>
</div>
</div>
</div>
</div>
${isClone?"":`<div class="opt-group opt-style-panel">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Style variation</span></div>
<div class="opt-group-subtitle">Create a styled variant and save it as a new voice (CustomVoice only).</div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field wide"><label>Style instruction</label><input class="opt-style-instruct" type="text" placeholder="cheerful, calm, excited"></div>
<div class="opt-field wide"><label>Style backend</label><select class="opt-style-backend">${styleBackendOptions("customvoice",!0)}</select></div>
<div class="opt-field wide"><label>New voice ID</label><input class="opt-style-voice-id" type="text" placeholder="DE_F_Amala_happy"></div>
</div>
<p class="opt-group-note">Preview first. Saving creates a new active WAV voice from the current reference text. Same-voice style only works when the selected backend knows this voice and honors <code>instruct</code>; CustomVoice is style-aware; Base/Streaming are fastest but often ignore style.</p>
<div class="backend-help opt-style-backend-help"></div>
<div class="opt-style-preview-box">
<div class="opt-controls">
<button class="btn-secondary opt-preview-style">Preview style</button>
</div>
<audio class="opt-style-audio" controls></audio>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-style">Save style variation</button>
</div>
</div>
</div>`}
<div class="opt-group opt-maintenance open">
<div class="opt-group-title"><span class="opt-chevron"></span><span class="opt-title-text">Loudness</span></div>
<div class="opt-group-subtitle">Normalize the volume of the reference audio file.</div>
<div class="opt-group-body">
<div class="opt-controls">
<div class="opt-field"><label>Target dBFS</label><input class="opt-target-db" type="number" step="0.5" value="-20"></div>
<button class="btn-secondary opt-db-minus">-</button>
<button class="btn-secondary opt-db-plus">+</button>
<button class="btn-secondary opt-db-auto">Auto</button>
<button class="btn-secondary opt-restart-tts" title="Restarts the WAV-loading TTS backends (Voice Clone + Streaming) so they rescan the active_voices folder">Restart WAV engines</button>
<button class="btn-secondary opt-rebenchmark">Rebenchmark this one</button>
<span class="opt-restart-note" hidden>Restart TTS before benchmarking.</span>
</div>
<div class="opt-group-footer">
<button class="btn-primary opt-save-volume">Save volume</button>
</div>
</div>
</div>
<div class="opt-status optimizer-workflow">Click the pencil to load waveform and tools.</div>
</div>
</div>
`;const vrLengthEl=wrap.querySelector(".vr-length");hydrateVoiceDuration(v,vrLengthEl);const photoCell=wrap.querySelector(".vr-photo"),photoInput=wrap.querySelector(".photo-input"),updatePhotoImg=()=>{const ts=Date.now(),imgSrc=`/api/voice/picture/${encodeURIComponent(v.id)}?t=${ts}`,img=document.createElement("img");img.src=imgSrc,img.alt="",photoCell.innerHTML="",photoCell.appendChild(img),photoCell.appendChild(photoInput);const compactAvatar=wrap.querySelector(".vl-avatar");compactAvatar&&(compactAvatar.className="vl-avatar vl-avatar-photo",compactAvatar.style.background="",compactAvatar.innerHTML=`<img src="${imgSrc}" alt="" class="vl-avatar-img">`);const inspectorAvatar=document.querySelector(".inspector-avatar");inspectorAvatar&&wrap.classList.contains("vr-selected")&&(inspectorAvatar.classList.add("insp-avatar-photo"),inspectorAvatar.style.background="",inspectorAvatar.innerHTML=`<img src="${imgSrc}" alt="" class="insp-avatar-img">`),v.has_picture=!0};photoCell.addEventListener("update-photo",updatePhotoImg),photoCell.addEventListener("click",()=>photoInput.click()),photoInput.addEventListener("change",async()=>{if(!photoInput.files.length)return;const fd=new FormData;fd.append("voice_id",v.id),fd.append("file",photoInput.files[0]);try{const r=await fetch("/api/voice/picture",{method:"POST",body:fd});if(!r.ok)throw new Error((await r.json()).detail);updatePhotoImg(),toast("Photo uploaded","success")}catch(e){toast("Photo upload failed: "+e.message,"error")}}),photoCell.addEventListener("dragenter",e=>{e.preventDefault(),photoCell.classList.add("drag-over")}),photoCell.addEventListener("dragover",e=>{e.preventDefault(),photoCell.classList.add("drag-over")}),photoCell.addEventListener("dragleave",()=>photoCell.classList.remove("drag-over")),photoCell.addEventListener("drop",async e=>{if(e.preventDefault(),photoCell.classList.remove("drag-over"),e.dataTransfer.files&&e.dataTransfer.files.length>0){photoInput.files=e.dataTransfer.files,photoInput.dispatchEvent(new Event("change"));return}let url=e.dataTransfer.getData("text/uri-list");if(!url){const html=e.dataTransfer.getData("text/html");if(html){const match=html.match(/src=["'](.*?)["']/);match&&(url=match[1])}}if(url||(url=e.dataTransfer.getData("text/plain")),url&&/^https?:\/\//i.test(url)){status("Downloading picture from URL...");try{const r=await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,image_url:url})});if(!r.ok)throw new Error((await r.json()).detail);updatePhotoImg(),toast("Photo saved from URL","success"),status("Photo saved successfully")}catch(err){toast("Photo URL download failed: "+err.message,"error"),status("Photo URL download failed")}}});const normalizeBtn=wrap.querySelector(".normalize-voice-btn"),dbValue=wrap.querySelector(".vr-db-value"),dbCell=wrap.querySelector(".vr-db");normalizeBtn.addEventListener("click",async()=>{var _a3;const target=libraryTargetDb();if(confirm(`Normalize "${v.id}" to ${target} dBFS?`)){normalizeBtn.disabled=!0,status("Normalizing "+v.id+"\u2026");try{const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:target})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a3=d.duration)!=null?_a3:v.duration,v.file_type=d.file_type||v.file_type,v.path=d.path||v.path,v.needs_tts_restart=!0,markVoiceAudioChanged(v),dbValue.textContent=fmtDbfs2(v),dbCell.title=v.loudness?`avg ${fmtDbfs2(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",vrLengthEl&&(vrLengthEl.textContent=fmtDuration(v.duration)),toast("Normalized: "+v.id,"success"),status(`Normalized ${v.id} to ${target} dBFS. Restart TTS before rebenchmarking.`)}catch(e){toast("Normalize failed: "+e.message,"error"),status("Normalize failed")}finally{normalizeBtn.disabled=!1}}});const flagEmojiEl=wrap.querySelector(".flag-emoji"),flagCodeEl=wrap.querySelector(".flag-code"),flagPicker=wrap.querySelector(".flag-picker");wrap.querySelector(".vr-flag").addEventListener("click",e=>{e.stopPropagation(),document.querySelectorAll(".flag-picker.open").forEach(fp=>{fp!==flagPicker&&fp.classList.remove("open")}),flagPicker.classList.toggle("open")}),flagPicker.querySelectorAll(".flag-opt").forEach(opt=>{opt.addEventListener("click",async e=>{e.stopPropagation();const cc=opt.dataset.cc;flagPicker.classList.remove("open"),flagEmojiEl.textContent=cc2flag(cc),flagCodeEl.textContent=ccDisplay(cc),flagPicker.querySelectorAll(".flag-opt").forEach(o=>o.classList.toggle("active",o.dataset.cc===cc)),v.flag=cc,await saveMeta2(v.id,{flag:cc})})});const gBadge=wrap.querySelector(".gender-badge");gBadge.addEventListener("click",async()=>{const cycle=["F","M","N"];v.gender=cycle[(cycle.indexOf(v.gender||"F")+1)%3],gBadge.innerHTML=`<span class="gender-sym">${genderMap[v.gender]||"?"}</span><span class="gender-txt">${genderLabel[v.gender]||"\u2014"}</span>`,gBadge.className="gender-badge "+genderClass[v.gender],await saveMeta2(v.id,{gender:v.gender})});const nameText=wrap.querySelector(".vr-name-text"),renameConf=wrap.querySelector(".rename-confirm"),nameInput=wrap.querySelector(".vr-name-input"),renameOk=wrap.querySelector(".rename-ok"),renameCancel=wrap.querySelector(".rename-cancel"),startRename=()=>{nameText.style.display="none",renameConf.classList.add("show"),nameInput.focus(),nameInput.select()};nameText.addEventListener("dblclick",startRename);const cancelRename=()=>{nameText.style.display="",renameConf.classList.remove("show"),nameInput.value=v.id};renameCancel.addEventListener("click",cancelRename);const doRename=async()=>{const newId=nameInput.value.trim();if(!newId||newId===v.id){cancelRename();return}if(!/^[A-Za-z0-9_\-\.]+$/.test(newId)){toast("Invalid characters in name","error");return}try{const r=await fetch("/api/voice/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({old_id:v.id,new_id:newId})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();v.id=newId,d.path&&(v.path=d.path),d.file_type&&(v.file_type=d.file_type),nameText.textContent=newId,nameText.title=newId,nameText.style.display="",renameConf.classList.remove("show"),wrap.dataset.id=newId,nameInput.value=newId,toast("Renamed to "+newId,"success")}catch(e){toast("Rename failed: "+e.message,"error")}};renameOk.addEventListener("click",doRename),nameInput.addEventListener("keydown",e=>{e.key==="Enter"&&doRename(),e.key==="Escape"&&cancelRename()}),wrap.querySelector(".vl-compact").addEventListener("click",function(e){e.target.closest("button")||selectVoice(wrap)});const editAudioBtn=wrap.querySelector(".edit-audio-btn"),optPanel=wrap.querySelector(".vr-optimizer"),vrTypeEl=wrap.querySelector(".vr-type"),optCanvas=wrap.querySelector(".opt-wave"),optStart=wrap.querySelector(".opt-start"),optEnd=wrap.querySelector(".opt-end"),optTranscript=wrap.querySelector(".opt-transcript"),optTargetDb=wrap.querySelector(".opt-target-db"),optStyleInstruct=wrap.querySelector(".opt-style-instruct"),optStyleBackend=wrap.querySelector(".opt-style-backend"),optStyleVoiceId=wrap.querySelector(".opt-style-voice-id"),optCompareBackend=wrap.querySelector(".opt-compare-backend"),optPlayReferenceBtn=wrap.querySelector(".opt-play-reference"),optSynthReferenceBtn=wrap.querySelector(".opt-synth-reference"),optCompareRefAudio=wrap.querySelector(".opt-compare-ref-audio"),optCompareSynthAudio=wrap.querySelector(".opt-compare-synth-audio"),optPreviewStyleBtn=wrap.querySelector(".opt-preview-style"),optSaveStyleBtn=wrap.querySelector(".opt-save-style"),optStyleAudio=wrap.querySelector(".opt-style-audio"),optStatus=wrap.querySelector(".opt-status"),optSaveTextBtn=wrap.querySelector(".opt-save-text"),optRestartTtsBtn=wrap.querySelector(".opt-restart-tts"),optRebenchmarkBtn=wrap.querySelector(".opt-rebenchmark"),optRestartNote=wrap.querySelector(".opt-restart-note");let optState={loaded:!1,id:null,duration:0,buffer:null,audio:null,compareSynthUrl:null};const setOptStatus=msg=>{optStatus.textContent=msg,status(msg)},setVoiceRestartState=(required,msg="")=>{v.needs_tts_restart=required,optPanel.classList.toggle("opt-restart-needed",required),optRestartNote.hidden=!required,optRestartNote.textContent=required?"Restart TTS before benchmarking; the backend may still have the old voice cached.":"",optRebenchmarkBtn.title=required?"Restart TTS first, otherwise the benchmark may use a cached voice":"Benchmark this voice",benchmarkOneBtn.title=required?"Restart TTS first, otherwise the benchmark may use a cached voice":"Benchmark this voice",msg&&setOptStatus(msg)},markTtsRestartRequired=msg=>setVoiceRestartState(!0,msg),refreshOptimizerFromVoice=async()=>{var _a3;markVoiceAudioChanged(v),optState.loaded=!1,optState.buffer=null,optState.id=null,await loadOptimizer(),vrLengthEl&&(vrLengthEl.textContent=fmtDuration(v.duration)),vrLengthEl&&(vrLengthEl.title=String((_a3=v.duration)!=null?_a3:"")),dbValue.textContent=fmtDbfs2(v),dbCell.title=v.loudness?`avg ${fmtDbfs2(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:""},saveOptimizerText=async()=>{const transcript=optTranscript.value.trim();setOptStatus("Saving reference text..."),v.transcript=transcript,refInput.value=transcript,refInput.title=transcript,refTranscribeBtn.style.display=transcript?"none":"",await saveMeta2(v.id,{transcript}),markTtsRestartRequired("Reference text saved. Restart TTS before rebenchmarking."),toast("Reference text saved: "+v.id,"success")},redrawOpt=()=>{!optState.buffer||optCanvas.clientWidth<4||drawOptimizerWave(optCanvas,optState.buffer,parseFloat(optStart.value)||0,parseFloat(optEnd.value)||optState.duration)};new ResizeObserver(()=>redrawOpt()).observe(optCanvas);const syncCompareReferenceAudio=()=>{if(!optState.id||!optCompareRefAudio)return;const src="/api/audio/"+optState.id;optCompareRefAudio.src.endsWith(src)||(optCompareRefAudio.src=src,optCompareRefAudio.load());const stopAtEnd=()=>{const end=parseFloat(optEnd.value)||optState.duration;optCompareRefAudio.currentTime>=end&&optCompareRefAudio.pause()};optCompareRefAudio.ontimeupdate=stopAtEnd},optWaveTimeFromEvent=e=>{const rect=optCanvas.getBoundingClientRect(),x=Math.max(0,Math.min(rect.width,e.clientX-rect.left));return optState.duration?x/Math.max(1,rect.width)*optState.duration:0},setOptCropRange=(start,end)=>{start=Math.max(0,Math.min(optState.duration||0,Number(start)||0)),end=Math.max(0,Math.min(optState.duration||0,Number(end)||0)),end<start&&([start,end]=[end,start]),optStart.value=start.toFixed(2),optEnd.value=end.toFixed(2),redrawOpt()},optWaveSelectionPixels=e=>{const rect=optCanvas.getBoundingClientRect(),duration=Math.max(.01,optState.duration||.01),sx=(parseFloat(optStart.value)||0)/duration*rect.width,ex=(parseFloat(optEnd.value)||optState.duration||0)/duration*rect.width;return{x:e.clientX-rect.left,sx,ex}},optWaveDragMode=e=>{const{x,sx,ex}=optWaveSelectionPixels(e),hit=18,nearStart=Math.abs(x-sx)<=hit,nearEnd=Math.abs(x-ex)<=hit;return nearStart&&nearEnd?Math.abs(x-sx)<=Math.abs(x-ex)?"start":"end":nearStart?"start":nearEnd?"end":x>sx&&x<ex?"move":"new"};(()=>{let drag=null;optCanvas.addEventListener("pointerdown",e=>{var _a3;if(!optState.buffer||!optState.duration)return;e.preventDefault(),(_a3=optCanvas.setPointerCapture)==null||_a3.call(optCanvas,e.pointerId);const mode=optWaveDragMode(e),currentStart=parseFloat(optStart.value)||0,currentEnd=parseFloat(optEnd.value)||optState.duration;drag={mode,anchor:optWaveTimeFromEvent(e),start:currentStart,end:currentEnd,length:Math.max(.05,currentEnd-currentStart)},optCanvas.style.cursor=mode==="move"?"grabbing":"ew-resize",mode==="new"&&setOptCropRange(drag.anchor,drag.anchor)}),optCanvas.addEventListener("pointermove",e=>{if(!optState.buffer||!optState.duration)return;if(!drag){const mode=optWaveDragMode(e);optCanvas.style.cursor=mode==="move"?"grab":mode==="start"||mode==="end"?"ew-resize":"crosshair";return}e.preventDefault();const t=optWaveTimeFromEvent(e);if(drag.mode==="start")setOptCropRange(Math.min(t,drag.end-.05),drag.end);else if(drag.mode==="end")setOptCropRange(drag.start,Math.max(t,drag.start+.05));else if(drag.mode==="move"){let start=t-(drag.anchor-drag.start);start=Math.max(0,Math.min((optState.duration||0)-drag.length,start)),setOptCropRange(start,start+drag.length)}else setOptCropRange(drag.anchor,t)});const finish=e=>{var _a3;drag&&((_a3=optCanvas.releasePointerCapture)==null||_a3.call(optCanvas,e.pointerId),drag=null,optCanvas.style.cursor="crosshair")};optCanvas.addEventListener("pointerup",finish),optCanvas.addEventListener("pointercancel",finish),optCanvas.addEventListener("pointerleave",()=>{drag||(optCanvas.style.cursor="crosshair")})})();const loadOptimizer=async()=>{if(optState.loaded)return;setOptStatus("Loading voice optimizer\u2026");const d=await loadLibraryVoiceAudio(v);optState.id=d.id,optState.duration=d.duration,v.duration=d.duration,optState.buffer=await decodeVoiceAudio(v),optState.loaded=!0,optStart.value="0.00",optEnd.value=d.duration.toFixed(2),optEnd.max=d.duration.toFixed(2),optTranscript.value=d.transcript||v.transcript||"",redrawOpt(),syncCompareReferenceAudio(),setVoiceRestartState(!!v.needs_tts_restart),setOptStatus(v.needs_tts_restart?"Optimizer ready. Restart TTS before benchmarking this edit.":"Optimizer ready")};if(wrap._loadOptimizer=loadOptimizer,wrap._redrawOpt=redrawOpt,editAudioBtn.addEventListener("click",async()=>{editAudioBtn.disabled=!0;try{const opening=!wrap.classList.contains("edit-open");document.querySelectorAll(".vl-row.edit-open").forEach(r=>{r!==wrap&&r.classList.remove("edit-open")}),wrap.classList.toggle("edit-open",opening),opening&&(await loadOptimizer(),wrap.scrollIntoView({behavior:"smooth",block:"nearest"}))}catch(e){toast("Edit load failed: "+e.message,"error"),status("Edit load failed")}finally{editAudioBtn.disabled=!1}}),[optStart,optEnd].forEach(inp=>inp.addEventListener("input",()=>{redrawOpt(),syncCompareReferenceAudio()})),optStyleInstruct==null||optStyleInstruct.addEventListener("input",()=>{optStyleVoiceId.value.trim()||(optStyleVoiceId.value=suggestedStyleVoiceId(v.id,optStyleInstruct.value))}),optStyleBackend==null||optStyleBackend.addEventListener("change",()=>updateStyleBackendHelp(wrap)),optCompareBackend.addEventListener("change",()=>{var _a3;return setOptStatus(`Comparison backend: ${((_a3=optCompareBackend.options[optCompareBackend.selectedIndex])==null?void 0:_a3.textContent)||optCompareBackend.value}`)}),optCompareBackend.value===""&&(optCompareBackend.innerHTML=styleBackendOptions("voice_clone"),optCompareBackend.disabled=!availableTtsBackends().length),optStyleBackend&&updateStyleBackendHelp(wrap),wrap.querySelector(".opt-db-minus").addEventListener("click",()=>{optTargetDb.value=(Number(optTargetDb.value||-20)-1).toFixed(1)}),wrap.querySelector(".opt-db-plus").addEventListener("click",()=>{optTargetDb.value=(Number(optTargetDb.value||-20)+1).toFixed(1)}),wrap.querySelector(".opt-db-auto").addEventListener("click",()=>{optTargetDb.value="-20.0"}),wrap.querySelector(".opt-play").addEventListener("click",async()=>{try{await loadOptimizer(),optState.audio&&optState.audio.pause(),optState.audio=new Audio("/api/audio/"+optState.id),optState.audio.currentTime=parseFloat(optStart.value)||0;const end=parseFloat(optEnd.value)||optState.duration;optState.audio.ontimeupdate=()=>{optState.audio.currentTime>=end&&optState.audio.pause()},optState.audio.play()}catch(e){toast("Preview failed: "+e.message,"error")}}),optPlayReferenceBtn.addEventListener("click",async()=>{try{await loadOptimizer(),syncCompareReferenceAudio(),optCompareRefAudio.currentTime=parseFloat(optStart.value)||0,await optCompareRefAudio.play().catch(()=>{}),setOptStatus("Playing reference WAV selection for comparison.")}catch(e){toast("Reference playback failed: "+e.message,"error")}}),optSynthReferenceBtn.addEventListener("click",async()=>{const text=optTranscript.value.trim();if(!text){toast("Enter reference text first","error"),optTranscript.focus();return}if(v.needs_tts_restart){if(!confirm("This voice is still marked as needing a TTS restart. If you already restarted TTS manually, clear the restart flags and synthesize now?")){setOptStatus("Restart TTS before synthesizing this comparison, or clear the flag after a manual restart.");return}try{const d=await clearTtsRestartFlags2();setVoiceRestartState(!1,`Restart flags cleared (${d.cleared_restart_flags||0}). Synthesizing comparison...`),toast("Restart flags cleared","success")}catch(e){toast("Could not clear restart flags: "+e.message,"error"),setOptStatus("Could not clear restart flags");return}}optSynthReferenceBtn.disabled=!0;try{await loadOptimizer(),setOptStatus("Synthesizing reference text for comparison...");const source=await createTtsAudioSource(v.id,text,optCompareBackend.value,"settings","");optState.compareSynthUrl&&URL.revokeObjectURL(optState.compareSynthUrl),optCompareSynthAudio.src=source.url,optState.compareSynthUrl=source.streaming?null:source.url,await optCompareSynthAudio.play().catch(()=>{}),setOptStatus(source.streaming?"Streaming synthesized comparison.":"Synthesized comparison ready.")}catch(e){toast("Synthesis comparison failed: "+e.message,"error"),setOptStatus("Synthesis comparison failed")}finally{optSynthReferenceBtn.disabled=!1}}),wrap.querySelector(".opt-auto-trim").addEventListener("click",async()=>{try{await loadOptimizer();const d=await clientAutoTrimBounds(optState.id);optStart.value=Number(d.start).toFixed(2),optEnd.value=Number(d.end).toFixed(2),redrawOpt(),setOptStatus(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error")}}),wrap.querySelector(".opt-recognize").addEventListener("click",async()=>{try{await loadOptimizer();const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:optState.id})});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();optTranscript.value=d.text||"",setOptStatus("Reference text recognised. Review it, then Save text.")}catch(e){toast("Recognition failed: "+e.message,"error")}}),optSaveTextBtn.addEventListener("click",async()=>{optSaveTextBtn.disabled=!0;try{await saveOptimizerText()}catch(e){toast("Save text failed: "+e.message,"error"),setOptStatus("Save text failed")}finally{optSaveTextBtn.disabled=!1}}),wrap.querySelector(".opt-save-crop").addEventListener("click",async()=>{var _a3;try{await loadOptimizer();const cropStart=Math.max(0,parseFloat(optStart.value)||0),cropEnd=Math.min(optState.duration,parseFloat(optEnd.value)||optState.duration);if(cropStart<=.01&&cropEnd>=optState.duration-.05){setOptStatus("No crop range selected. Adjust Start or End first, then Save crop."),toast("No crop range selected","error");return}if(cropEnd<=cropStart+.1){setOptStatus("Crop range is too short."),toast("Crop range is too short","error");return}setOptStatus(`Saving crop ${cropStart.toFixed(2)}s -> ${cropEnd.toFixed(2)}s...`);const pr=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:optState.id,start:cropStart,end:cropEnd})});if(!pr.ok){const e=await pr.json();throw new Error(e.detail||pr.statusText)}const p=await pr.json(),rr=await fetch("/api/voice-replace",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:p.id,voice_id:v.id,path:v.path,transcript:optTranscript.value})});if(!rr.ok){const e=await rr.json().catch(()=>({}));throw new Error(e.detail||rr.statusText)}const saved=await rr.json();v.transcript=optTranscript.value,v.duration=(_a3=saved.duration)!=null?_a3:p.duration,saved.loudness&&(v.loudness=saved.loudness),saved.path&&(v.path=saved.path),saved.file_type&&(v.file_type=saved.file_type),markVoiceAudioChanged(v),refInput.value=v.transcript,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",vrTypeEl&&(vrTypeEl.textContent=voiceFileType(v).toUpperCase(),vrTypeEl.title=voiceFileType(v)),await refreshOptimizerFromVoice(),toast("Voice crop saved: "+v.id,"success"),markTtsRestartRequired(saved.backup?"Crop saved and loaded. Restart TTS before rebenchmarking; undo is available.":"Crop saved and loaded. Restart TTS before rebenchmarking.")}catch(e){toast("Save crop failed: "+e.message,"error"),setOptStatus("Save crop failed")}}),optStyleInstruct){const styleVariationInput=()=>{const style=optStyleInstruct.value.trim(),text=optTranscript.value.trim()||benchmarkSampleText(),newId=optStyleVoiceId.value.trim()||suggestedStyleVoiceId(v.id,style);return style?text?/^[A-Za-z0-9_\-.]+$/.test(newId)?{style,text,newId,backend:optStyleBackend.value}:(toast("Invalid characters in new voice ID","error"),optStyleVoiceId.focus(),null):(toast("Enter reference text first","error"),optTranscript.focus(),null):(toast("Enter a style instruction first","error"),optStyleInstruct.focus(),null)};optPreviewStyleBtn.addEventListener("click",async()=>{const input=styleVariationInput();if(input){optPreviewStyleBtn.disabled=!0;try{setOptStatus("Synthesizing style preview...");const blob=await fetchTtsPreviewBlob(v.id,input.text,"wav",input.style,input.backend);optStyleAudio.src&&URL.revokeObjectURL(optStyleAudio.src),optStyleAudio.src=URL.createObjectURL(blob),optStyleAudio.style.display="",await optStyleAudio.play().catch(()=>{}),setOptStatus("Style preview ready. If it sounds right, save it as a new voice.")}catch(e){toast("Style preview failed: "+e.message,"error"),setOptStatus("Style preview failed")}finally{optPreviewStyleBtn.disabled=!1}}}),optSaveStyleBtn.addEventListener("click",async()=>{const input=styleVariationInput();if(input){optSaveStyleBtn.disabled=!0;try{setOptStatus(`Synthesizing style variation ${input.newId}...`);const r=await fetch("/api/tts-style-variation",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({source_voice:v.id,voice_id:input.newId,text:input.text,instruct:input.style,backend:input.backend})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();toast("Style variation saved: "+d.voice_id,"success"),setOptStatus("Style variation saved. Restart TTS so the backend scans the new voice."),await loadVoiceLibrary2(),renderIntegrationSnippets()}catch(e){toast("Style variation failed: "+e.message,"error"),setOptStatus("Style variation failed")}finally{optSaveStyleBtn.disabled=!1}}})}wrap.querySelector(".opt-undo").addEventListener("click",async()=>{var _a3;if(confirm(`Restore the original backup for "${v.id}"?`))try{setOptStatus("Restoring original\u2026");const r=await fetch("/api/voice/undo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.duration=(_a3=d.duration)!=null?_a3:v.duration,v.loudness=d.loudness||v.loudness,v.path=d.path||v.path,v.file_type=d.file_type||v.file_type,markVoiceAudioChanged(v),vrTypeEl&&(vrTypeEl.textContent=voiceFileType(v).toUpperCase(),vrTypeEl.title=voiceFileType(v)),await refreshOptimizerFromVoice(),toast("Original restored: "+v.id,"success"),markTtsRestartRequired("Original restored. Restart TTS before rebenchmarking.")}catch(e){toast("Undo failed: "+e.message,"error"),setOptStatus("Undo failed")}}),wrap.querySelector(".opt-save-volume").addEventListener("click",async()=>{var _a3;try{setOptStatus("Saving volume\u2026");const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:Number(optTargetDb.value||-20)})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a3=d.duration)!=null?_a3:v.duration,d.path&&(v.path=d.path),d.file_type&&(v.file_type=d.file_type),markVoiceAudioChanged(v),dbValue.textContent=fmtDbfs2(v),dbCell.title=v.loudness?`avg ${fmtDbfs2(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",await refreshOptimizerFromVoice(),toast("Volume saved: "+v.id,"success"),markTtsRestartRequired("Volume saved. Restart TTS before rebenchmarking this voice.")}catch(e){toast("Volume save failed: "+e.message,"error"),setOptStatus("Volume save failed")}}),optRestartTtsBtn.addEventListener("click",async()=>{optRestartTtsBtn.disabled=!0;try{setOptStatus("Restarting WAV backends (Voice Clone + Streaming)\u2026");const r=await fetch("/api/tts/restart",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_voices2.forEach(voice=>{voice.needs_tts_restart=!1}),updateLibraryInsights();const names=(d.restarted||[]).join(", ")||"containers",errTxt=(d.errors||[]).length?` (errors: ${d.errors.join("; ")})`:"";setVoiceRestartState(!1,`Restarted: ${names}${errTxt}. Rebenchmark now uses the edited voice.`),toast(`TTS restarted: ${names}`,"success")}catch(e){toast("Restart TTS failed: "+e.message,"error"),setOptStatus("Restart TTS failed: "+e.message)}finally{optRestartTtsBtn.disabled=!1}});const refInput=wrap.querySelector(".vr-ref input"),refTranscribeBtn=wrap.querySelector(".ref-transcribe-btn");refInput.addEventListener("input",debounce(async()=>{v.transcript=refInput.value,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",await saveMeta2(v.id,{transcript:v.transcript}),wrap.classList.contains("edit-open")?markTtsRestartRequired("Reference text saved. Restart TTS before rebenchmarking."):v.needs_tts_restart=!0},800)),refTranscribeBtn.addEventListener("click",async()=>{refTranscribeBtn.disabled=!0;try{const d=await loadLibraryVoiceAudio(v),tr=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:d.id})});if(!tr.ok){const e=await tr.json();throw new Error(e.detail)}const text=await tr.json();v.transcript=text.text||"",refInput.value=v.transcript,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",await saveMeta2(v.id,{transcript:v.transcript}),v.needs_tts_restart=!0,toast("Reference text recognised; restart TTS before benchmarking","success")}catch(e){toast("Recognition failed: "+e.message,"error")}finally{refTranscribeBtn.disabled=!1}});const noteInput=wrap.querySelector(".vr-note input");noteInput.addEventListener("input",debounce(async()=>{v.note=noteInput.value,await saveMeta2(v.id,{note:v.note})},800));const sourceInput=wrap.querySelector(".vr-source input");sourceInput.addEventListener("input",debounce(async()=>{v.origin=sourceInput.value.trim(),await saveMeta2(v.id,{origin:v.origin});const tblCell=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"] .vl-tbl-source`);tblCell&&(tblCell.textContent=_displaySource(v)||"-")},800));const starSpans=wrap.querySelectorAll(".star");starSpans.forEach(s=>{s.addEventListener("click",async()=>{const val=parseInt(s.dataset.val),newRating=val===v.rating?0:val;v.rating=newRating,starSpans.forEach((ss,i)=>ss.classList.toggle("on",i<newRating)),await saveMeta2(v.id,{rating:newRating})}),s.addEventListener("mouseenter",()=>{const val=parseInt(s.dataset.val);starSpans.forEach((ss,i)=>ss.classList.toggle("on",i<val))}),s.addEventListener("mouseleave",()=>{starSpans.forEach((ss,i)=>ss.classList.toggle("on",i<(v.rating||0)))})});const benchmarkOneBtn=wrap.querySelector(".benchmark-one-btn"),benchmarkThisVoice=async triggerBtn=>{if(v.needs_tts_restart&&!confirm("This voice changed since the last TTS restart. Benchmarking now may use the cached old voice. Continue anyway?")){setOptStatus("Restart TTS first, then rebenchmark this voice.");return}triggerBtn.disabled=!0,status("Benchmarking "+v.id+"...");try{setBenchmarkProgress(0,1,`Benchmarking ${v.id}`);const d=await runVoiceBenchmark(v.id);mergeBenchmarkResults(d);const hit=(d.voices||[]).find(x=>x.voice_id===v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark);const benchCell=wrap.querySelector(".vr-bench")||document.querySelector("#voices-inspector .vr-bench");if(benchCell){benchCell.className="vr-bench "+benchmarkClass2(v),benchCell.title=benchmarkTitle2(v);const benchValue=benchCell.querySelector(".vr-bench-value");benchValue&&(benchValue.textContent=fmtBenchmark2(v))}const tblRow=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);if(tblRow){const bc=benchmarkClass2(v),btitle=benchmarkTitle2(v),tblDur=tblRow.querySelector(".vl-tbl-dur"),tblFactor=tblRow.querySelector(".vl-tbl-factor"),tblTime=tblRow.querySelector(".vl-tbl-time"),tblWpm=tblRow.querySelector(".vl-tbl-wpm");if(tblDur&&(tblDur.textContent=fmtBenchmarkAudio(v),tblDur.title=`${fmtBenchmarkAudio(v)} \u2014 length of synthesised benchmark audio`),tblFactor&&(tblFactor.textContent=fmtFactor(v),tblFactor.className=`vl-tbl-factor ${bc}`,tblFactor.title=btitle),tblTime&&(tblTime.textContent=fmtElapsed(v),tblTime.className=`vl-tbl-time ${bc}`,tblTime.title=btitle),tblWpm){const wpm=voiceWpm(v);tblWpm.textContent=fmtWpm(v),tblWpm.title=wpm!=null?`${wpm} wpm \u2014 130\u2013180 wpm is natural for long listening`:""}}setBenchmarkProgress(1,1,`Finished ${v.id}`),toast("Benchmarked "+v.id,"success"),setVoiceRestartState(!1,"Benchmark saved for "+v.id)}catch(e){toast("Benchmark failed: "+e.message,"error"),setOptStatus("Benchmark failed")}finally{triggerBtn.disabled=!1}};benchmarkOneBtn.addEventListener("click",()=>benchmarkThisVoice(benchmarkOneBtn)),optRebenchmarkBtn.addEventListener("click",()=>benchmarkThisVoice(optRebenchmarkBtn));const originalPlayBtn=wrap.querySelector(".vr-play-original button"),synthPlayBtn=wrap.querySelector(".vr-play-synth button"),playIcon='<span class="mdi mdi-play"></span>',pauseIcon='<span class="mdi mdi-pause-circle-outline"></span>',generatingIcon='<span class="mdi mdi-timer-sand"></span>';function setLibraryPlayButtonState(btn,state){btn.classList.toggle("is-generating",state==="generating"),btn.innerHTML=state==="playing"?pauseIcon:state==="generating"?generatingIcon:playIcon,btn.title=state==="generating"?"Generating synthesized sample...":state==="playing"?"Pause playback":btn.dataset.playKind==="synth"?"Generate and play synthesized sample":"Play original recording"}async function playLibraryVoice(kind,playBtn){var _a3,_b2,_c2;const bar=$("lib-audio-bar"),audio=$("lib-audio"),playKey=v.id+":"+kind;if(playBtn.dataset.playKind=kind,_activePlayVoiceId===playKey&&!audio.paused){audio.pause(),setLibraryPlayButtonState(playBtn,"idle");return}if(_activePlayVoiceId===playKey&&audio.paused&&audio.src){_activePlayButton=playBtn;try{await audio.play()}catch(e){toast("Play failed: "+e.message,"error")}return}_activePlayButton&&_activePlayButton!==playBtn&&setLibraryPlayButtonState(_activePlayButton,"idle"),_activePlayButton=playBtn,_activePlayVoiceId=playKey,_activePlayUrl&&(URL.revokeObjectURL(_activePlayUrl),_activePlayUrl=null),kind==="synth"&&setLibraryPlayButtonState(playBtn,"generating"),playBtn.disabled=!0;try{if(kind==="synth"){v.needs_tts_restart&&toast("This voice changed since backend refresh; synthesized playback may use a cached voice.","error");const synthMode=((_a3=document.querySelector("#vl-synth-mode-seg .vl-synth-seg-btn.active"))==null?void 0:_a3.dataset.mode)||"preview",text=synthMode==="transcript"&&((_b2=v.transcript)==null?void 0:_b2.trim())||benchmarkSampleText(),textLabel=synthMode==="transcript"?"reference transcript":"preview text",backend=isClone?libraryTtsBackend():"voice_design",source=await createTtsAudioSource(v.id,text,backend,"settings","");audio.src=source.url,source.streaming||(_activePlayUrl=source.url),$("lib-audio-label").textContent=v.id+" \xB7 synthesized "+textLabel+" \xB7 "+(((_c2=backendById(backend))==null?void 0:_c2.label)||backend)}else audio.src=voiceFileUrl2(v),$("lib-audio-label").textContent=v.id+" \xB7 original recording";bar.style.display="",audio.onended=()=>{setLibraryPlayButtonState(playBtn,"idle"),_activePlayVoiceId=null,_activePlayUrl&&(URL.revokeObjectURL(_activePlayUrl),_activePlayUrl=null)},audio.onpause=()=>{_activePlayButton===playBtn&&setLibraryPlayButtonState(playBtn,"idle")},audio.onplay=()=>{setLibraryPlayButtonState(playBtn,"playing")},await audio.play()}catch(e){setLibraryPlayButtonState(playBtn,"idle"),toast("Play failed: "+e.message,"error")}finally{playBtn.disabled=!1}}originalPlayBtn.dataset.playKind="original",synthPlayBtn.dataset.playKind="synth",setLibraryPlayButtonState(originalPlayBtn,"idle"),setLibraryPlayButtonState(synthPlayBtn,"idle"),originalPlayBtn.addEventListener("click",()=>playLibraryVoice("original",originalPlayBtn)),synthPlayBtn.addEventListener("click",()=>playLibraryVoice("synth",synthPlayBtn));const toggleCb=wrap.querySelector(".toggle input");toggleCb.addEventListener("change",async()=>{const nextEnabled=toggleCb.checked,previousEnabled=v.enabled!==!1;toggleCb.disabled=!0;try{const saved=await saveMeta2(v.id,{enabled:nextEnabled});v.enabled=nextEnabled,saved&&saved.path&&(v.path=saved.path),wrap.classList.toggle("vr-disabled",!v.enabled),toast(nextEnabled?"Moved to active_voices":"Moved to hidden_voices","success"),!v.enabled&&!$("show-disabled-cb").checked&&(wrap.style.transition="opacity .4s",wrap.style.opacity="0",setTimeout(()=>wrap.remove(),400))}catch(e){toggleCb.checked=previousEnabled,v.enabled=previousEnabled,wrap.classList.toggle("vr-disabled",!v.enabled),toast("Move failed: "+e.message,"error")}finally{toggleCb.disabled=!1}});const deleteBtn=wrap.querySelector(".delete-btn"),deleteConfirm=wrap.querySelector(".delete-confirm"),deleteCancelBtn=wrap.querySelector(".delete-confirm-cancel"),deleteGoBtn=wrap.querySelector(".delete-confirm-go"),closeDeleteConfirm=()=>wrap.classList.remove("delete-pending");return deleteBtn.addEventListener("click",e=>{e.stopPropagation(),document.querySelectorAll(".vl-row.delete-pending").forEach(row=>{row!==wrap&&row.classList.remove("delete-pending")}),wrap.classList.add("delete-pending"),deleteGoBtn.focus()}),deleteCancelBtn.addEventListener("click",e=>{e.stopPropagation(),closeDeleteConfirm()}),deleteConfirm.addEventListener("click",e=>e.stopPropagation()),deleteGoBtn.addEventListener("click",async e=>{e.stopPropagation(),deleteGoBtn.disabled=!0,deleteCancelBtn.disabled=!0;try{const r=await fetch(`/api/voice/${encodeURIComponent(v.id)}`,{method:"DELETE"});if(!r.ok){const e2=await r.json();throw new Error(e2.detail)}_voices2=_voices2.filter(x=>x.id!==v.id),wrap.style.transition="opacity .3s",wrap.style.opacity="0",setTimeout(()=>{wrap.remove(),$("voice-count").textContent=_voices2.filter(x=>$("show-disabled-cb").checked||x.enabled!==!1).length+" / "+_voices2.length+" voices"},300),toast(`Moved to Voice Trash: ${v.id}`,"success")}catch(e2){toast("Delete failed: "+e2.message,"error"),deleteGoBtn.disabled=!1,deleteCancelBtn.disabled=!1,closeDeleteConfirm()}}),wrap}function renderVoiceGroupsBar(){var _a2;const bar=$("voice-groups-bar");if(!bar)return;const groups={};(_voices2||[]).forEach(v=>{const g=(v.group||"").trim();g&&(groups[g]=(groups[g]||0)+1)});const names=Object.keys(groups).sort();if(!names.length){bar.hidden=!0,bar.innerHTML="";return}bar.hidden=!1;const active=window._voiceGroupFilter||"";bar.innerHTML='<span class="vg-label"><span class="mdi mdi-folder-multiple-outline"></span> Groups</span>'+names.map(g=>`<span class="vg-chip${g===active?" active":""}" data-group="${escHtml(g)}">
${escHtml(g)} <span class="vg-count">${groups[g]}</span>
<button class="vg-del" data-group="${escHtml(g)}" title="Move all ${groups[g]} voices in this group to Voice Trash">&times;</button>
</span>`).join("")+(active?'<button class="vg-clear" title="Clear group filter">Show all</button>':""),bar.querySelectorAll(".vg-chip").forEach(chip=>{chip.addEventListener("click",e=>{if(e.target.closest(".vg-del"))return;const g=chip.dataset.group;window._voiceGroupFilter=window._voiceGroupFilter===g?"":g;const groupSel=$("library-filter-group");groupSel&&(groupSel.value=window._voiceGroupFilter||""),renderVoiceList2()})}),bar.querySelectorAll(".vg-del").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),deleteVoiceGroup(btn.dataset.group)})}),(_a2=bar.querySelector(".vg-clear"))==null||_a2.addEventListener("click",()=>{window._voiceGroupFilter="";const groupSel=$("library-filter-group");groupSel&&(groupSel.value=""),renderVoiceList2()})}async function deleteVoiceGroup(group){const count=(_voices2||[]).filter(v=>(v.group||"").trim()===group).length;if(confirm(`Move all ${count} voice${count!==1?"s":""} in group "${group}" to Voice Trash? You can restore the complete packages from the trash folder later.`))try{const r=await fetch("/api/voices/delete-group",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({group})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();window._voiceGroupFilter===group&&(window._voiceGroupFilter=""),typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),toast(`Moved ${d.count} voice${d.count!==1?"s":""} from "${group}" to Voice Trash`,"success"),await loadVoiceLibrary2()}catch(e){toast("Delete failed: "+e.message,"error")}}async function saveMeta2(voiceId,patch){const r=await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,...patch})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),r.json()}document.addEventListener("click",()=>{document.querySelectorAll(".flag-picker.open").forEach(fp=>fp.classList.remove("open")),document.querySelectorAll(".vl-row.delete-pending").forEach(row=>row.classList.remove("delete-pending"))},{passive:!0});const _bulkSelected2=new Set;function _bulkUpdate(){const bar=$("vl-bulk-bar"),count=$("vl-bulk-count"),n=_bulkSelected2.size;bar&&(bar.hidden=n===0),count&&(count.textContent=`${n} selected`),document.querySelectorAll(".vl-bulk-cb").forEach(cb=>{cb.checked=_bulkSelected2.has(cb.dataset.id)});const headerCb=document.getElementById("vl-select-all-header");if(headerCb){const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id),allSelected=rows.length>0&&rows.every(r=>_bulkSelected2.has(r.dataset.id));headerCb.checked=allSelected}}function _bulkToggle(id,checked){checked?_bulkSelected2.add(id):_bulkSelected2.delete(id),_bulkUpdate()}const _origRenderVoiceList=renderVoiceList2;renderVoiceList2=function(){_origRenderVoiceList.apply(this,arguments),_bulkInjectCheckboxes()};let _bulkLastClickedId=null;function _bulkInjectCheckboxes(){document.querySelectorAll("#voice-list .vl-row").forEach(row=>{if(row.querySelector(".vl-bulk-cb"))return;const id=row.dataset.id,cb=document.createElement("input");cb.type="checkbox",cb.className="vl-bulk-cb",cb.dataset.id=id,cb.checked=_bulkSelected2.has(id),cb.title="Select for bulk edit (shift-click to select a range)",cb.addEventListener("click",e=>{if(e.stopPropagation(),e.shiftKey&&_bulkLastClickedId){const ids=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id).map(r=>r.dataset.id),from=ids.indexOf(_bulkLastClickedId),to=ids.indexOf(id);if(from!==-1&&to!==-1){const[lo,hi]=from<to?[from,to]:[to,from];for(let i=lo;i<=hi;i++)cb.checked?_bulkSelected2.add(ids[i]):_bulkSelected2.delete(ids[i])}}else _bulkToggle(id,cb.checked);_bulkLastClickedId=id,_bulkUpdate()});const compact=row.querySelector(".vl-compact");compact&&compact.prepend(cb)})}(_j=$("vl-bulk-select-all"))==null||_j.addEventListener("click",()=>{document.querySelectorAll("#voice-list .vl-row").forEach(row=>{row.dataset.id&&_bulkSelected2.add(row.dataset.id)}),_bulkUpdate()}),(_k=$("vl-select-all-header"))==null||_k.addEventListener("change",e=>{const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id);e.target.checked?rows.forEach(r=>_bulkSelected2.add(r.dataset.id)):rows.forEach(r=>_bulkSelected2.delete(r.dataset.id)),_bulkUpdate()}),(_l=$("vl-bulk-deselect"))==null||_l.addEventListener("click",()=>{_bulkSelected2.clear(),_bulkUpdate()}),(_m=$("vl-bulk-hide"))==null||_m.addEventListener("click",async()=>{if(!_bulkSelected2.size)return;const ids=[..._bulkSelected2],r=await _bulkSetEnabled(ids,!1);toast(`Hidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected2.clear(),await loadVoiceLibrary2()}),(_n=$("vl-bulk-unhide"))==null||_n.addEventListener("click",async()=>{if(!_bulkSelected2.size)return;const ids=[..._bulkSelected2],r=await _bulkSetEnabled(ids,!0);toast(`Unhidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected2.clear(),await loadVoiceLibrary2()}),(_o=$("vl-bulk-tag"))==null||_o.addEventListener("click",()=>{if(!_bulkSelected2.size)return;const ids=[..._bulkSelected2],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-tag-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-tag-inp",inp.placeholder="tag1, tag2\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML='<span class="mdi mdi-check"></span> Apply',applyBtn.addEventListener("click",async()=>{const newTags=inp.value.split(",").map(t=>t.trim()).filter(Boolean);let done=0;for(const id of ids){const v=(window._voices||[]).find(vv=>vv.id===id),merged=String((v==null?void 0:v.tag)||"").split(",").map(t=>t.trim()).filter(Boolean).slice();for(const t of newTags)merged.some(e=>e.toLowerCase()===t.toLowerCase())||merged.push(t);await saveMeta2(id,{tag:merged.join(", ")}).catch(()=>{}),done++}toast(`Tag added on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected2.clear(),await loadVoiceLibrary2()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_p=$("vl-bulk-source"))==null||_p.addEventListener("click",()=>{if(!_bulkSelected2.size)return;const ids=[..._bulkSelected2],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-source-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-source-inp",inp.placeholder="e.g. fish-audio, cloned\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML='<span class="mdi mdi-check"></span> Apply',applyBtn.addEventListener("click",async()=>{const val=inp.value.trim();let done=0;for(const id of ids)await saveMeta2(id,{origin:val}).catch(()=>{}),done++;toast(`Source set on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected2.clear(),await loadVoiceLibrary2()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_q=$("vl-bulk-rating"))==null||_q.addEventListener("click",()=>{if(!_bulkSelected2.size)return;const ids=[..._bulkSelected2],bar=$("vl-bulk-bar");let sel=bar.querySelector(".vl-bulk-rating-sel");sel||(sel=document.createElement("select"),sel.className="vl-bulk-rating-sel",sel.innerHTML='<option value="">\u2B50 Pick rating\u2026</option>'+[1,2,3,4,5].map(n=>`<option value="${n}">${"\u2605".repeat(n)}</option>`).join(""),sel.addEventListener("change",async()=>{const rating=Number(sel.value);if(!rating)return;let done=0;for(const id of ids)await saveMeta2(id,{rating}).catch(()=>{}),done++;toast(`Rated ${done} voice${done!==1?"s":""}`,"success"),sel.remove(),_bulkSelected2.clear(),await loadVoiceLibrary2()}),bar.appendChild(sel),sel.focus())}),(_r=$("vl-bulk-delete"))==null||_r.addEventListener("click",()=>{_bulkSelected2.size&&_showBulkDeleteConfirm([..._bulkSelected2])});function _showBulkDeleteConfirm(ids){var _a2;(_a2=document.querySelector(".vl-bdc-overlay"))==null||_a2.remove();const plural=ids.length!==1?"s":"",names=ids.slice(0,12).map(id=>`<span class="vl-bdc-id" title="${escHtml(id)}">${escHtml(id)}</span>`).join(""),more=ids.length>12?`<span class="vl-bdc-more">+${ids.length-12} more</span>`:"",ov=document.createElement("div");ov.className="vl-bdc-overlay",ov.innerHTML=`
<div class="vl-bdc-modal" role="alertdialog" aria-modal="true" aria-label="Confirm move voices to trash">
<div class="vl-bdc-title"><span class="mdi mdi-delete-outline"></span> Move ${ids.length} voice${plural} to Voice Trash?</div>
<p class="vl-bdc-sub">This removes the selected voice${plural} from the library while preserving each complete package in the Voice Trash folder.</p>
<div class="vl-bdc-list">${names}${more}</div>
<div class="vl-bdc-actions">
<button type="button" class="vl-bdc-cancel">Cancel</button>
<button type="button" class="vl-bdc-go">Move ${ids.length} voice${plural} to Trash</button>
</div>
</div>`;const close=()=>{ov.remove(),document.removeEventListener("keydown",onKey)};function onKey(e){e.key==="Escape"&&close()}ov.addEventListener("click",e=>{e.target===ov&&close()}),ov.querySelector(".vl-bdc-cancel").addEventListener("click",close),document.addEventListener("keydown",onKey),ov.querySelector(".vl-bdc-go").addEventListener("click",async()=>{const goBtn=ov.querySelector(".vl-bdc-go"),cancelBtn=ov.querySelector(".vl-bdc-cancel");goBtn.disabled=cancelBtn.disabled=!0;let done=0,errors=0;await runPool(ids,async id=>{try{(await fetch(`/api/voice/${encodeURIComponent(id)}`,{method:"DELETE"})).ok?done++:errors++}catch{errors++}},5,n=>{goBtn.innerHTML=`<span class="reh-imsdb-spinner"></span> Moving ${n}/${ids.length}\u2026`}),close(),toast(`Moved ${done} voice${done!==1?"s":""} to Voice Trash${errors?` (${errors} errors)`:""}`,errors?"error":"success"),_bulkSelected2.clear(),await loadVoiceLibrary2()}),document.body.appendChild(ov),ov.querySelector(".vl-bdc-cancel").focus()}async function _bulkSetEnabled(ids,enabled){let done=0;for(const id of ids)await saveMeta2(id,{enabled}).catch(()=>{}),done++;return done}Object.assign(root,{_vlCacheClear,loadVoiceLibrary:loadVoiceLibrary2,mergeBenchmarkResults,loadLibraryVoiceAudio,renderVoiceList:renderVoiceList2,runVoiceBenchmark,runVoiceBenchmarkBatch,activeBenchmarkVoices,benchmarkTargetVoices,openSavedLibraryVoice,suggestLibVoiceId,uploadLibAddFile,transcribeLibAddCurrent,voiceFileUrl:voiceFileUrl2,saveMeta:saveMeta2,deleteVoiceGroup,libAddState}),Object.defineProperty(root,"_voices",{configurable:!0,get:()=>_voices2,set:v=>{_voices2=v}}),root.TTVC=root.TTVC||{},root.TTVC.voiceLibrary=root.TTVC.voiceLibrary||{},Object.assign(root.TTVC.voiceLibrary,{get voices(){return _voices2},load:loadVoiceLibrary2,render:renderVoiceList2,audio:{load:loadLibraryVoiceAudio,url:voiceFileUrl2},benchmarks:{run:runVoiceBenchmark,runBatch:runVoiceBenchmarkBatch},saveMeta:saveMeta2})}(window),function(root){var _a;function backendVoiceId2(value){return typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||JSON.stringify(value)}function shouldFilterBackendVoices2(backend){return["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend||"")}function shouldReplaceWithLibraryVoices(backend){return backend==="voice_design"}async function activeLibraryVoiceIds2(){return _voices.length||await loadVoiceLibrary(),new Set((_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id))}function cleanReferenceText(text){return String(text||"").trim()}function selectedPreviewLibraryVoice(){var _a2;const id=((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"";return id?(_voices||[]).find(v=>v.id===id):null}function previewVoiceWarnings(v){var _a2;const warnings=[],backend=backendById(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"");backend&&backend.id&&!["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend.id)&&warnings.push(backend.id==="nvidia_magpie"?"NVIDIA Magpie uses fixed speaker voices, not saved WAV clone identity.":"This backend may follow style/model voice more than the saved WAV identity."),backend&&backend.id==="nvidia_zeroshot"&&v.duration&&(Number(v.duration)<3||Number(v.duration)>10)&&warnings.push("NVIDIA Zeroshot works best with a clear 3-10 second prompt."),backend&&backend.id==="nvidia_flow"&&!v.transcript&&warnings.push("NVIDIA Flow requires the exact saved reference transcript for this voice."),v.transcript||warnings.push("No reference transcript is saved; cloned identity is harder to judge."),v.duration&&(Number(v.duration)<3||Number(v.duration)>20)&&warnings.push("Reference clip length is outside the 3-20 second sweet spot."),v.needs_tts_restart&&warnings.push("This voice changed since the last backend refresh; restart or clear restart flags before judging it.");const healthWarnings=v.health&&Array.isArray(v.health.warnings)?v.health.warnings:[];return warnings.push(...healthWarnings.slice(0,3)),warnings}function updatePreviewVoiceMatchPanel2(){const panel=$("preview-match-panel");if(!panel)return;const v=selectedPreviewLibraryVoice();if(!v){panel.hidden=!0;return}panel.hidden=!1;const lang=v.language||v.lang||(v.id||"").split("_")[0]||"-",gender=v.gender||(v.id||"").split("_")[1]||"-",db=fmtDbfs(v),dur=v.duration?fmtDuration(v.duration):"-";$("preview-match-title").textContent=v.id,$("preview-match-detail").textContent=`${lang} \xB7 ${gender} \xB7 ${dur} \xB7 ${db} dBFS`;const warnings=previewVoiceWarnings(v);$("preview-match-warning").textContent=warnings.length?warnings.join(" "):"For a fair voice match check, play the WAV and synthesize the exact saved reference text.";const transcript=cleanReferenceText(v.transcript||"");$("preview-match-transcript").textContent=transcript||"No reference text saved for this voice.",$("preview-ref-use-text").disabled=!transcript,$("preview-ref-synth").disabled=!transcript;const audio=$("preview-ref-audio"),expected=voiceFileUrl(v);audio.dataset.src!==expected&&(audio.pause(),audio.src=expected,audio.dataset.src=expected);const actionsEl=panel.querySelector(".preview-match-actions");let personaBtn=panel.querySelector(".preview-persona-btn");v.persona?personaBtn||(personaBtn=document.createElement("button"),personaBtn.className="btn-secondary preview-persona-btn",personaBtn.type="button",personaBtn.textContent="Rewrite with persona",actionsEl==null||actionsEl.appendChild(personaBtn),personaBtn.addEventListener("click",async()=>{const text=$("preview-text-area").value.trim();if(!text){toast("Enter text to rewrite","error");return}const lv=selectedPreviewLibraryVoice();if(!(lv!=null&&lv.persona)){toast("This voice has no persona","error");return}personaBtn.disabled=!0,personaBtn.textContent="Rewriting\u2026";try{const llmUrl=localStorage.getItem("refine-llm-url")||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",r=await fetch("/api/rewrite-with-persona",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,persona:lv.persona,llm_url:llmUrl,model:(_appSettings==null?void 0:_appSettings.llm_model)||"",mode:"rewrite"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("preview-text-area").value=d.text,toast("Text rewritten in persona style","success")}catch(e){toast("Persona rewrite failed: "+e.message,"error")}finally{personaBtn.disabled=!1,personaBtn.textContent="Rewrite with persona"}})):personaBtn==null||personaBtn.remove();const personaToggle=$("preview-persona-toggle");if(personaToggle){const label=personaToggle.closest("label");v.persona?(personaToggle.disabled=!1,label&&(label.title="Rewrite text through this voice's character persona before generating")):(personaToggle.checked=!1,personaToggle.disabled=!0,label&&(label.title="This voice has no character persona saved \u2014 set one on the Voice Inspector page first."))}}async function synthesizeSelectedReferenceText(){const v=selectedPreviewLibraryVoice();if(!v){toast("Select a library voice first","error");return}let text=cleanReferenceText(v.transcript||"");if(!text){toast("This voice has no reference text","error");return}if(v.needs_tts_restart){if(!confirm("This voice is marked as needing a TTS restart. If you already restarted the backend, clear the flag and synthesize anyway?"))return;await clearTtsRestartFlags(),v.needs_tts_restart=!1,updatePreviewVoiceMatchPanel2()}const backend=$("tts-backend-select").value;if(!backend){toast("No available TTS backend","error");return}const btn=$("preview-ref-synth");btn.disabled=!0;try{$("preview-text-area").value=text;const source=await createTtsAudioSource2(v.id,text,backend,$("preview-playback-mode").value,$("preview-style-instruction").value.trim());previewBlob2=source.blob;const audio=$("preview-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,toast(source.streaming?"Reference text streaming":"Reference text synthesized","success")}catch(e){toast("Reference synthesis failed: "+e.message,"error")}finally{btn.disabled=!1}}$("fetch-tts-voices-btn").addEventListener("click",async()=>{var _a2;$("fetch-tts-voices-btn").disabled=!0;try{const backend=(_a2=$("tts-backend-select"))==null?void 0:_a2.value;if(!backend)throw new Error("No available TTS backend");let ids;if(shouldReplaceWithLibraryVoices(backend))_voices.length||await loadVoiceLibrary(),ids=(_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id);else{const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());let voices=Array.isArray(rawVoices)?rawVoices:[];if(shouldFilterBackendVoices2(backend)){const activeIds=await activeLibraryVoiceIds2();voices=voices.filter(v=>activeIds.has(backendVoiceId2(v)))}ids=voices.map(backendVoiceId2)}const sel=$("tts-voice-select"),prev=sel.value;window.VoicePicker?(VoicePicker.upgrade("tts-voice-select"),VoicePicker.populate("tts-voice-select",ids),prev&&ids.includes(prev)&&VoicePicker.setValue("tts-voice-select",prev)):(sel.innerHTML='<option value="">\u2014 select after fetch \u2014</option>',ids.forEach(id=>{const o=document.createElement("option");o.value=o.textContent=id,sel.appendChild(o)}),prev&&ids.includes(prev)&&(sel.value=prev)),updatePreviewVoiceMatchPanel2();const suffix=shouldFilterBackendVoices2(backend)||shouldReplaceWithLibraryVoices(backend)?" active voices":" voices";toast("Fetched "+ids.length+suffix,"success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("fetch-tts-voices-btn").disabled=!1}});function _ttsIsFishBackend(id){return/fish/i.test(id||"")}const FISH_EMOTION_TAGS={"Basic emotions":[["happy","Cheerful, upbeat"],["sad","Melancholic, downcast"],["angry","Frustrated, aggressive"],["excited","Energetic, enthusiastic"],["calm","Peaceful, relaxed"],["nervous","Anxious, uncertain"],["confident","Assertive, self-assured"],["surprised","Shocked, amazed"],["satisfied","Content, pleased"],["delighted","Very pleased, joyful"],["scared","Frightened, fearful"],["worried","Concerned, troubled"],["upset","Disturbed, distressed"],["frustrated","Annoyed, exasperated"],["depressed","Very sad, hopeless"],["empathetic","Understanding, caring"],["embarrassed","Ashamed, awkward"],["disgusted","Repelled, revolted"],["moved","Emotionally touched"],["proud","Accomplished, satisfied"],["relaxed","At ease, casual"],["grateful","Thankful, appreciative"],["curious","Inquisitive, interested"],["sarcastic","Ironic, mocking"]],"Advanced emotions":[["disdainful","Contemptuous, scornful"],["unhappy","Discontent, dissatisfied"],["anxious","Very worried, uneasy"],["hysterical","Uncontrollably emotional"],["indifferent","Uncaring, neutral"],["uncertain","Doubtful, unsure"],["doubtful","Skeptical, questioning"],["confused","Puzzled, perplexed"],["disappointed","Let down"],["regretful","Sorry, remorseful"],["guilty","Culpable, responsible"],["ashamed","Deeply embarrassed"],["jealous","Envious, resentful"],["envious","Wanting what others have"],["hopeful","Optimistic about future"],["optimistic","Positive outlook"],["pessimistic","Negative outlook"],["nostalgic","Longing for the past"],["lonely","Isolated, alone"],["bored","Uninterested, weary"],["contemptuous","Showing contempt"],["sympathetic","Showing sympathy"],["compassionate","Showing deep care"],["determined","Resolved, decided"],["resigned","Accepting defeat"]]},FISH_TAG_SAMPLE_TEXT=`[happy] I got the promotion!
[uncertain] But... it means relocating.
[sad] I'll miss everyone here.
[hopeful] Though it's a great opportunity.
[determined] I'm going to make it work!`;function _fishInsertTag(tag){var _a2,_b;const ta=$("preview-text-area");if(!ta)return;const snippet=`[${tag}] `,s=(_a2=ta.selectionStart)!=null?_a2:ta.value.length,e=(_b=ta.selectionEnd)!=null?_b:ta.value.length;ta.value=ta.value.slice(0,s)+snippet+ta.value.slice(e);const pos=s+snippet.length;ta.setSelectionRange(pos,pos),ta.focus()}function _fishTagsRender(){const body=$("fish-tags-body");!body||body.dataset.built||(body.innerHTML=Object.entries(FISH_EMOTION_TAGS).map(([group,tags])=>`
<div class="fish-tags-group">
<div class="fish-tags-group-title">${escHtml(group)} <span class="note">(${tags.length})</span></div>
<div class="fish-tags-grid">
${tags.map(([t,d])=>`<button type="button" class="fish-tag" data-tag="${escHtml(t)}" title="${escHtml(d)}">[${escHtml(t)}]</button>`).join("")}
</div>
</div>`).join(""),body.querySelectorAll(".fish-tag").forEach(b=>b.addEventListener("click",()=>_fishInsertTag(b.dataset.tag))),body.dataset.built="1")}function _fishTagsSync(){var _a2;const panel=$("fish-tags-panel");panel&&(panel.hidden=!_ttsIsFishBackend(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||""))}(function(){var _a2;const toggle=$("fish-tags-toggle"),body=$("fish-tags-body");toggle==null||toggle.addEventListener("click",()=>{_fishTagsRender(),body.hidden=!body.hidden,toggle.textContent=body.hidden?"Show tags":"Hide tags"}),(_a2=$("fish-tags-sample"))==null||_a2.addEventListener("click",()=>{const ta=$("preview-text-area");ta&&(ta.value=FISH_TAG_SAMPLE_TEXT,ta.focus())})})();function _ttsApplyEmotionTag(text,emotionValue){var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"";if(!_ttsIsFishBackend(backend)||!emotionValue||/^\s*\[/.test(text))return text;const tag=typeof _rehEmotionEnglishTag=="function"?_rehEmotionEnglishTag(emotionValue):"";return tag?`[${tag}] ${text}`:text}setTimeout(function(){const sel=$("preview-emotion-select");!sel||typeof window.REH_EMOTIONS=="undefined"||(window.REH_EMOTIONS.forEach(e=>{if(!e.value)return;const o=document.createElement("option");o.value=e.value,o.textContent=`${e.emoji} ${e.label}`,sel.appendChild(o)}),sel.addEventListener("change",()=>{var _a2;const backend=((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"",help=$("preview-emotion-help");if(_ttsIsFishBackend(backend))help&&(help.style.display=sel.value?"block":"none",help.textContent=sel.value?"Applied as an inline [tag] in the text for Fish-Speech \u2014 the style instruction field below is ignored by this backend.":"");else{help&&(help.style.display="none");const styleInput=$("preview-style-instruction");styleInput&&sel.value&&(styleInput.value=sel.value)}}),_fishTagsSync())},0),$("tts-backend-select").addEventListener("change",()=>{var _a2;const sel=$("tts-voice-select");sel.innerHTML='<option value="">\u2014 select after fetch \u2014</option>',updateBackendHelp(),updatePreviewVoiceMatchPanel2(),previewBlob2=null,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,(_a2=$("preview-emotion-select"))==null||_a2.dispatchEvent(new Event("change")),_fishTagsSync()}),$("tts-voice-select").addEventListener("change",updatePreviewVoiceMatchPanel2),$("preview-ref-play").addEventListener("click",async()=>{updatePreviewVoiceMatchPanel2();const audio=$("preview-ref-audio");try{await audio.play()}catch(e){toast("Reference playback failed: "+e.message,"error")}}),$("preview-ref-use-text").addEventListener("click",()=>{const v=selectedPreviewLibraryVoice(),text=cleanReferenceText((v==null?void 0:v.transcript)||"");if(!text){toast("This voice has no reference text","error");return}$("preview-text-area").value=text,toast("Reference text copied to target text","success")}),$("preview-ref-synth").addEventListener("click",synthesizeSelectedReferenceText);let _ttsStreamHealth=null;function effectiveTtsPlaybackMode(override="settings"){return override&&override!=="settings"?override:_appSettings.tts_stream_mode||"auto"}async function isTtsStreamAvailable(force=!1){if(_ttsStreamHealth&&!force)return _ttsStreamHealth.ok;try{return _ttsStreamHealth=await fetch("/api/tts-stream-health").then(r=>r.json()),!!_ttsStreamHealth.ok}catch{return _ttsStreamHealth={ok:!1},!1}}async function createTtsStreamUrl(voice,text,instruct=""){if(!await isTtsStreamAvailable())throw new Error("streaming backend unavailable");const r=await fetch("/api/tts-stream-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,voice,instruct})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return(await r.json()).url}async function _ttsPreviewFetchWithRetry(body,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})}catch(e){if(i===tries)throw e;await new Promise(r=>setTimeout(r,2500*i))}}function _ttsBackendForVoice2(voiceId,fallbackBackend){if(!voiceId||voiceId==="me")return fallbackBackend;const v=(window._voices||[]).find(x=>x.id===voiceId);return v&&!v.has_ref?"voice_design":fallbackBackend}async function fetchTtsPreviewBlob2(voice,text,responseFormat="wav",instruct="",backend="voice_clone",applyPersona=!1,extra=null){const body={text,voice,response_format:responseFormat,instruct,backend};applyPersona&&(body.apply_persona=!0),extra&&typeof extra=="object"&&Object.assign(body,extra);const r=await _ttsPreviewFetchWithRetry(body);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const blob=await r.blob();if(responseFormat==="wav"){const v=(window._voices||[]).find(x=>x.id===voice),gainDb=v&&v.loudness&&typeof v.loudness.gain_db=="number"?v.loudness.gain_db:0;if(gainDb)try{const nr=await fetch("/api/audio/apply-gain?gain_db="+encodeURIComponent(gainDb),{method:"POST",body:blob});if(nr.ok)return await nr.blob()}catch{}}return blob}function _textWords(s){return String(s||"").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean)}function _textSimilarity(a,b){const wa=_textWords(a),wb=_textWords(b);if(!wa.length&&!wb.length)return 1;if(!wa.length||!wb.length)return 0;const counts=new Map;wa.forEach(w=>counts.set(w,(counts.get(w)||0)+1));let overlap=0;return wb.forEach(w=>{const c=counts.get(w);c&&(overlap++,counts.set(w,c-1))}),2*overlap/(wa.length+wb.length)}async function _voiceRoundtripCheck2(voiceId,text,backend,instruct=""){const blob=await fetchTtsPreviewBlob2(voiceId,text,"wav",instruct,backend),fd=new FormData;fd.append("file",blob,"roundtrip.wav"),fd.append("backend","configured");const r=await fetch("/api/transcribe-bytes",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return{transcript:d.text||"",score:_textSimilarity(text,d.text||""),blob}}async function createTtsAudioSource2(voice,text,backend="voice_clone",modeOverride="settings",instruct="",applyPersona=!1,extra=null){const mode=effectiveTtsPlaybackMode(modeOverride);if(backend!=="streaming"||mode==="buffered"){const blob=await fetchTtsPreviewBlob2(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}try{return{url:await createTtsStreamUrl(voice,text,instruct),blob:null,streaming:!0,label:"streaming"}}catch(e){if(mode==="streaming")throw e;const blob=await fetchTtsPreviewBlob2(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}}let previewBlob2=null;const PREVIEW_SAMPLE_TEXT="Hello! This is a voice preview from TTS Voice Creator - Clone and Design.";$("preview-text-area").addEventListener("focus",()=>{$("preview-text-area").value===PREVIEW_SAMPLE_TEXT&&($("preview-text-area").value="")},{once:!0});function _onPreviewGenerated(source,voice,text,backend,instruct){typeof effectsSourceBlob!="undefined"&&(window._effectsSourceBlob=null),window._effectsSynthArgs={voice,text,instruct:instruct||"",backend};const ea=$("effects-apply-btn");ea&&(ea.disabled=!1);const ap=$("add-to-playlist-btn");ap&&source.blob&&(ap.disabled=!1),typeof historyPush=="function"&&source.blob&&historyPush(voice,text,backend,source.blob,source.url)}const _TRYOUT_SPEED_KEY="ttsvc_tryout_native_speed";(function(){const saved=localStorage.getItem(_TRYOUT_SPEED_KEY);if(saved){const el=$("preview-native-speed");el&&(el.value=saved)}})(),(_a=$("preview-native-speed"))==null||_a.addEventListener("change",function(){localStorage.setItem(_TRYOUT_SPEED_KEY,this.value)}),$("preview-btn").addEventListener("click",async()=>{var _a2,_b,_c,_d;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value),applyPersona=((_b=$("preview-persona-toggle"))==null?void 0:_b.checked)||!1;if(!backend){toast("No available TTS backend","error");return}if(!voice){toast("Select a TTS voice","error");return}if(!text){toast("Enter preview text","error");return}$("preview-btn").disabled=!0,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0,$("add-to-playlist-btn")&&($("add-to-playlist-btn").disabled=!0),$("effects-apply-btn")&&($("effects-apply-btn").disabled=!0);const _nspd=parseFloat((_c=$("preview-native-speed"))==null?void 0:_c.value),_extra=!isNaN(_nspd)&&_nspd!==1?{speed:_nspd}:null;try{const audio=$("preview-audio"),source=((_d=$("preview-chunked-toggle"))==null?void 0:_d.checked)&&text.length>200&&typeof generateChunkedTts=="function"?await generateChunkedTts(voice,text,backend,instruct,_extra,applyPersona):await createTtsAudioSource2(voice,text,backend,$("preview-playback-mode").value,instruct,applyPersona,_extra);previewBlob2=source.blob,window._previewVoice=voice,window._previewBackend=backend,window._previewText=text,audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,_onPreviewGenerated(source,voice,text,backend,instruct),toast(source.streaming?"Streaming preview playing":source.label==="chunked"?`Chunked (${text.length} chars) playing`:"Preview playing","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{$("preview-btn").disabled=!1}}),$("save-preview-mp3-btn").addEventListener("click",async()=>{var _a2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,instruct=$("preview-style-instruction").value.trim(),text=_ttsApplyEmotionTag($("preview-text-area").value.trim(),(_a2=$("preview-emotion-select"))==null?void 0:_a2.value);if(!backend){toast("No available TTS backend","error");return}if(!voice||!text)return;const btn=$("save-preview-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob2(voice,text,"mp3",instruct,backend),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(voice||"preview")+"_preview.mp3",a.click(),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),$("save-preview-btn").addEventListener("click",()=>{if(!previewBlob2)return;const a=document.createElement("a");a.href=URL.createObjectURL(previewBlob2),a.download=($("tts-voice-select").value||"preview")+"_preview.wav",a.click()});const PERF_HISTORY_KEY="vcf-perf-history",PERF_HISTORY_MAX=50,PERF_HISTORY_SORT={key:"ts",dir:"desc"};function perfHistoryLoad2(){try{return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY)||"[]")}catch{return[]}}function perfHistorySave(entries){try{localStorage.setItem(PERF_HISTORY_KEY,JSON.stringify(entries.slice(-PERF_HISTORY_MAX)))}catch{}}function perfHistoryAdd2(entry){const h=perfHistoryLoad2();h.push(entry),perfHistorySave(h)}function perfSparklineSvg(rtfValues){if(!rtfValues.length)return"";const W=120,H=32,PAD=2,barW=Math.max(4,Math.floor((W-PAD*2)/rtfValues.length)-1),maxV=Math.max(...rtfValues,1),bars=rtfValues.map((v,i)=>{const bh=Math.max(3,Math.round(v/maxV*(H-PAD*2))),x=PAD+i*(barW+1),y=H-PAD-bh,col=v<1?"var(--green)":"var(--yellow)";return`<rect x="${x}" y="${y}" width="${barW}" height="${bh}" rx="1" fill="${col}" opacity=".8"/>`}).join("");return`<svg viewBox="0 0 ${W} ${H}" class="perf-sparkline" aria-hidden="true">${bars}</svg>`}function perfHistoryVoiceLookup(voiceId){return(Array.isArray(window._voices)&&window._voices.length?window._voices:typeof _voices!="undefined"&&Array.isArray(_voices)?_voices:[]).find(v=>v&&(v.id===voiceId||v.name===voiceId||v.voice_id===voiceId))||null}function perfHistoryVoiceMeta(entry){var _a2,_b,_c;const voice=(entry==null?void 0:entry.voice)||"",saved=(entry==null?void 0:entry.voiceMeta)||{},lib=perfHistoryVoiceLookup(voice)||{},language=saved.lang||saved.language||(entry==null?void 0:entry.lang)||(entry==null?void 0:entry.language)||lib.lang||lib.language||"",gender=String(saved.gender||(entry==null?void 0:entry.gender)||lib.gender||"").trim().toUpperCase().charAt(0);return{id:voice,label:saved.label||saved.display_name||(entry==null?void 0:entry.voiceLabel)||lib.display_name||lib.name||voice,lang:language,gender:["F","M","N"].includes(gender)?gender:"",flag:saved.flag||(entry==null?void 0:entry.flag)||lib.flag||"",avatar:saved.avatar||(entry==null?void 0:entry.avatar)||lib.avatar||"",hasPicture:!!((_c=(_b=(_a2=saved.has_picture)!=null?_a2:saved.hasPicture)!=null?_b:entry==null?void 0:entry.hasPicture)!=null?_c:lib.has_picture)}}function perfHistoryExtraFields(voice,backend){const lib=perfHistoryVoiceLookup(voice)||{};return{voiceMeta:{label:lib.display_name||lib.name||voice,lang:lib.lang||lib.language||"",gender:lib.gender||"",flag:lib.flag||"",avatar:lib.avatar||"",has_picture:!!lib.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function perfHistoryGenderLabel(gender){return{F:"Female",M:"Male",N:"Diverse"}[gender]||""}function perfHistoryDeviceLabel(entry){return(entry==null?void 0:entry.device)||(typeof backendComputeDevice=="function"?backendComputeDevice((entry==null?void 0:entry.backend)||""):"")||"Unknown"}function perfHistoryDeviceHtml(entry){const label=perfHistoryDeviceLabel(entry),cls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass((entry==null?void 0:entry.backend)||""):label.toLowerCase().includes("gpu")?"gpu":label.toLowerCase().includes("cpu")?"cpu":"";return`<span class="bench-device ${escHtml(cls)}">${escHtml(label)}</span>`}function perfHistoryAvatarHtml(entry){var _a2;const meta=perfHistoryVoiceMeta(entry),title=meta.label||meta.id||"Voice";if(meta.hasPicture)return`<span class="perf-history-avatar" title="${escHtml(title)}"><img src="/api/voice/picture/${encodeURIComponent(meta.id)}" alt=""></span>`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,24):null;if(icon)return`<span class="perf-history-avatar" title="${escHtml(title)}">${icon.replace(/vp-avatar/g,"perf-history-avatar-icon")}</span>`;const color=typeof avatarColor=="function"?avatarColor(meta.lang||meta.id||title):"#6b7280",init=((_a2=(title||"?").trim()[0])==null?void 0:_a2.toUpperCase())||"?";return`<span class="perf-history-avatar perf-history-avatar-init" style="background:${color}" title="${escHtml(title)}">${escHtml(init)}</span>`}function perfHistorySortValue(entry,key){switch(key){case"backend":return String(entry.backend||"").toLowerCase();case"language":return String(perfHistoryVoiceMeta(entry).lang||"").toLowerCase();case"gender":return String(perfHistoryGenderLabel(perfHistoryVoiceMeta(entry).gender)||"").toLowerCase();case"voice":return String(entry.voice||"").toLowerCase();case"device":return String(perfHistoryDeviceLabel(entry)||"").toLowerCase();case"avgLatencyMs":return Number(entry.avgLatencyMs);case"minLatencyMs":return Number(entry.minLatencyMs);case"avgRtf":return Number(entry.avgRtf);case"ts":default:return Number(entry.ts)}}function perfHistoryCompare(a,b){const av=perfHistorySortValue(a,PERF_HISTORY_SORT.key),bv=perfHistorySortValue(b,PERF_HISTORY_SORT.key);let result=0;if(typeof av=="string"||typeof bv=="string")result=String(av).localeCompare(String(bv),void 0,{numeric:!0,sensitivity:"base"});else{const an=Number.isFinite(av)?av:-1/0,bn=Number.isFinite(bv)?bv:-1/0;result=an===bn?0:an-bn}return PERF_HISTORY_SORT.dir==="asc"?result:-result}function perfHistoryHeadButton(key,label){const active=PERF_HISTORY_SORT.key===key,icon=active?PERF_HISTORY_SORT.dir==="asc"?"mdi-arrow-up":"mdi-arrow-down":"mdi-swap-vertical";return`<button class="perf-history-sort${active?" active":""}" type="button" data-history-sort="${escHtml(key)}" aria-label="Sort by ${escHtml(label)}">
<span>${escHtml(label)}</span><span class="mdi ${icon}" aria-hidden="true"></span>
</button>`}function renderPerfHistory2(){var _a2,_b;const histList=$("perf-history-list");if(!histList)return;const filterEl=$("perf-history-filter-current"),filterOn=filterEl==null?void 0:filterEl.checked,curBack=(_a2=$("perf-backend-select"))==null?void 0:_a2.value,curVoice=(_b=$("perf-voice-select"))==null?void 0:_b.value;let entries=perfHistoryLoad2().slice();if(filterOn&&curBack&&(entries=entries.filter(e=>e.backend===curBack&&e.voice===curVoice)),!entries.length){histList.innerHTML='<div class="perf-history-empty">'+(filterOn?"No history for this backend/voice yet.":"No benchmark history yet. Run a benchmark above to start tracking.")+"</div>";return}entries.sort((a,b)=>perfHistoryCompare(a,b)||Number(b.ts)-Number(a.ts));const head=`<div class="perf-history-row perf-history-head">
<span>${perfHistoryHeadButton("ts","Date / Time")}</span>
<span>${perfHistoryHeadButton("backend","Backend")}</span>
<span>Profile</span>
<span>${perfHistoryHeadButton("language","Language")}</span>
<span>${perfHistoryHeadButton("gender","Gender")}</span>
<span>${perfHistoryHeadButton("voice","Voice")}</span>
<span>${perfHistoryHeadButton("device","CPU / GPU")}</span>
<span>${perfHistoryHeadButton("avgLatencyMs","Avg latency")}</span>
<span>${perfHistoryHeadButton("minLatencyMs","Min")}</span>
<span>${perfHistoryHeadButton("avgRtf","Avg RTF")}</span>
<span></span>
</div>`,rows=entries.map(e=>{const dt=new Date(e.ts).toLocaleString([],{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),rtfCls=e.avgRtf<1?"perf-good":"perf-slow",meta=perfHistoryVoiceMeta(e);return`<div class="perf-history-row">
<span class="perf-history-ts">${escHtml(dt)}</span>
<span>${escHtml(e.backend)}</span>
<span>${perfHistoryAvatarHtml(e)}</span>
<span>${escHtml(meta.lang||"-")}</span>
<span>${escHtml(perfHistoryGenderLabel(meta.gender)||"-")}</span>
<span>${escHtml(e.voice)}</span>
<span>${perfHistoryDeviceHtml(e)}</span>
<span>${Math.round(e.avgLatencyMs)} ms</span>
<span>${Math.round(e.minLatencyMs)} ms</span>
<span class="${rtfCls}">${e.avgRtf.toFixed(2)}</span>
<span class="perf-history-del" data-ts="${e.ts}" title="Remove"><span class="mdi mdi-close"></span></span>
</div>`}).join("");histList.innerHTML=head+rows,histList.querySelectorAll(".perf-history-sort").forEach(btn=>{btn.addEventListener("click",()=>{const key=btn.dataset.historySort||"ts";PERF_HISTORY_SORT.key===key?PERF_HISTORY_SORT.dir=PERF_HISTORY_SORT.dir==="asc"?"desc":"asc":(PERF_HISTORY_SORT.key=key,PERF_HISTORY_SORT.dir=key==="backend"||key==="voice"?"asc":"desc"),renderPerfHistory2()})}),histList.querySelectorAll(".perf-history-del").forEach(btn=>{btn.addEventListener("click",()=>{const ts=Number(btn.dataset.ts);perfHistorySave(perfHistoryLoad2().filter(e=>e.ts!==ts)),renderPerfHistory2()})})}(function(){var _a2,_b;const perfBackendSel=$("perf-backend-select"),perfVoiceSel=$("perf-voice-select"),perfFetchBtn=$("perf-fetch-voices-btn"),perfRunBtn=$("perf-run-btn"),perfClearBtn=$("perf-clear-btn"),perfProgress=$("perf-progress"),perfResultsCard=$("perf-results-card"),perfSummary=$("perf-summary"),perfTbody=$("perf-tbody"),perfText=$("perf-text"),perfRunsSel=$("perf-runs");if(!perfRunBtn)return;let perfRows=[];function populatePerfBackends(){if(!perfBackendSel)return;const cur=perfBackendSel.value;perfBackendSel.innerHTML=availableTtsBackends().map(b=>`<option value="${escHtml(b.id)}"${b.id===cur?" selected":""}>${escHtml(b.label)}</option>`).join("")||'<option value="">No backends available</option>'}populatePerfBackends(),window.BenchmarkVoicePicker&&BenchmarkVoicePicker.upgrade("perf-voice-select",{placeholder:"-- select after fetch --",empty:"No voices"}),perfFetchBtn.addEventListener("click",async()=>{const backend=perfBackendSel.value;if(!backend){toast("Select a backend first","error");return}perfFetchBtn.disabled=!0;try{const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json()),cur=perfVoiceSel.value,items=rawVoices.map(v=>{const id=backendVoiceId2(v);return{id,label:id,meta:(window._voices||[]).find(x=>x&&x.id===id)||(typeof v=="object"?v:null)}}).filter(v=>v.id);window.BenchmarkVoicePicker?(BenchmarkVoicePicker.populate("perf-voice-select",items,{placeholder:"-- select after fetch --",empty:"No voices"}),cur&&items.some(v=>v.id===cur)&&BenchmarkVoicePicker.set("perf-voice-select",cur)):perfVoiceSel.innerHTML=items.map(v=>`<option value="${escHtml(v.id)}"${v.id===cur?" selected":""}>${escHtml(v.id)}</option>`).join("")||'<option value="">No voices</option>',window.dispatchEvent(new CustomEvent("benchmark:tts-voices-fetched",{detail:{backend,voices:items}}))}catch(e){toast("Fetch voices failed: "+e.message,"error")}finally{perfFetchBtn.disabled=!1}});function updateTrendDisplay(backend,voice,currentAvgRtf){const trendRow=$("perf-trend-row"),trendBadge=$("perf-trend-badge"),sparkWrap=$("perf-sparkline-wrap")||(trendRow==null?void 0:trendRow.querySelector(".perf-sparkline-wrap"));if(!trendRow)return;const history=perfHistoryLoad2().filter(e=>e.backend===backend&&e.voice===voice&&typeof e.avgRtf=="number");if(history.length===0){trendRow.style.display="none";return}const prevRtf=history[history.length-1].avgRtf,delta=currentAvgRtf-prevRtf,pct=Math.abs(delta/Math.max(prevRtf,.01))*100;let cls,label;pct<5?(cls="perf-trend-stable",label='<span class="mdi mdi-minus"></span> Stable'):delta<0?(cls="perf-trend-better",label=`<span class="mdi mdi-arrow-down"></span> ${pct.toFixed(0)}% faster`):(cls="perf-trend-worse",label=`<span class="mdi mdi-arrow-up"></span> ${pct.toFixed(0)}% slower`),trendBadge.className="perf-trend-badge "+cls,trendBadge.innerHTML=label;const rtfValues=[...history.slice(-9).map(e=>e.avgRtf),currentAvgRtf];sparkWrap&&(sparkWrap.innerHTML=perfSparklineSvg(rtfValues)),trendRow.style.display=""}function renderPerfTable(sessionDone=!1){if(!perfRows.length){perfResultsCard.style.display="none";return}perfResultsCard.style.display="",perfTbody.innerHTML=perfRows.map((r,i)=>{const rtf=r.audioDuration>0?(r.latencyMs/1e3/r.audioDuration).toFixed(2):"\u2014",ok2=r.ok;return`<tr class="${ok2?"":"perf-row-error"}">
<td>${i+1}</td>
<td>${escHtml(r.backend)}</td>
<td><span class="bench-device ${typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass(r.backend):""}" title="Inferred from the selected TTS backend metadata">${escHtml(typeof backendComputeDevice=="function"?backendComputeDevice(r.backend):"Unknown")}</span></td>
<td>${escHtml(r.voice)}</td>
<td>${ok2?r.latencyMs:"\u2014"}</td>
<td>${ok2&&r.audioDuration>0?r.audioDuration.toFixed(2):"\u2014"}</td>
<td>${ok2?rtf:"\u2014"}</td>
<td>${ok2?'<span class="perf-ok">OK</span>':`<span class="perf-err">${escHtml(r.error||"Error")}</span>`}</td>
</tr>`}).join("");const ok=perfRows.filter(r=>r.ok);if(ok.length){const avg=ok.reduce((s,r)=>s+r.latencyMs,0)/ok.length,minL=Math.min(...ok.map(r=>r.latencyMs)),maxL=Math.max(...ok.map(r=>r.latencyMs)),rtfArr=ok.filter(r=>r.audioDuration>0).map(r=>r.latencyMs/1e3/r.audioDuration),avgRtf=rtfArr.length?rtfArr.reduce((s,v)=>s+v,0)/rtfArr.length:0,labelEl=$("perf-results-label");labelEl&&(labelEl.textContent=`${ok.length} run${ok.length>1?"s":""} \u2014 ${perfBackendSel.value} / ${perfVoiceSel.value}`),perfSummary.innerHTML=`
<span class="perf-stat"><strong>${Math.round(avg)} ms</strong> avg latency</span>
<span class="perf-stat"><strong>${minL} ms</strong> best</span>
<span class="perf-stat"><strong>${maxL} ms</strong> worst</span>
<span class="perf-stat"><strong>${avgRtf.toFixed(2)}</strong> avg RTF</span>
<span class="perf-stat ${avgRtf<1?"perf-good":"perf-slow"}">${avgRtf<1?'<span class="mdi mdi-check-circle-outline"></span> Real-time capable':'<span class="mdi mdi-alert-outline"></span> Slower than real-time'}</span>
`,sessionDone&&rtfArr.length&&(updateTrendDisplay(perfBackendSel.value,perfVoiceSel.value,avgRtf),perfHistoryAdd2({ts:Date.now(),backend:perfBackendSel.value,voice:perfVoiceSel.value,...perfHistoryExtraFields(perfVoiceSel.value,perfBackendSel.value),textLen:perfText.value.trim().length,avgLatencyMs:avg,minLatencyMs:minL,maxLatencyMs:maxL,avgRtf,runCount:ok.length,allOk:ok.length===perfRows.length}),renderPerfHistory2())}else perfSummary.innerHTML='<span class="perf-err">All runs failed</span>'}perfClearBtn.addEventListener("click",()=>{perfRows=[],renderPerfTable(),$("perf-trend-row")&&($("perf-trend-row").style.display="none"),perfProgress.style.display="none"}),perfRunBtn.addEventListener("click",async()=>{const backend=perfBackendSel.value,voice=perfVoiceSel.value,text=perfText.value.trim(),runs=parseInt(perfRunsSel.value)||3;if(!backend){toast("Select a backend first","error");return}if(!voice){toast("Fetch and select a voice first","error");return}if(!text){toast("Enter sample text","error");return}perfRows=[],perfRunBtn.disabled=!0,perfProgress.style.display="";for(let i=0;i<runs;i++){perfProgress.textContent=`Run ${i+1} / ${runs}\u2026`;const row={backend,voice,ok:!1,latencyMs:0,audioDuration:0,error:""};try{const t0=performance.now(),blob=await fetchTtsPreviewBlob2(voice,text,"wav","",backend);row.latencyMs=Math.round(performance.now()-t0),row.ok=!0;try{const audioCtx=new(window.AudioContext||window.webkitAudioContext),buf=await audioCtx.decodeAudioData(await blob.arrayBuffer());row.audioDuration=buf.duration,audioCtx.close()}catch{}}catch(e){row.error=e.message}perfRows.push(row),renderPerfTable(!1)}perfProgress.textContent=`Done \u2014 ${runs} run${runs>1?"s":""} completed.`,renderPerfTable(!0),perfRunBtn.disabled=!1}),(_a2=$("perf-history-filter-current"))==null||_a2.addEventListener("change",renderPerfHistory2),(_b=$("perf-history-clear-btn"))==null||_b.addEventListener("click",()=>{perfHistorySave([]),renderPerfHistory2(),toast("Benchmark history cleared","success")}),renderPerfHistory2()})(),Object.assign(root,{backendVoiceId:backendVoiceId2,shouldFilterBackendVoices:shouldFilterBackendVoices2,shouldReplaceWithLibraryVoices,activeLibraryVoiceIds:activeLibraryVoiceIds2,cleanReferenceText,selectedPreviewLibraryVoice,previewVoiceWarnings,updatePreviewVoiceMatchPanel:updatePreviewVoiceMatchPanel2,synthesizeSelectedReferenceText,_ttsIsFishBackend,FISH_EMOTION_TAGS,FISH_TAG_SAMPLE_TEXT,_fishInsertTag,_fishTagsRender,_fishTagsSync,_ttsApplyEmotionTag,_ttsStreamHealth,effectiveTtsPlaybackMode,isTtsStreamAvailable,createTtsStreamUrl,_ttsPreviewFetchWithRetry,_ttsBackendForVoice:_ttsBackendForVoice2,fetchTtsPreviewBlob:fetchTtsPreviewBlob2,_textWords,_textSimilarity,_voiceRoundtripCheck:_voiceRoundtripCheck2,createTtsAudioSource:createTtsAudioSource2,previewBlob:previewBlob2,PREVIEW_SAMPLE_TEXT,_onPreviewGenerated,_TRYOUT_SPEED_KEY,PERF_HISTORY_KEY,PERF_HISTORY_MAX,PERF_HISTORY_SORT,perfHistoryLoad:perfHistoryLoad2,perfHistorySave,perfHistoryAdd:perfHistoryAdd2,perfSparklineSvg,perfHistoryVoiceLookup,perfHistoryVoiceMeta,perfHistoryExtraFields,perfHistoryGenderLabel,perfHistoryDeviceLabel,perfHistoryDeviceHtml,perfHistoryAvatarHtml,perfHistorySortValue,perfHistoryCompare,perfHistoryHeadButton,renderPerfHistory:renderPerfHistory2}),root.TTVC=root.TTVC||{},root.TTVC.tts=root.TTVC.tts||{},Object.assign(root.TTVC.tts,{preview:{synthesize:synthesizeSelectedReferenceText,createAudio:createTtsAudioSource2},history:{load:perfHistoryLoad2,save:perfHistorySave,render:renderPerfHistory2}})}(window),function(root){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k;function _parseWavBytes(bytes){const v=new DataView(bytes.buffer,bytes.byteOffset,bytes.byteLength);let off=12,fmt=null,dataOff=0,dataSize=0;for(;off+8<=bytes.length;){const id=v.getUint32(off,!1),sz=v.getUint32(off+4,!0);id===1718449184?fmt={channels:v.getUint16(off+10,!0),sampleRate:v.getUint32(off+12,!0),bitDepth:v.getUint16(off+22,!0)}:id===1684108385&&(dataOff=off+8,dataSize=sz),off+=8+sz}return{fmt,dataOff,dataSize}}function _buildWavBlob(pcmBytes,fmt){const out=new Uint8Array(44+pcmBytes.length),dv=new DataView(out.buffer);return dv.setUint32(0,1380533830,!1),dv.setUint32(4,36+pcmBytes.length,!0),dv.setUint32(8,1463899717,!1),dv.setUint32(12,1718449184,!1),dv.setUint32(16,16,!0),dv.setUint16(20,1,!0),dv.setUint16(22,fmt.channels,!0),dv.setUint32(24,fmt.sampleRate,!0),dv.setUint32(28,fmt.sampleRate*fmt.channels*(fmt.bitDepth>>3),!0),dv.setUint16(32,fmt.channels*(fmt.bitDepth>>3),!0),dv.setUint16(34,fmt.bitDepth,!0),dv.setUint32(36,1684108385,!1),dv.setUint32(40,pcmBytes.length,!0),out.set(pcmBytes,44),new Blob([out],{type:"audio/wav"})}function _silencePcmBytes(ms,fmt){const bytesPerSample=fmt.bitDepth>>3,frames=Math.round(ms/1e3*fmt.sampleRate);return new Uint8Array(frames*fmt.channels*bytesPerSample)}async function mergeWavBlobs(blobs,gapMs=0){if(!blobs||blobs.length===0)return null;if(blobs.length===1&&!gapMs)return blobs[0];const parsed=[];for(const b of blobs){const bytes=new Uint8Array(await b.arrayBuffer()),p=_parseWavBytes(bytes);if(!p.fmt)throw new Error("Invalid WAV in chunk");parsed.push({bytes,...p})}const ref=parsed[0].fmt,silence=gapMs>0?_silencePcmBytes(gapMs,ref):null,totalPcm=parsed.reduce((s,p)=>s+p.dataSize,0)+(silence?silence.length*(parsed.length-1):0),out=new Uint8Array(totalPcm);let pos=0;return parsed.forEach((p,i)=>{out.set(p.bytes.slice(p.dataOff,p.dataOff+p.dataSize),pos),pos+=p.dataSize,silence&&i<parsed.length-1&&(out.set(silence,pos),pos+=silence.length)}),_buildWavBlob(out,ref)}function splitTextIntoChunks(text,maxLen=800){const abbrev=/\b(Mr|Mrs|Ms|Dr|Prof|Sr|Jr|vs|etc|e\.g|i\.e)\.\s/g,safe=text.replace(abbrev,m=>m.replace(".","\0")),restore=s=>s.replace(/\x00/g,"."),segments=[];for(const line of safe.split(`
`)){if(!line.trim()){segments.length&&segments.push("");continue}const sentences=line.match(/[^.!?]+[.!?]+\s*/g)||[],rest=line.replace(/[^.!?]+[.!?]+\s*/g,"").trim();segments.push(...sentences),rest&&segments.push(rest)}if(!segments.length)return[text];const chunks=[];let cur="",paraPending=!1;for(const seg of segments){if(seg===""){paraPending=!0;continue}const joined=cur?cur+(paraPending?`
`:" ")+seg.trim():seg.trim();joined.length>maxLen&&cur?(chunks.push(restore(cur.trim())),cur=seg.trim()):cur=joined,paraPending=!1}return cur.trim()&&chunks.push(restore(cur.trim())),chunks.length?chunks:[text]}async function generateChunkedTts2(voice,text,backend,instruct,extra=null,applyPersona=!1){const chunks=splitTextIntoChunks(text),prog=$("preview-chunk-progress");prog&&(prog.hidden=!1,prog.textContent=`Chunk 1 / ${chunks.length}\u2026`);const blobs=[];for(let i=0;i<chunks.length;i++)prog&&(prog.textContent=`Chunk ${i+1} / ${chunks.length}\u2026`),blobs.push(await fetchTtsPreviewBlob(voice,chunks[i],"wav",instruct,backend,applyPersona,extra));prog&&(prog.textContent="Merging\u2026");const merged=await mergeWavBlobs(blobs);return prog&&(prog.hidden=!0,prog.textContent=""),{url:URL.createObjectURL(merged),blob:merged,streaming:!1,label:"chunked"}}const _genHistory=[];function historyPush2(voice,text,backend,blob,url){const id=Date.now()+"-"+Math.random().toString(36).slice(2,6);_genHistory.unshift({id,ts:Date.now(),voice,text:text.slice(0,200),backend,blob,url}),_genHistory.length>20&&_genHistory.pop(),renderHistory()}function _histAvatarColor(id){const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<(id||"").length;i++)h=h*31+id.charCodeAt(i)>>>0;return palette[h%palette.length]}function renderHistory(){const list=$("history-list");if(list){if(!_genHistory.length){list.innerHTML='<div class="history-empty">No generations yet.</div>';return}list.innerHTML=_genHistory.map(item=>{const t=new Date(item.ts),ts=String(t.getHours()).padStart(2,"0")+":"+String(t.getMinutes()).padStart(2,"0"),preview=escHtml(item.text.length>90?item.text.slice(0,90)+"\u2026":item.text),v=(window._voices||[]).find(vx=>vx.id===item.voice),avatar=v!=null&&v.has_picture?`<img src="/api/voice/picture/${encodeURIComponent(item.voice)}" class="hist-avatar" alt="">`:`<span class="hist-avatar hist-avatar-init" style="background:${_histAvatarColor(item.voice)}">${(item.voice||"?")[0].toUpperCase()}</span>`;return`<div class="history-item" data-hid="${escHtml(item.id)}">
<div class="history-item-row1">
${avatar}
<span class="history-voice">${escHtml(item.voice)}</span>
<span class="history-backend">${escHtml(item.backend)}</span>
<span class="history-time">${ts}</span>
<button class="hist-del-btn" title="Delete"><span class="mdi mdi-close"></span></button>
</div>
<div class="history-item-text">${preview}</div>
<div class="history-item-actions">
<button class="btn-secondary btn-xs hist-play-btn" ${item.blob?"":"disabled"}><span class="mdi mdi-play"></span> Play</button>
<button class="btn-secondary btn-xs hist-reuse-btn"><span class="mdi mdi-restore"></span> Reuse</button>
<button class="btn-secondary btn-xs hist-playlist-btn" ${item.blob?"":"disabled"}><span class="mdi mdi-plus"></span> Playlist</button>
</div>
</div>`}).join(""),list.querySelectorAll(".hist-del-btn").forEach(btn=>{btn.addEventListener("click",e=>{var _a2;e.stopPropagation();const hid=(_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid,idx=_genHistory.findIndex(h=>h.id===hid);idx!==-1&&(_genHistory.splice(idx,1),renderHistory())})}),list.querySelectorAll(".hist-play-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});if(!(item!=null&&item.url))return;const audio=$("preview-audio");audio.src=item.url,audio.style.display="",audio.play().catch(()=>{})})}),list.querySelectorAll(".hist-reuse-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});if(!item)return;$("preview-text-area").value=item.text;const bSel=$("tts-backend-select");bSel&&[...bSel.options].forEach(o=>{o.value===item.backend&&(bSel.value=item.backend)});const vSel=$("tts-voice-select");vSel&&[...vSel.options].forEach(o=>{o.value===item.voice&&(vSel.value=item.voice)}),toast("Settings restored from history","success")})}),list.querySelectorAll(".hist-playlist-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});item!=null&&item.blob&&playlistAdd(item.voice,item.text,item.blob,item.url)})})}}(_a=$("history-clear-btn"))==null||_a.addEventListener("click",()=>{_genHistory.length=0,renderHistory(),toast("History cleared","success")});const _playlist=[];function playlistAdd(voice,text,blob,url){const id="pl-"+Date.now()+"-"+Math.random().toString(36).slice(2,5);_playlist.push({id,voice,text:text.slice(0,120),blob,url}),renderPlaylist(),$("playlist-export-btn")&&($("playlist-export-btn").disabled=!1),toast("Added to playlist","success")}function renderPlaylist(){const list=$("playlist-list");if(list){if(!_playlist.length){list.innerHTML='<div class="history-empty">No clips in playlist. Use <strong>+ Playlist</strong> after generating.</div>',$("playlist-export-btn")&&($("playlist-export-btn").disabled=!0);return}list.innerHTML=_playlist.map((item,i)=>`
<div class="playlist-item" data-pid="${escHtml(item.id)}">
<span class="playlist-num">${i+1}</span>
<div class="playlist-info">
<span class="history-voice">${escHtml(item.voice)}</span>
<span class="playlist-text">${escHtml(item.text.length>70?item.text.slice(0,70)+"\u2026":item.text)}</span>
</div>
<div class="playlist-item-actions">
<button class="btn-secondary btn-xs pl-up" ${i===0?"disabled":""}><span class="mdi mdi-upload"></span></button>
<button class="btn-secondary btn-xs pl-dn" ${i===_playlist.length-1?"disabled":""}><span class="mdi mdi-download"></span></button>
<button class="btn-secondary btn-xs pl-rm"><span class="mdi mdi-close"></span></button>
</div>
</div>`).join(""),list.querySelectorAll(".pl-rm").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx>=0&&(_playlist.splice(idx,1),renderPlaylist())})}),list.querySelectorAll(".pl-up").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx>0&&([_playlist[idx-1],_playlist[idx]]=[_playlist[idx],_playlist[idx-1]],renderPlaylist())})}),list.querySelectorAll(".pl-dn").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx<_playlist.length-1&&([_playlist[idx],_playlist[idx+1]]=[_playlist[idx+1],_playlist[idx]],renderPlaylist())})})}}(_b=$("add-to-playlist-btn"))==null||_b.addEventListener("click",()=>{var _a2,_b2,_c2;if(!previewBlob){toast("Generate audio first","error");return}playlistAdd(window._previewVoice||((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"",window._previewText||((_b2=$("preview-text-area"))==null?void 0:_b2.value)||"",previewBlob,((_c2=$("preview-audio"))==null?void 0:_c2.src)||"")}),(_c=$("playlist-export-btn"))==null||_c.addEventListener("click",async()=>{if(!_playlist.length)return;const btn=$("playlist-export-btn"),orig=btn.textContent;btn.disabled=!0,btn.textContent="Merging\u2026";try{const merged=await mergeWavBlobs(_playlist.map(p=>p.blob).filter(Boolean));if(!merged)throw new Error("No audio to export");const a=document.createElement("a");a.href=URL.createObjectURL(merged),a.download="playlist_"+Date.now()+".wav",a.click(),toast("Playlist exported as WAV","success")}catch(e){toast("Export failed: "+e.message,"error")}finally{btn.disabled=_playlist.length===0,btn.textContent=orig}}),(_d=$("playlist-clear-btn"))==null||_d.addEventListener("click",()=>{_playlist.length=0,renderPlaylist(),toast("Playlist cleared","success")});const _FX_PRESETS={studio:{reverb:{on:!0,room_size:.6,wet:.35},compressor:{on:!0,threshold_db:-24,ratio:3}},broadcast:{compressor:{on:!0,threshold_db:-20,ratio:6}},telephone:{compressor:{on:!0,threshold_db:-18,ratio:8},bandpass:{on:!0,low:300,high:3400}},warm:{reverb:{on:!0,room_size:.2,wet:.15},compressor:{on:!0,threshold_db:-26,ratio:2}},radio:{compressor:{on:!0,threshold_db:-22,ratio:5},bandpass:{on:!0,low:150,high:5500}}};function fxSliderBind(sliderId,labelId,fmt){const s=$(sliderId),l=$(labelId);if(!s||!l)return;const upd=()=>{l.textContent=fmt(s.value)};upd(),s.addEventListener("input",upd)}fxSliderBind("fx-reverb-room","fx-reverb-room-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-reverb-wet","fx-reverb-wet-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-comp-thresh","fx-comp-thresh-val",v=>v+" dB"),fxSliderBind("fx-comp-ratio","fx-comp-ratio-val",v=>v+":1"),fxSliderBind("fx-chorus-rate","fx-chorus-rate-val",v=>parseFloat(v).toFixed(1)+" Hz"),fxSliderBind("fx-chorus-mix","fx-chorus-mix-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-pitch-semi","fx-pitch-semi-val",v=>(parseFloat(v)>=0?"+":"")+v+" st"),fxSliderBind("fx-bandpass-low","fx-bandpass-low-val",v=>Math.round(v)+" Hz"),fxSliderBind("fx-bandpass-high","fx-bandpass-high-val",v=>Math.round(v)+" Hz"),(_e=$("effects-preset"))==null||_e.addEventListener("change",()=>{const preset=_FX_PRESETS[$("effects-preset").value];preset&&(["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-on","fx-bandpass-on"].forEach(id=>{const el=$(id);el&&(el.checked=!1)}),preset.reverb&&($("fx-reverb-on").checked=!!preset.reverb.on,preset.reverb.room_size!=null&&($("fx-reverb-room").value=preset.reverb.room_size),preset.reverb.wet!=null&&($("fx-reverb-wet").value=preset.reverb.wet)),preset.compressor&&($("fx-compressor-on").checked=!!preset.compressor.on,preset.compressor.threshold_db!=null&&($("fx-comp-thresh").value=preset.compressor.threshold_db),preset.compressor.ratio!=null&&($("fx-comp-ratio").value=preset.compressor.ratio)),preset.bandpass&&($("fx-bandpass-on").checked=!!preset.bandpass.on,preset.bandpass.low!=null&&($("fx-bandpass-low").value=preset.bandpass.low),preset.bandpass.high!=null&&($("fx-bandpass-high").value=preset.bandpass.high)),["fx-reverb-room","fx-reverb-wet","fx-comp-thresh","fx-comp-ratio","fx-chorus-rate","fx-chorus-mix","fx-pitch-semi","fx-bandpass-low","fx-bandpass-high"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))}))}),(_f=$("effects-reset-btn"))==null||_f.addEventListener("click",()=>{$("effects-preset").value="",["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-on","fx-bandpass-on"].forEach(id=>{const el=$(id);el&&(el.checked=!1)}),$("fx-bandpass-low").value="300",$("fx-bandpass-high").value="3400",$("fx-reverb-room").value="0.35",$("fx-reverb-wet").value="0.25",$("fx-comp-thresh").value="-20",$("fx-comp-ratio").value="4",$("fx-chorus-rate").value="1",$("fx-chorus-mix").value="0.5",$("fx-pitch-semi").value="0",["fx-reverb-room","fx-reverb-wet","fx-comp-thresh","fx-comp-ratio","fx-chorus-rate","fx-chorus-mix","fx-pitch-semi","fx-bandpass-low","fx-bandpass-high"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))})});let _effectsSourceBlob=null;(_g=$("effects-apply-btn"))==null||_g.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2;const chain=[];if((_a2=$("fx-reverb-on"))!=null&&_a2.checked&&chain.push({type:"reverb",params:{room_size:+$("fx-reverb-room").value,wet:+$("fx-reverb-wet").value,dry:1-+$("fx-reverb-wet").value}}),(_b2=$("fx-compressor-on"))!=null&&_b2.checked&&chain.push({type:"compressor",params:{threshold_db:+$("fx-comp-thresh").value,ratio:+$("fx-comp-ratio").value}}),(_c2=$("fx-chorus-on"))!=null&&_c2.checked&&chain.push({type:"chorus",params:{rate_hz:+$("fx-chorus-rate").value,mix:+$("fx-chorus-mix").value}}),(_d2=$("fx-pitch-on"))!=null&&_d2.checked&&chain.push({type:"pitch_shift",params:{semitones:+$("fx-pitch-semi").value}}),(_e2=$("fx-bandpass-on"))!=null&&_e2.checked&&(chain.push({type:"highpass",params:{cutoff_hz:+$("fx-bandpass-low").value}}),chain.push({type:"lowpass",params:{cutoff_hz:+$("fx-bandpass-high").value}})),!chain.length){toast("Enable at least one effect","error");return}const btn=$("effects-apply-btn"),st=$("effects-status");btn.disabled=!0,st&&(st.textContent="Processing\u2026");try{let blob=previewBlob||_effectsSourceBlob;if(!blob){const args=window._effectsSynthArgs;if(!args||typeof fetchTtsPreviewBlob!="function"){toast("Generate audio first","error");return}st&&(st.textContent="Synthesizing audio\u2026"),blob=await fetchTtsPreviewBlob(args.voice,args.text,"wav",args.instruct,args.backend)}const fd=new FormData;fd.append("audio",blob,"audio.wav"),fd.append("effects",JSON.stringify(chain));const resp=await fetch("/api/audio/effects",{method:"POST",body:fd});if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.detail||resp.statusText)}const out=await resp.blob();_effectsSourceBlob||(_effectsSourceBlob=previewBlob),previewBlob=out;const audio=$("preview-audio");audio.src=URL.createObjectURL(out),audio.style.display="",audio.play().catch(()=>{}),st&&(st.textContent="Applied."),toast("Effects applied","success")}catch(e){st&&(st.textContent=""),toast("Effects failed: "+e.message,"error")}finally{btn.disabled=!1}});let _refineOriginal=null;(function(){const inp=$("refine-llm-url");if(!inp)return;const saved=_appSettings&&_appSettings.refine_llm_url||localStorage.getItem("refine-llm-url");saved?inp.value=saved:inp.value=_appSettings&&_appSettings.llm_url||_appSettings&&_appSettings.engine_local_urls&&_appSettings.engine_local_urls.ollama||localStorage.getItem("llm-local-url-ollama")||"http://localhost:11434/v1",inp.addEventListener("input",()=>{localStorage.setItem("refine-llm-url",inp.value),_patchSettings({refine_llm_url:inp.value}),_appSettings&&(_appSettings.refine_llm_url=inp.value)})})();function updateRefineButtonState2(){var _a2,_b2;const btn=$("refine-btn");btn&&(btn.disabled=!((_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)!=null&&_b2.trim()))}(_h=$("stt-tts-text"))==null||_h.addEventListener("input",updateRefineButtonState2),(_i=$("refine-btn"))==null||_i.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l,_m,_n,_o,_p;const text=(_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)==null?void 0:_b2.trim();if(!text)return;const btn=$("refine-btn"),st=$("refine-status");btn.disabled=!0,st&&(st.textContent="Refining\u2026");try{const r=await fetch("/api/refine-text",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,llm_url:((_d2=(_c2=$("refine-llm-url"))==null?void 0:_c2.value)==null?void 0:_d2.trim())||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",model:((_f2=(_e2=$("refine-model"))==null?void 0:_e2.value)==null?void 0:_f2.trim())||(_appSettings==null?void 0:_appSettings.refine_model)||(_appSettings==null?void 0:_appSettings.llm_model)||"",toggles:{fillers:(_h2=(_g2=$("refine-fillers"))==null?void 0:_g2.checked)!=null?_h2:!0,repetitions:(_j2=(_i2=$("refine-repetitions"))==null?void 0:_i2.checked)!=null?_j2:!0,corrections:(_l=(_k2=$("refine-corrections"))==null?void 0:_k2.checked)!=null?_l:!0,punctuation:(_n=(_m=$("refine-punctuation"))==null?void 0:_m.checked)!=null?_n:!0}})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_refineOriginal=text,$("stt-tts-text").value=d.text,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!1),st&&(st.textContent="Done."),toast("Transcription refined","success")}catch(e){st&&(st.textContent=""),toast("Refinement failed: "+e.message,"error")}finally{btn.disabled=!((_p=(_o=$("stt-tts-text"))==null?void 0:_o.value)!=null&&_p.trim())}}),(_j=$("refine-restore-btn"))==null||_j.addEventListener("click",()=>{_refineOriginal&&($("stt-tts-text").value=_refineOriginal,_refineOriginal=null,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!0),$("refine-status")&&($("refine-status").textContent=""),toast("Original transcription restored","success"))}),(_k=$("preview-style-instruction"))==null||_k.addEventListener("input",()=>{typeof updateBackendHelp=="function"&&updateBackendHelp()}),Object.assign(root,{_parseWavBytes,_buildWavBlob,_silencePcmBytes,mergeWavBlobs,splitTextIntoChunks,generateChunkedTts:generateChunkedTts2,_genHistory,historyPush:historyPush2,_histAvatarColor,renderHistory,_playlist,playlistAdd,renderPlaylist,_FX_PRESETS,fxSliderBind,_effectsSourceBlob,_refineOriginal,updateRefineButtonState:updateRefineButtonState2}),root.TTVC=root.TTVC||{},root.TTVC.generation=root.TTVC.generation||{},Object.assign(root.TTVC.generation,{audio:{merge:mergeWavBlobs,chunkedTts:generateChunkedTts2},text:{splitChunks:splitTextIntoChunks},history:{render:renderHistory,push:historyPush2},playlist:{add:playlistAdd,render:renderPlaylist}})}(window),function(){var _a;const batchBackendSel=$("batch-backend-select"),batchRunsSel=$("batch-runs"),batchLoadBtn=$("batch-load-voices-btn"),batchSearchInput=$("batch-voice-search"),batchSelectAllBtn=$("batch-select-all-btn"),batchSelectNoneBtn=$("batch-select-none-btn"),batchVoiceList=$("batch-voice-list"),batchSelCount=$("batch-selected-count"),batchRunBtn=$("batch-run-btn"),batchStopBtn=$("batch-stop-btn"),perfRunSelectedBtn=$("perf-run-selected-btn"),batchProgress=$("batch-progress"),batchProgLabel=$("batch-progress-label"),batchProgCount=$("batch-progress-count"),batchProgBar=$("batch-progress-bar"),batchResultsCard=$("batch-results-card"),batchResultsLabel=$("batch-results-label"),batchTbody=$("batch-tbody");if(!batchRunBtn)return;let batchStopped=!1,batchResults=[],batchVoices=[],batchSelected=new Set,batchSortField="factor",batchSortDir=-1;function populateBatchBackends(){if(!batchBackendSel)return;const cur=batchBackendSel.value;batchBackendSel.innerHTML=availableTtsBackends().map(b=>`<option value="${escHtml(b.id)}"${b.id===cur?" selected":""}>${escHtml(b.label)}</option>`).join("")||'<option value="">No backends available</option>'}populateBatchBackends();function libraryVoice(id){return(window._voices||[]).find(v=>v&&v.id===id)||null}function normalizeBatchVoice(value){const id=typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||(value==null?void 0:value.voice_id)||"",meta=typeof value=="object"?value.meta||libraryVoice(id)||value:libraryVoice(id)||null;return id?{id,label:(meta==null?void 0:meta.display_name)||(value==null?void 0:value.label)||(meta==null?void 0:meta.name)||(value==null?void 0:value.name)||id,meta}:null}function batchVoiceStats(v){var _a2,_b;const meta=(v==null?void 0:v.meta)||libraryVoice(v==null?void 0:v.id)||{},b=meta.benchmark||{},parts=[],dur=Number(meta.duration),speed=Number((_a2=b.speed)!=null?_a2:b.avg_speed),rtf=Number((_b=b.rtf)!=null?_b:b.avg_rtf);return Number.isFinite(dur)&&dur>0&&parts.push(`${dur.toFixed(1)}s`),Number.isFinite(speed)&&speed>0?parts.push(`${speed.toFixed(2)}x`):Number.isFinite(rtf)&&rtf>0&&parts.push(`RTF ${rtf.toFixed(2)}`),parts.join(" \xB7 ")}function batchColor(id){const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i<String(id||"").length;i++)h=h*31+String(id).charCodeAt(i)>>>0;return palette[h%palette.length]}function batchAvatar(v){const meta=(v==null?void 0:v.meta)||libraryVoice(v==null?void 0:v.id)||{};if(meta.has_picture)return`<img class="batch-voice-avatar" src="/api/voice/picture/${encodeURIComponent(v.id)}" alt="">`;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,28):null;if(icon)return icon.replace(/vp-avatar/g,"batch-voice-avatar");const init=(v.label||v.id||"?")[0].toUpperCase();return`<span class="batch-voice-avatar batch-voice-avatar-init" style="background:${batchColor(meta.lang||v.id)}">${escHtml(init)}</span>`}function batchHistoryExtraFields(voice,backend){const item=batchVoices.find(v=>v.id===voice)||{},meta=item.meta||libraryVoice(voice)||{};return{voiceMeta:{label:meta.display_name||meta.name||item.label||voice,lang:meta.lang||meta.language||"",gender:meta.gender||"",flag:meta.flag||"",avatar:meta.avatar||"",has_picture:!!meta.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function batchSearchText(v){const meta=v.meta||{};return[v.id,v.label,meta.display_name,meta.name,meta.lang,meta.language,Array.isArray(meta.tags)?meta.tags.join(" "):meta.tags,batchVoiceStats(v)].filter(Boolean).join(" ").toLowerCase()}function syncRunSelectedLabel(){const count=batchSelected.size;perfRunSelectedBtn&&(perfRunSelectedBtn.disabled=count===0,perfRunSelectedBtn.innerHTML=`<span class="mdi mdi-playlist-play"></span> ${count?`Run ${count} selected voice${count===1?"":"s"}`:"Run selected voices"}`)}function updateSelCount(){const total=batchVoices.length,checked=batchSelected.size,visible=filteredBatchVoices().length;batchSelCount&&(batchSelCount.textContent=total?`${checked} of ${total} selected${visible!==total?` \xB7 ${visible} shown`:""}`:""),batchRunBtn.disabled=checked===0,syncRunSelectedLabel()}function filteredBatchVoices(){const q=((batchSearchInput==null?void 0:batchSearchInput.value)||"").trim().toLowerCase();return q?batchVoices.filter(v=>batchSearchText(v).includes(q)):batchVoices}function renderBatchVoiceList(){if(!batchVoiceList)return;const visible=filteredBatchVoices();if(!batchVoices.length){batchVoiceList.innerHTML='<div class="perf-history-empty">No voices found. Fetch voices above or reload from backend.</div>',updateSelCount();return}if(!visible.length){batchVoiceList.innerHTML='<div class="perf-history-empty">No matching voices.</div>',updateSelCount();return}const activeIds=new Set(activeVoiceIds());batchVoiceList.innerHTML=visible.map(v=>{const checked=batchSelected.has(v.id),isActive=activeIds.has(v.id),stats=batchVoiceStats(v),meta=v.meta||libraryVoice(v.id)||{},flag=meta.flag||"",lang=meta.lang||meta.language||"",gender={M:"\u2642",F:"\u2640",N:"\u26AC"}[meta.gender]||"",langGender=[flag||lang,gender].filter(Boolean).join(" ");return`<label class="batch-voice-item${isActive?" is-active":""}${checked?" is-selected":""}">
<input type="checkbox" class="batch-voice-cb" value="${escHtml(v.id)}" aria-label="Benchmark ${escHtml(v.label||v.id)}"${checked?" checked":""}>
${batchAvatar(v)}
<span class="batch-voice-main"><span class="batch-voice-name">${escHtml(v.label||v.id)}</span><span class="batch-voice-id">${escHtml(v.id)}</span></span>
${langGender?`<span class="batch-voice-meta">${escHtml(langGender)}</span>`:""}
${stats?`<span class="batch-voice-stats">${escHtml(stats)}</span>`:""}
${isActive?'<span class="batch-voice-tag">active</span>':""}
</label>`}).join(""),batchVoiceList.querySelectorAll(".batch-voice-cb").forEach(cb=>cb.addEventListener("change",()=>{cb.checked?batchSelected.add(cb.value):batchSelected.delete(cb.value),renderBatchVoiceList()})),updateSelCount()}function buildVoiceList(voices,opts={}){const previous=opts.preserve?new Set(batchSelected):null,byId=new Map;if((voices||[]).map(normalizeBatchVoice).filter(Boolean).forEach(v=>byId.set(v.id,v)),batchVoices=Array.from(byId.values()).sort((a,b)=>(a.label||a.id).localeCompare(b.label||b.id)),previous)batchSelected=new Set(batchVoices.filter(v=>previous.has(v.id)).map(v=>v.id));else if(opts.keepSelection)batchSelected=new Set(Array.from(batchSelected).filter(id=>byId.has(id)));else{const activeIds=new Set(activeVoiceIds());batchSelected=new Set(batchVoices.filter(v=>activeIds.has(v.id)).map(v=>v.id))}renderBatchVoiceList()}async function populateFromLibrary(){try{(!window._voices||!window._voices.length)&&typeof loadVoiceLibrary=="function"&&await loadVoiceLibrary();const voices=(window._voices||[]).filter(v=>v.enabled!==!1).map(v=>({id:v.id,label:v.display_name||v.name||v.id,meta:v}));buildVoiceList(voices)}catch(e){batchVoiceList&&(batchVoiceList.innerHTML=`<div class="perf-history-empty">Voice library unavailable: ${escHtml(e.message)}</div>`),updateSelCount()}}window.populateBatchBenchmarkFromLibrary=populateFromLibrary;async function loadBatchVoicesFromBackend(){const backend=batchBackendSel.value;if(!backend){toast("Select a backend first","error");return}batchLoadBtn.disabled=!0,batchVoiceList.innerHTML='<div class="perf-history-empty">Loading from backend...</div>';try{const voices=(await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json())).map(v=>{const id=backendVoiceId(v);return{id,label:id,meta:libraryVoice(id)||(typeof v=="object"?v:null)}}).filter(v=>v.id);buildVoiceList(voices)}catch(e){batchVoiceList.innerHTML=`<div class="perf-history-empty">Failed: ${escHtml(e.message)}</div>`,updateSelCount()}finally{batchLoadBtn.disabled=!1}}batchLoadBtn.addEventListener("click",loadBatchVoicesFromBackend),batchSearchInput==null||batchSearchInput.addEventListener("input",renderBatchVoiceList),batchSelectAllBtn.addEventListener("click",()=>{batchVoices.forEach(v=>batchSelected.add(v.id)),renderBatchVoiceList()}),batchSelectNoneBtn.addEventListener("click",()=>{batchSelected.clear(),renderBatchVoiceList()}),window.addEventListener("benchmark:tts-voices-fetched",e=>{const detail=e.detail||{};detail.backend&&batchBackendSel&&(batchBackendSel.value=detail.backend),buildVoiceList(detail.voices||[])});function batchSortValue(r,field){const factor=r.ok&&r.avgRtf>0?1/r.avgRtf:null;switch(field){case"voice":return(r.voice||"").toLowerCase();case"lang":return(r.flag||r.lang||"").toLowerCase();case"gender":return(r.gender||"").toLowerCase();case"device":return typeof backendComputeDevice=="function"?backendComputeDevice(r.backend||"").toLowerCase():"";case"latency":return r.ok?r.avgLatency:999999;case"best":return r.ok?r.minLatency:999999;case"duration":return r.ok&&r.avgAudio>0?r.avgAudio:999999;case"factor":return factor!=null?factor:-1;case"time":return r.ok?r.avgLatency/1e3:999999;case"wpm":return r.ok&&r.avgAudio>0&&r.wordCount>0?Math.round(r.wordCount/(r.avgAudio/60)):-1;default:return 0}}function renderBatchResults(){if(!batchResults.length){batchResultsCard.style.display="none";return}batchResultsCard.style.display="";const sorted=batchResults.slice().sort((a,b)=>{if(a.ok&&!b.ok)return-1;if(!a.ok&&b.ok)return 1;const av=batchSortValue(a,batchSortField),bv=batchSortValue(b,batchSortField);return typeof av=="string"?av.localeCompare(bv)*batchSortDir:(av-bv)*batchSortDir}),okCount=batchResults.filter(r=>r.ok).length;batchResultsLabel&&(batchResultsLabel.textContent=`${okCount} / ${batchResults.length} voices \u2014 ${batchBackendSel.value}`),document.querySelectorAll("#batch-table th[data-sort]").forEach(th=>{th.classList.remove("perf-sort-asc","perf-sort-desc"),th.dataset.sort===batchSortField&&th.classList.add(batchSortDir===1?"perf-sort-asc":"perf-sort-desc")}),batchTbody.innerHTML=sorted.map(r=>{const factor=r.ok&&r.avgRtf>0?1/r.avgRtf:null,timeSec=r.ok&&r.avgLatency>0?r.avgLatency/1e3:null,wpm=r.ok&&r.avgAudio>0&&r.wordCount>0?Math.round(r.wordCount/(r.avgAudio/60)):null,factorCls=factor==null?"":factor>=1?"perf-good":"perf-slow",trend=r.trend?`<span class="perf-trend-badge ${r.trend.cls}" style="font-size:11px;padding:1px 6px">${r.trend.label}</span>`:"",device=typeof backendComputeDevice=="function"?backendComputeDevice(r.backend||batchBackendSel.value):"Unknown",deviceCls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass(r.backend||batchBackendSel.value):"",genderMap={M:"\u2642",F:"\u2640",N:"\u26AC"};return`<tr class="${r.ok?"":"perf-row-error"}">
<td>${escHtml(r.voice)}</td>
<td>${escHtml(r.flag||r.lang||"\u2014")}</td>
<td>${escHtml(genderMap[r.gender]||"\u2014")}</td>
<td><span class="bench-device ${deviceCls}" title="Inferred from the selected TTS backend metadata">${escHtml(device)}</span></td>
<td>${r.ok?Math.round(r.avgLatency)+" ms":"\u2014"}</td>
<td>${r.ok?r.minLatency+" ms":"\u2014"}</td>
<td>${r.ok&&r.avgAudio>0?r.avgAudio.toFixed(1)+"s":"\u2014"}</td>
<td class="${factorCls}">${factor!=null?factor.toFixed(2)+"\xD7":"\u2014"}${trend}</td>
<td>${timeSec!=null?timeSec.toFixed(1)+"s":"\u2014"}</td>
<td>${wpm!=null?wpm+" wpm":"\u2014"}</td>
<td>${r.ok?'<span class="perf-ok">OK</span>':`<span class="perf-err">${escHtml(r.error||"Failed")}</span>`}</td>
</tr>`}).join("")}(_a=document.querySelector("#batch-table"))==null||_a.addEventListener("click",e=>{const th=e.target.closest("th[data-sort]");if(!th)return;const field=th.dataset.sort;field===batchSortField?batchSortDir*=-1:(batchSortField=field,batchSortDir=field==="voice"||field==="lang"||field==="gender"?1:-1),renderBatchResults()});async function runBatchBenchmark(){var _a2,_b,_c,_d,_e;const topBackend=((_a2=$("perf-backend-select"))==null?void 0:_a2.value)||"";topBackend&&batchBackendSel&&batchBackendSel.value!==topBackend&&(batchBackendSel.value=topBackend);const topRuns=((_b=$("perf-runs"))==null?void 0:_b.value)||"";topRuns&&batchRunsSel&&Array.from(batchRunsSel.options).some(o=>o.value===topRuns)&&(batchRunsSel.value=topRuns);const singleVoice=((_c=$("perf-voice-select"))==null?void 0:_c.value)||"";!batchSelected.size&&singleVoice&&batchVoices.some(v=>v.id===singleVoice)&&(batchSelected.add(singleVoice),renderBatchVoiceList());const backend=batchBackendSel.value,text=(_d=$("perf-text"))==null?void 0:_d.value.trim(),runs=parseInt(batchRunsSel.value)||1,validIds=new Set(batchVoices.map(v=>v.id)),selected=Array.from(batchSelected).filter(id=>validIds.has(id));if(!backend){toast("Select a backend first","error");return}if(!text){toast("Enter sample text in the single-voice form above","error");return}if(!selected.length){toast("Select one or more voices first","error");return}batchStopped=!1,batchResults=[],batchRunBtn.disabled=!0,perfRunSelectedBtn&&(perfRunSelectedBtn.disabled=!0),batchStopBtn.disabled=!1,batchProgress.style.display="",batchResultsCard.style.display="none";for(let vi=0;vi<selected.length&&!batchStopped;vi++){const voice=selected[vi];batchProgLabel.textContent=`${voice} (${vi+1} / ${selected.length})`,batchProgCount.textContent=`${vi+1} / ${selected.length}`,batchProgBar.style.width=`${Math.round(vi/selected.length*100)}%`;const _vm=batchVoices.find(bv=>bv.id===voice),_meta=(_vm==null?void 0:_vm.meta)||libraryVoice(voice)||{},entry={voice,backend,ok:!1,avgLatency:0,minLatency:0,avgRtf:0,avgAudio:0,wordCount:text?text.trim().split(/\s+/).length:0,lang:_meta.lang||_meta.language||"",flag:_meta.flag||"",gender:_meta.gender||"",error:""},rowRunResults=[];for(let ri=0;ri<runs&&!batchStopped;ri++)try{const t0=performance.now(),blob=await fetchTtsPreviewBlob(voice,text,"wav","",backend),lat=Math.round(performance.now()-t0);let dur=0;try{const ac=new(window.AudioContext||window.webkitAudioContext);dur=(await ac.decodeAudioData(await blob.arrayBuffer())).duration,ac.close()}catch{}rowRunResults.push({lat,dur})}catch(e){entry.error=e.message;break}if(rowRunResults.length){entry.ok=!0,entry.avgLatency=rowRunResults.reduce((sum,r)=>sum+r.lat,0)/rowRunResults.length,entry.minLatency=Math.min(...rowRunResults.map(r=>r.lat));const durRows=rowRunResults.filter(r=>r.dur>0);entry.avgAudio=durRows.length?durRows.reduce((sum,r)=>sum+r.dur,0)/durRows.length:0;const rtfArr=durRows.map(r=>r.lat/1e3/r.dur);if(entry.avgRtf=rtfArr.length?rtfArr.reduce((sum,value)=>sum+value,0)/rtfArr.length:0,entry.avgRtf>0){const prev=perfHistoryLoad().filter(e=>e.backend===backend&&e.voice===voice&&typeof e.avgRtf=="number");if(prev.length){const prevRtf=prev[prev.length-1].avgRtf,delta=entry.avgRtf-prevRtf,pct=Math.abs(delta/Math.max(prevRtf,.01))*100;pct<5?entry.trend={cls:"perf-trend-stable",label:"stable"}:delta<0?entry.trend={cls:"perf-trend-better",label:`${pct.toFixed(0)}% faster`}:entry.trend={cls:"perf-trend-worse",label:`${pct.toFixed(0)}% slower`}}perfHistoryAdd({ts:Date.now(),backend,voice,textLen:text.length,...batchHistoryExtraFields(voice,backend),avgLatencyMs:entry.avgLatency,minLatencyMs:entry.minLatency,maxLatencyMs:Math.max(...rowRunResults.map(r=>r.lat)),avgRtf:entry.avgRtf,runCount:rowRunResults.length,allOk:!0})}}batchResults.push(entry),renderBatchResults()}batchProgBar.style.width="100%",batchProgLabel.textContent=batchStopped?`Stopped after ${batchResults.length} voice${batchResults.length!==1?"s":""}.`:`Done - ${batchResults.length} voice${batchResults.length!==1?"s":""} benchmarked.`,batchStopBtn.disabled=!0,batchRunBtn.disabled=!1,syncRunSelectedLabel(),renderPerfHistory();const ok=batchResults.filter(r=>r.ok),best=ok.slice().sort((a,b)=>a.avgRtf-b.avgRtf)[0];toast(`Batch done: ${ok.length}/${batchResults.length} OK`+(best?`, best RTF ${best.avgRtf.toFixed(2)} (${best.voice})`:""),ok.length<batchResults.length?"error":"success");const syncable=batchResults.filter(r=>r.ok&&r.avgRtf>0&&r.avgAudio>0);if(syncable.length&&typeof window.loadVoiceLibrary=="function"){const benchText=(((_e=$("perf-text"))==null?void 0:_e.value)||"").trim();await Promise.allSettled(syncable.map(r=>fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:r.voice,benchmark:{ok:!0,text:benchText,elapsed_sec:r.avgLatency/1e3,audio_sec:r.avgAudio,speed:1/r.avgRtf,rtf:r.avgRtf,ttfa_ms:r.minLatency}})}))),await window.loadVoiceLibrary()}}batchRunBtn.addEventListener("click",runBatchBenchmark),perfRunSelectedBtn==null||perfRunSelectedBtn.addEventListener("click",runBatchBenchmark),batchStopBtn.addEventListener("click",()=>{batchStopped=!0,batchStopBtn.disabled=!0,batchProgLabel.textContent="Stopping after current voice..."})}(),function(){let initialized=!1,benchmarkDataLoaded=!1,sttEngines=[],turnHistoryCount=0;const q=id=>document.getElementById(id),fmtMs=ms=>ms==null?"\u2014":ms>=1e3?(ms/1e3).toFixed(2)+"s":Math.round(ms)+"ms",fmtSec=sec=>sec==null?"\u2014":Number(sec).toFixed(Number(sec)<10?2:1)+"s",esc=s=>typeof escHtml=="function"?escHtml(s):String(s!=null?s:"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"})[c]),say=(msg,type="")=>typeof toast=="function"?toast(msg,type):console.log(msg);function setBenchTab(name){document.querySelectorAll(".bench-tab").forEach(btn=>btn.classList.toggle("active",btn.dataset.benchTab===name)),document.querySelectorAll(".bench-pane").forEach(pane=>pane.classList.toggle("active",pane.dataset.benchPane===name))}async function fetchJson(url,opts){const r=await fetch(url,opts);if(!r.ok){let detail=r.statusText||`HTTP ${r.status}`;try{const d=await r.json();detail=typeof d.detail=="string"?d.detail:JSON.stringify(d.detail||d)}catch{try{detail=await r.text()}catch{}}throw new Error(detail||`HTTP ${r.status}`)}return r.json()}function dedupeEngines(items){const seen=new Set,out=[];for(const item of items){const key=`${item.url||""}|${item.model||""}|${item.id||item.label||""}`;seen.has(key)||!item.url||(seen.add(key),out.push(item))}return out}function benchmarkModelList(engine){const values=[],add=v=>{const s=String(v||"").trim();s&&!values.includes(s)&&values.push(s)};return add(engine.model),(engine.models||[]).forEach(add),values.length||add("whisper-1"),values}async function fetchBenchmarkModels(engine){if(!(engine!=null&&engine.url))return[];try{const d=await fetchJson("/api/engine-models?type=stt&url="+encodeURIComponent(engine.url));return Array.isArray(d.models)?d.models.filter(Boolean):[]}catch(e){return console.warn("[benchmark models]",engine.label||engine.id,e),[]}}async function hydrateBenchmarkModels(engines){await Promise.all(engines.map(async engine=>{const found=await fetchBenchmarkModels(engine);engine.models=benchmarkModelList({...engine,models:found.length?found:engine.models}),engine.models.includes(engine.model)||(engine.model=engine.models[0]||engine.model||"whisper-1")}))}function renderBenchmarkModelOptions(index,filter=""){const engine=sttEngines[index],picker=document.querySelector(`.bench-model-picker[data-index="${index}"]`),list=picker==null?void 0:picker.querySelector(".bench-model-options");if(!engine||!list)return;const qv=String(filter||"").trim().toLowerCase(),models=benchmarkModelList(engine).filter(m=>!qv||m.toLowerCase().includes(qv));list.innerHTML=models.length?models.map(m=>`
<button type="button" class="bench-model-option${m===engine.model?" selected":""}" data-model="${esc(m)}">
<span>${esc(m)}</span>${m===engine.model?'<span class="mdi mdi-check"></span>':""}
</button>`).join(""):'<div class="bench-model-empty">No matching models</div>'}function setBenchmarkModel(index,model){var _a,_b;const engine=sttEngines[index];if(!engine)return;engine.model=String(model||"").trim()||engine.model||"whisper-1";const picker=document.querySelector(`.bench-model-picker[data-index="${index}"]`),hidden=picker==null?void 0:picker.querySelector(".bench-engine-model"),label=picker==null?void 0:picker.querySelector(".bench-model-label");hidden&&(hidden.value=engine.model),label&&(label.textContent=engine.model),(_a=picker==null?void 0:picker.querySelector(".bench-model-menu"))==null||_a.setAttribute("hidden",""),(_b=picker==null?void 0:picker.querySelector(".bench-model-trigger"))==null||_b.classList.remove("open")}function closeBenchmarkModelPickers(except=null){document.querySelectorAll(".bench-model-picker").forEach(picker=>{var _a,_b;except&&picker===except||((_a=picker.querySelector(".bench-model-menu"))==null||_a.setAttribute("hidden",""),(_b=picker.querySelector(".bench-model-trigger"))==null||_b.classList.remove("open"))})}function bindBenchmarkModelPickers(){document.querySelectorAll(".bench-model-picker").forEach(picker=>{var _a;picker.addEventListener("click",ev=>ev.stopPropagation());const index=Number(picker.dataset.index),trigger=picker.querySelector(".bench-model-trigger"),menu=picker.querySelector(".bench-model-menu"),search=picker.querySelector(".bench-model-search");renderBenchmarkModelOptions(index),trigger==null||trigger.addEventListener("click",ev=>{ev.preventDefault(),ev.stopPropagation();const opening=menu==null?void 0:menu.hasAttribute("hidden");closeBenchmarkModelPickers(picker),opening?(menu==null||menu.removeAttribute("hidden"),trigger.classList.add("open"),search.value="",renderBenchmarkModelOptions(index),setTimeout(()=>search==null?void 0:search.focus(),0)):(menu==null||menu.setAttribute("hidden",""),trigger.classList.remove("open"))}),search==null||search.addEventListener("input",()=>renderBenchmarkModelOptions(index,search.value)),(_a=picker.querySelector(".bench-model-options"))==null||_a.addEventListener("click",ev=>{const opt=ev.target.closest(".bench-model-option");opt&&setBenchmarkModel(index,opt.dataset.model||"")})})}async function loadBenchmarkSttEngines(){const list=q("bench-stt-engines"),count=q("bench-stt-engine-count");if(!list)return;list.innerHTML='<div class="bench-engine-empty">Checking STT engines...</div>';let settings={};const items=[];try{settings=await fetchJson("/api/settings")}catch{}try{((await fetchJson("/api/stt-backends")).backends||[]).forEach(b=>items.push({id:b.id,backend:b.id,label:b.label||b.id,url:b.url,model:b.models&&b.models[0]||b.model||"whisper-1",models:b.models||[],available:!!b.available,source:"configured"}))}catch(e){console.warn("[benchmark stt backends]",e)}try{((await fetchJson("/api/local-containers")).containers||[]).filter(c=>c.role==="stt"||c.role==="stt+tts").forEach(c=>{var _a;const url=c.port?`http://host.docker.internal:${c.port}`:"",model=((_a=settings==null?void 0:settings.engine_local_models)==null?void 0:_a["dc-"+c.name])||c.model||"whisper-1";items.push({id:c.name,label:c.label||c.name,url,model,models:c.models||[],available:c.running===!0||c.status==="running",source:c.stack||"docker"})})}catch(e){console.warn("[benchmark local containers]",e)}if(sttEngines=dedupeEngines(items),count&&(count.textContent=sttEngines.length?`Loading models for ${sttEngines.length} engine${sttEngines.length===1?"":"s"}...`:"No engines found."),await hydrateBenchmarkModels(sttEngines),count&&(count.textContent=sttEngines.length?`${sttEngines.length} engine${sttEngines.length===1?"":"s"} loaded.`:"No engines found."),!sttEngines.length){list.innerHTML='<div class="bench-engine-empty">No STT engines found. Check Engines -> Speech to Text.</div>';return}list.innerHTML=sttEngines.map((e,i)=>`
<label class="bench-engine-item ${e.available?"is-online":"is-offline"}">
<input type="checkbox" class="bench-stt-engine-check" data-index="${i}" ${e.available?"checked":""}>
<span class="bench-engine-main">
<strong>${esc(e.label)}</strong>
<small>${esc(e.url||"")}</small>
</span>
<span class="bench-engine-state">${e.available?"Running":"Stopped"}</span>
<div class="bench-model-picker" data-index="${i}">
<button type="button" class="bench-model-trigger" title="Choose model">
<span class="bench-model-label">${esc(e.model||benchmarkModelList(e)[0]||"whisper-1")}</span>
<span class="mdi mdi-chevron-down"></span>
</button>
<div class="bench-model-menu" hidden>
<div class="bench-model-search-row"><span class="mdi mdi-magnify"></span><input class="bench-model-search" type="search" placeholder="Search models..."></div>
<div class="bench-model-options"></div>
</div>
<input class="bench-engine-model" data-index="${i}" type="hidden" value="${esc(e.model||benchmarkModelList(e)[0]||"whisper-1")}">
</div>
</label>`).join(""),bindBenchmarkModelPickers()}async function loadBenchmarkVoiceLibrary(){const sel=q("bench-stt-library-voice");if(!sel)return;const picker=window.BenchmarkVoicePicker;picker?picker.populate("bench-stt-library-voice",[],{placeholder:"Loading voices...",empty:"Loading voices..."}):sel.innerHTML='<option value="">Loading voices...</option>';try{const usable=(await(typeof window.fetchVoiceLibrary=="function"?window.fetchVoiceLibrary():fetchJson("/api/voices"))||[]).filter(v=>v&&v.enabled!==!1&&v.has_ref&&(v.transcript||"").trim()).sort((a,b)=>String(a.display_name||a.id).localeCompare(String(b.display_name||b.id))),items=usable.map(v=>({id:v.id,label:v.display_name||v.name||v.id,meta:v}));picker?picker.populate("bench-stt-library-voice",items,{placeholder:"-- choose from voice library --",empty:"No library voices with reference transcripts found"}):sel.innerHTML='<option value="">-- choose from voice library --</option>'+usable.map(v=>`<option value="${esc(v.id)}">${esc(v.display_name||v.name||v.id)}${v.duration?` (${Number(v.duration).toFixed(1)}s)`:""}</option>`).join("");const note=q("bench-stt-source-note");note&&(note.textContent=usable.length?`${usable.length} voices with reference text available.`:"No library voices with reference transcripts found.")}catch(e){picker?picker.populate("bench-stt-library-voice",[],{placeholder:"Voice library unavailable",empty:"Voice library unavailable"}):sel.innerHTML='<option value="">Voice library unavailable</option>';const note=q("bench-stt-source-note");note&&(note.textContent="Could not load voice library: "+e.message)}}async function useBenchmarkLibraryVoice(){const sel=q("bench-stt-library-voice"),voiceId=(sel==null?void 0:sel.value)||"";if(!voiceId){say("Choose a voice library sample first","error");return}const btn=q("bench-stt-load-voice"),note=q("bench-stt-source-note");btn&&(btn.disabled=!0),note&&(note.textContent="Loading voice sample...");try{const d=await fetchJson("/api/voice-load",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId})});q("bench-stt-source-id")&&(q("bench-stt-source-id").value=d.id||""),q("bench-stt-ref-text")&&(q("bench-stt-ref-text").value=d.transcript||""),q("bench-stt-audio")&&(q("bench-stt-audio").value=""),q("bench-stt-ref-file")&&(q("bench-stt-ref-file").value=""),note&&(note.textContent=`${d.voice_id||voiceId} loaded from library${d.duration?` (${Number(d.duration).toFixed(1)}s)`:""}.`),say("Voice library sample loaded","success")}catch(e){note&&(note.textContent="Voice load failed."),say("Voice load failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function selectedSttEngines(){return[...document.querySelectorAll(".bench-stt-engine-check:checked")].map(cb=>{const i=Number(cb.dataset.index),item={...sttEngines[i]},modelInput=document.querySelector(`.bench-engine-model[data-index="${i}"]`);return item.model=(modelInput==null?void 0:modelInput.value.trim())||item.model||"whisper-1",item}).filter(Boolean)}async function runSttBenchmark(){var _a,_b,_c,_d,_e,_f,_g,_h;const audio=(_b=(_a=q("bench-stt-audio"))==null?void 0:_a.files)==null?void 0:_b[0],sourceId=((_c=q("bench-stt-source-id"))==null?void 0:_c.value)||"",refFile=(_e=(_d=q("bench-stt-ref-file"))==null?void 0:_d.files)==null?void 0:_e[0],refText=((_f=q("bench-stt-ref-text"))==null?void 0:_f.value)||"",engines=selectedSttEngines();if(!audio&&!sourceId){say("Choose an audio file or load a voice library sample first","error");return}if(!refFile&&!refText.trim()){say("Add a reference .txt or paste reference text","error");return}if(!engines.length){say("Select at least one STT engine","error");return}const btn=q("bench-stt-run"),st=q("bench-stt-status"),tbody=(_g=q("bench-stt-table"))==null?void 0:_g.querySelector("tbody");btn&&(btn.disabled=!0),st&&(st.textContent=`Running ${engines.length} STT benchmark${engines.length===1?"":"s"}...`),tbody&&(tbody.innerHTML=`<tr><td colspan="6" class="bench-empty">Benchmarking ${engines.length} engines...</td></tr>`);try{const fd=new FormData;audio&&fd.append("audio",audio,audio.name),sourceId&&fd.append("source_id",sourceId),refFile&&fd.append("reference_file",refFile,refFile.name),fd.append("reference_text",refText),fd.append("engines_json",JSON.stringify(engines));const d=await fetchJson("/api/stt-benchmark",{method:"POST",body:fd});renderSttResults(d),st&&(st.textContent=`Finished ${((_h=d.results)==null?void 0:_h.length)||0} STT engines.`),say("STT benchmark complete","success")}catch(e){st&&(st.textContent="STT benchmark failed"),tbody&&(tbody.innerHTML=`<tr><td colspan="6" class="bench-empty perf-err">${esc(e.message)}</td></tr>`),say("STT benchmark failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function renderSttResults(d){var _a;const tbody=(_a=q("bench-stt-table"))==null?void 0:_a.querySelector("tbody"),summary=q("bench-stt-summary"),rows=d.results||[];summary&&(summary.innerHTML=[["Engines",String(rows.length)],["Reference words",String(d.reference_words||0)],["Best time",d.best_time_sec==null?"\u2014":fmtSec(d.best_time_sec)],["Best accuracy",d.best_accuracy==null?"\u2014":d.best_accuracy.toFixed(1)+"%"]].map(([label,val])=>`<span class="perf-stat"><strong>${esc(val)}</strong> ${esc(label)}</span>`).join("")),tbody&&(tbody.innerHTML=rows.map(r=>{const acc=r.accuracy==null?"\u2014":`${Number(r.accuracy).toFixed(1)}%`,out=r.ok?r.output||"":`\u26A0 ${r.error||"failed"}`;return`<tr class="${r.ok?"":"perf-row-error"}">
<td><strong>${esc(r.engine)}</strong><small>${esc(r.url||"")}</small></td>
<td>${esc(r.model||"")}</td>
<td><span class="bench-device ${String(r.device).includes("CUDA")?"gpu":String(r.device).includes("CPU")?"cpu":""}">${esc(r.device||"Unknown")}</span></td>
<td>${fmtSec(r.time_sec)}</td>
<td class="${r.ok?"perf-ok":"perf-err"}">${esc(acc)}</td>
<td class="bench-output" title="${esc(out)}">${esc(out)}</td>
</tr>`}).join(""))}async function runTtsBenchmark(){var _a,_b,_c,_d;const text=((_a=q("bench-tts-text"))==null?void 0:_a.value.trim())||"";if(!text){say("Enter a sample sentence first","error");return}const activeOnly=(((_b=q("bench-tts-scope"))==null?void 0:_b.value)||"active")==="active",btn=q("bench-tts-run"),st=q("bench-tts-status"),tbody=(_c=q("bench-tts-table"))==null?void 0:_c.querySelector("tbody");btn&&(btn.disabled=!0),st&&(st.textContent="Running TTS benchmark..."),tbody&&(tbody.innerHTML='<tr><td colspan="7" class="bench-empty">Benchmarking voices...</td></tr>');try{const d=await fetchJson("/api/voices/benchmark",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,active_only:activeOnly})});renderTtsResults(d),st&&(st.textContent=`Finished ${d.benchmarked||0} voices.`),say("TTS benchmark complete",(_d=d.errors)!=null&&_d.length?"error":"success")}catch(e){st&&(st.textContent="TTS benchmark failed"),tbody&&(tbody.innerHTML=`<tr><td colspan="7" class="bench-empty perf-err">${esc(e.message)}</td></tr>`),say("TTS benchmark failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function renderTtsResults(d){var _a;const tbody=(_a=q("bench-tts-table"))==null?void 0:_a.querySelector("tbody"),summary=q("bench-tts-summary"),rows=d.voices||[],okRows=rows.map(r=>r.benchmark).filter(b=>b&&b.ok),avg=key=>{const vals=okRows.map(b=>Number(b[key])).filter(Number.isFinite);return vals.length?vals.reduce((a,b)=>a+b,0)/vals.length:null};summary&&(summary.innerHTML=[["Voices",String(rows.length)],["OK",String(okRows.length)],["Avg TTFA",avg("ttfa_ms")==null?"\u2014":Math.round(avg("ttfa_ms"))+"ms"],["Avg total",avg("elapsed_sec")==null?"\u2014":fmtSec(avg("elapsed_sec"))]].map(([label,val])=>`<span class="perf-stat"><strong>${esc(val)}</strong> ${esc(label)}</span>`).join("")),tbody&&(tbody.innerHTML=rows.length?rows.map(row=>{const b=row.benchmark||{},status2=b.ok?b.realtime_ok===!1?"Slow":"OK":b.error||"Failed";return`<tr class="${b.ok?"":"perf-row-error"}">
<td><strong>${esc(row.voice_id)}</strong></td>
<td>${b.ttfa_ms==null?"\u2014":Math.round(b.ttfa_ms)+"ms"}</td>
<td>${fmtSec(b.elapsed_sec)}</td>
<td>${fmtSec(b.audio_sec)}</td>
<td>${b.rtf==null?"\u2014":Number(b.rtf).toFixed(2)}</td>
<td>${b.speed==null?"\u2014":Number(b.speed).toFixed(2)+"x"}</td>
<td class="${b.ok?b.realtime_ok===!1?"perf-slow":"perf-ok":"perf-err"}" title="${esc((b.advice||[]).join(" "))}">${esc(status2)}</td>
</tr>`}).join(""):'<tr><td colspan="7" class="bench-empty">No voices benchmarked.</td></tr>')}async function initTurnControls(){try{const settings=await fetchJson("/api/settings");q("bench-turn-llm-url")&&(q("bench-turn-llm-url").value=settings.conv_llm_url||settings.llm_url||"")}catch{}try{const d=await fetchJson("/api/stt-backends"),sel=q("bench-turn-stt");sel&&(sel.innerHTML=(d.backends||[]).map(b=>`<option value="${esc(b.id)}" ${b.available?"":"disabled"}>${b.available?"\u2713":"\u2717"} ${esc(b.label)}</option>`).join("")||'<option value="configured">Configured</option>')}catch{}try{typeof refreshTtsBackendAvailability=="function"&&await refreshTtsBackendAvailability();const sel=q("bench-turn-tts-backend"),backends=(window._ttsBackends||(typeof _ttsBackends!="undefined"?_ttsBackends:[])||[]).filter(Boolean);sel&&(sel.innerHTML=backends.map(b=>`<option value="${esc(b.id)}" ${b.available?"":"disabled"}>${b.available?"\u2713":"\u2717"} ${esc(b.label)}</option>`).join("")||'<option value="voice_clone">Voice Clone</option>')}catch{}}async function fetchTurnModels(){var _a;const btn=q("bench-turn-fetch-llm"),sel=q("bench-turn-llm-model"),url=((_a=q("bench-turn-llm-url"))==null?void 0:_a.value.trim())||"";btn&&(btn.disabled=!0);try{const models=(await fetchJson("/api/conversation/llm-models"+(url?"?url="+encodeURIComponent(url):""))).models||[];sel&&(sel.innerHTML=models.length?models.map(m=>`<option value="${esc(m)}">${esc(m)}</option>`).join(""):'<option value="">No models found</option>')}catch(e){sel&&(sel.innerHTML='<option value="">Fetch failed</option>'),say("Model fetch failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}async function fetchTurnVoices(){var _a;const btn=q("bench-turn-fetch-voices"),sel=q("bench-turn-voice"),backend=((_a=q("bench-turn-tts-backend"))==null?void 0:_a.value)||"voice_clone",picker=window.BenchmarkVoicePicker;btn&&(btn.disabled=!0);try{const raw=await fetchJson("/api/tts-voices?backend="+encodeURIComponent(backend)),items=(Array.isArray(raw)?raw:[]).map(v=>{const id=typeof backendVoiceId=="function"?backendVoiceId(v):typeof v=="string"?v:v.id||v.voice||v.name;return id?{id,label:id,meta:(window._voices||[]).find(x=>x&&x.id===id)||(typeof v=="object"?v:null)}:null}).filter(Boolean);picker?picker.populate("bench-turn-voice",items,{placeholder:"Fetch voices",empty:"No voices"}):sel&&(sel.innerHTML=items.length?items.map(v=>`<option value="${esc(v.id)}">${esc(v.id)}</option>`).join(""):'<option value="">No voices</option>')}catch(e){picker?picker.populate("bench-turn-voice",[],{placeholder:"Fetch failed",empty:"Fetch failed"}):sel&&(sel.innerHTML='<option value="">Fetch failed</option>'),say("Voice fetch failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function updateTurnStats(stats){const max=stats.total_ms||1;[["stt",stats.stt_ms],["ttft",stats.llm_ttft_ms],["llm",stats.llm_total_ms],["tts",stats.tts_ms],["total",stats.total_ms]].forEach(([key,ms])=>{const val=q("bench-turn-val-"+key),fill=q("bench-turn-fill-"+key);val&&(val.textContent=fmtMs(ms)),fill&&(fill.style.width=max>0?Math.min(100,(ms||0)/max*100)+"%":"0%")})}function addTurnLog(role,text){var _a;const log=q("bench-turn-log");if(!log)return null;(_a=log.querySelector(".conv-chat-welcome"))==null||_a.remove();const wrap=document.createElement("div");wrap.className=`conv-bubble-wrap conv-bubble-wrap--${role}`;const bubble=document.createElement("div");return bubble.className=`conv-bubble conv-bubble--${role}`,bubble.textContent=text||"",wrap.appendChild(bubble),log.appendChild(wrap),log.scrollTop=log.scrollHeight,bubble}function addTurnHistory(total,ok){var _a;const hist=q("bench-turn-history");if(!hist)return;(_a=hist.querySelector(".conv-history-empty"))==null||_a.remove(),turnHistoryCount++;const item=document.createElement("div");item.className="conv-hist-item",item.innerHTML=`<span class="conv-hist-num">#${turnHistoryCount}</span><span class="${ok?"conv-hist-ok":"conv-hist-err"}"><span class="mdi ${ok?"mdi-check-circle-outline":"mdi-alert-outline"}"></span></span><span class="conv-hist-time ${ok?"conv-hist-ok":"conv-hist-err"}">${fmtMs(total)}</span>`,hist.prepend(item)}async function runTurnBenchmark(){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k;const audio=(_b=(_a=q("bench-turn-audio"))==null?void 0:_a.files)==null?void 0:_b[0],text=((_c=q("bench-turn-text"))==null?void 0:_c.value.trim())||"";if(!audio&&!text){say("Choose turn audio or enter fallback text","error");return}const btn=q("bench-turn-run"),st=q("bench-turn-status");btn&&(btn.disabled=!0),st&&(st.textContent="Running conversation turn...");const t0=Date.now(),userBubble=addTurnLog("user",text||"Transcribing audio..."),assistantBubble=addTurnLog("assistant","...");let assistantText="",lastStats=null;try{const fd=new FormData;audio&&fd.append("audio",audio,audio.name),text&&fd.append("text",text),fd.append("stt_backend",((_d=q("bench-turn-stt"))==null?void 0:_d.value)||"configured"),fd.append("llm_url",((_e=q("bench-turn-llm-url"))==null?void 0:_e.value.trim())||""),fd.append("llm_model",((_f=q("bench-turn-llm-model"))==null?void 0:_f.value)||""),fd.append("tts_backend",((_g=q("bench-turn-tts-backend"))==null?void 0:_g.value)||"voice_clone"),fd.append("tts_voice",((_h=q("bench-turn-voice"))==null?void 0:_h.value)||""),fd.append("system_prompt",((_i=q("bench-turn-system"))==null?void 0:_i.value.trim())||"You are a helpful voice assistant."),fd.append("history","[]");const resp=await fetch("/api/conversation/turn",{method:"POST",body:fd});if(!resp.ok)throw new Error("Server error "+resp.status);const reader=resp.body.getReader(),dec=new TextDecoder;let buf="";for(;;){const{done,value}=await reader.read();if(done)break;buf+=dec.decode(value,{stream:!0});const lines=buf.split(`
`);buf=lines.pop();for(const line of lines){if(!line.startsWith("data:"))continue;let evt;try{evt=JSON.parse(line.slice(5).trim())}catch{continue}if(evt.type==="transcript"&&userBubble&&(userBubble.textContent=evt.text||"(empty)"),evt.type==="token"&&(assistantText+=evt.delta||"",assistantBubble&&(assistantBubble.textContent=assistantText)),evt.type==="llm_done"&&(assistantText=evt.text||assistantText,assistantBubble&&(assistantBubble.textContent=assistantText)),evt.type==="audio"&&evt.b64){const bytes=Uint8Array.from(atob(evt.b64),c=>c.charCodeAt(0)),url=URL.createObjectURL(new Blob([bytes],{type:evt.mime||"audio/wav"})),audioEl=document.createElement("audio");audioEl.controls=!0,audioEl.src=url,audioEl.addEventListener("ended",()=>URL.revokeObjectURL(url),{once:!0}),(_j=q("bench-turn-log"))==null||_j.appendChild(audioEl)}if(evt.type==="stats"&&(lastStats=evt,updateTurnStats(evt)),evt.type==="error")throw new Error(`[${evt.stage||"turn"}] ${evt.message||"Unknown error"}`)}}const total=(_k=lastStats==null?void 0:lastStats.total_ms)!=null?_k:Date.now()-t0;addTurnHistory(total,!0),st&&(st.textContent=`Finished in ${fmtMs(total)}.`),say("Turn benchmark complete","success")}catch(e){assistantBubble&&(assistantBubble.textContent=e.message),addTurnHistory(Date.now()-t0,!1),st&&(st.textContent="Turn benchmark failed"),say("Turn benchmark failed: "+e.message,"error")}finally{btn&&(btn.disabled=!1)}}function bindBenchmarkSection(){var _a,_b,_c,_d,_e,_f,_g,_h,_i;initialized||!q("bench-stt-run")||(initialized=!0,document.querySelectorAll(".bench-tab").forEach(btn=>btn.addEventListener("click",()=>setBenchTab(btn.dataset.benchTab))),window.BenchmarkVoicePicker&&(BenchmarkVoicePicker.upgrade("bench-stt-library-voice",{placeholder:"-- choose from voice library --",empty:"No library voices with reference transcripts found"}),BenchmarkVoicePicker.upgrade("perf-voice-select",{placeholder:"-- select after fetch --",empty:"No voices"}),BenchmarkVoicePicker.upgrade("bench-turn-voice",{placeholder:"Fetch voices",empty:"No voices"})),document.addEventListener("click",()=>closeBenchmarkModelPickers()),(_a=q("bench-stt-refresh"))==null||_a.addEventListener("click",loadBenchmarkSttEngines),(_b=q("bench-stt-load-voice"))==null||_b.addEventListener("click",useBenchmarkLibraryVoice),(_c=q("bench-stt-audio"))==null||_c.addEventListener("change",()=>{q("bench-stt-source-id")&&(q("bench-stt-source-id").value="")}),(_d=q("bench-stt-run"))==null||_d.addEventListener("click",runSttBenchmark),(_e=q("bench-tts-run"))==null||_e.addEventListener("click",runTtsBenchmark),(_f=q("bench-turn-fetch-llm"))==null||_f.addEventListener("click",fetchTurnModels),(_g=q("bench-turn-fetch-voices"))==null||_g.addEventListener("click",fetchTurnVoices),(_h=q("bench-turn-run"))==null||_h.addEventListener("click",runTurnBenchmark),(_i=q("bench-turn-tts-backend"))==null||_i.addEventListener("change",()=>{window.BenchmarkVoicePicker?BenchmarkVoicePicker.populate("bench-turn-voice",[],{placeholder:"Fetch voices",empty:"No voices"}):q("bench-turn-voice")&&(q("bench-turn-voice").innerHTML='<option value="">Fetch voices</option>')}))}function loadBenchmarkSectionData(){bindBenchmarkSection(),!(benchmarkDataLoaded||!q("bench-stt-run"))&&(benchmarkDataLoaded=!0,loadBenchmarkSttEngines(),loadBenchmarkVoiceLibrary(),initTurnControls())}window.loadBenchmarkSectionData=loadBenchmarkSectionData,bindBenchmarkSection(),!initialized&&document.body&&new MutationObserver(()=>bindBenchmarkSection()).observe(document.body,{childList:!0,subtree:!0})}(),function(root){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l;let sttTtsSourceId=null,sttTtsOutputBlob=null,_sttBackends=[],sttTtsRecorder=null,sttTtsRecordStream=null,sttTtsRecordChunks=[],sttTtsRecordTimer=null,sttTtsRecordSecs=0;function sttTtsSelectedSttBackend(){var _a2;return((_a2=$("stt-tts-stt-backend"))==null?void 0:_a2.value)||"configured"}function sttBackendOptionHtml(selected="configured"){var _a2;if(!_sttBackends.length)return'<option value="configured">Configured Whisper/STT</option>';const preferred=_sttBackends.some(b=>b.id===selected&&b.available)?selected:((_a2=_sttBackends.find(b=>b.available))==null?void 0:_a2.id)||selected;return _sttBackends.map(b=>{const suffix=b.available?"":" (unavailable)",disabled=b.available?"":" disabled";return`<option value="${escHtml(b.id)}" ${b.id===preferred?"selected":""}${disabled}>${escHtml(b.label+suffix)}</option>`}).join("")}function updateSttBackendHelp(){const selected=sttTtsSelectedSttBackend(),b=_sttBackends.find(item=>item.id===selected)||_sttBackends.find(item=>item.available)||null,help=$("stt-tts-stt-help");if(help){if(!b){help.textContent="No STT engine status loaded yet.";return}help.innerHTML=sttBackendHelpHtml(b)}}async function refreshSttBackends2(selected=""){try{_sttBackends=((await fetch("/api/stt-backends").then(r=>r.json())).backends||[]).filter(b=>b&&b.id)}catch{_sttBackends=[]}["stt-tts-stt-backend","clone-stt-backend"].forEach(id=>{const sel=$(id);if(!sel)return;const prev=selected||sel.value||"configured";sel.innerHTML=sttBackendOptionHtml(prev),sel.disabled=!_sttBackends.some(b=>b.available)}),updateSttBackendHelp(),typeof window.updateStatusBar=="function"&&window.updateStatusBar(),typeof window.refreshStatusBarEngines=="function"&&window.refreshStatusBarEngines()}function sttTtsSelectedBackend(){var _a2;return((_a2=$("stt-tts-backend-select"))==null?void 0:_a2.value)||""}function sttTtsDownload(blob,name){if(!blob)return;const a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=name,a.click()}async function sttTtsUploadFile(file){if(!file)return;$("stt-tts-source-status").textContent="Uploading "+file.name+"...",sttTtsSourceId=null,sttTtsOutputBlob=null,$("stt-tts-transcribe-btn").disabled=!0,$("stt-tts-copy-preview-btn").disabled=!0,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0;const fd=new FormData;fd.append("file",file);try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();sttTtsSourceId=d.id;const audio=$("stt-tts-source-audio");audio.src="/api/audio/"+encodeURIComponent(d.id),audio.style.display="",$("stt-tts-source-status").textContent=`${d.filename||file.name} loaded (${Number(d.duration||0).toFixed(1)} s).`,$("stt-tts-transcribe-btn").disabled=!1,toast("Speech audio loaded","success")}catch(e){$("stt-tts-source-status").textContent="Upload failed.",toast("STT source upload failed: "+e.message,"error")}}async function sttTtsFetchVoices(){const backend=sttTtsSelectedBackend();if(!backend)throw new Error("No available TTS backend");const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());let voices=Array.isArray(rawVoices)?rawVoices:[];if(shouldFilterBackendVoices(backend)){const activeIds=await activeLibraryVoiceIds();voices=voices.filter(v=>activeIds.has(backendVoiceId(v)))}const sel=$("stt-tts-voice-select"),prev=sel.value;return sel.innerHTML='<option value="">-- select after fetch --</option>',voices.forEach(v=>{const id=backendVoiceId(v),opt=document.createElement("option");opt.value=opt.textContent=id,sel.appendChild(opt)}),prev&&voices.some(v=>backendVoiceId(v)===prev)&&(sel.value=prev),voices.length}(_a=$("stt-tts-file"))==null||_a.addEventListener("change",async()=>{const input=$("stt-tts-file");input.files&&input.files.length&&await sttTtsUploadFile(input.files[0]),input.value=""}),(_b=$("stt-tts-refresh-stt-btn"))==null||_b.addEventListener("click",async()=>{const btn=$("stt-tts-refresh-stt-btn");btn.disabled=!0;try{await refreshSttBackends2(sttTtsSelectedSttBackend()),toast("STT engines refreshed","success")}finally{btn.disabled=!1}}),(_c=$("stt-tts-stt-backend"))==null||_c.addEventListener("change",updateSttBackendHelp);function sttTtsSetRecording(on){$("stt-tts-rec-start").disabled=on,$("stt-tts-rec-stop").disabled=!on}function sttTtsStopTracks(){sttTtsRecordStream&&sttTtsRecordStream.getTracks().forEach(t=>t.stop()),sttTtsRecordStream=null}(_d=$("stt-tts-rec-start"))==null||_d.addEventListener("click",async()=>{try{sttTtsRecordStream=await requestMicrophoneStream(),sttTtsRecordChunks=[],sttTtsRecordSecs=0,$("stt-tts-rec-time").textContent="0:00",$("stt-tts-source-status").textContent="Recording...",sttTtsSetRecording(!0),sttTtsRecordTimer=setInterval(()=>{sttTtsRecordSecs++,$("stt-tts-rec-time").textContent=Math.floor(sttTtsRecordSecs/60)+":"+String(sttTtsRecordSecs%60).padStart(2,"0")},1e3),sttTtsRecorder=new MediaRecorder(sttTtsRecordStream,{audioBitsPerSecond:256e3}),sttTtsRecorder.ondataavailable=e=>{e.data.size&&sttTtsRecordChunks.push(e.data)},sttTtsRecorder.onstop=async()=>{clearInterval(sttTtsRecordTimer),sttTtsRecordTimer=null,sttTtsSetRecording(!1),sttTtsStopTracks();const mime=sttTtsRecorder.mimeType||"audio/webm",blob=new Blob(sttTtsRecordChunks,{type:mime}),ext=mime.includes("ogg")?".ogg":".webm";if(!blob.size){$("stt-tts-source-status").textContent="Recording was empty.",toast("Recording was empty","error");return}await sttTtsUploadFile(new File([blob],"stt-recording"+ext,{type:mime}))},sttTtsRecorder.start(100),toast("Recording started","success")}catch(e){sttTtsSetRecording(!1),sttTtsStopTracks();const message=await microphoneErrorMessage(e);$("stt-tts-source-status").textContent=message,toast(message,"error")}}),(_e=$("stt-tts-rec-stop"))==null||_e.addEventListener("click",()=>{sttTtsRecorder&&sttTtsRecorder.state!=="inactive"&&sttTtsRecorder.stop()}),(_f=$("stt-tts-transcribe-btn"))==null||_f.addEventListener("click",async()=>{if(!sttTtsSourceId){toast("Load speech audio first","error");return}const btn=$("stt-tts-transcribe-btn");btn.disabled=!0,$("stt-tts-source-status").textContent="Transcribing...";try{const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:sttTtsSourceId,backend:sttTtsSelectedSttBackend()})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("stt-tts-text").value=d.text||"",$("stt-tts-copy-preview-btn").disabled=!(d.text||"").trim();const used=d.backend?" via "+d.backend:"";$("stt-tts-source-status").textContent="Transcription ready"+used+".",typeof updateRefineButtonState=="function"&&updateRefineButtonState(),toast("Transcription ready","success")}catch(e){$("stt-tts-source-status").textContent="Transcription failed.",toast("STT failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_g=$("stt-tts-copy-preview-btn"))==null||_g.addEventListener("click",()=>{const text=$("stt-tts-text").value.trim();text&&($("preview-text-area").value=text,switchTab("generation"),toast("Copied transcription to TTS Generation","success"))}),(_h=$("stt-tts-backend-select"))==null||_h.addEventListener("change",()=>{$("stt-tts-voice-select").innerHTML='<option value="">-- select after fetch --</option>',sttTtsOutputBlob=null,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0,updateBackendHelp()}),(_i=$("stt-tts-fetch-voices-btn"))==null||_i.addEventListener("click",async()=>{const btn=$("stt-tts-fetch-voices-btn");btn.disabled=!0;try{const count=await sttTtsFetchVoices();toast("Fetched "+count+" voices","success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_j=$("stt-tts-generate-btn"))==null||_j.addEventListener("click",async()=>{const backend=sttTtsSelectedBackend(),voice=$("stt-tts-voice-select").value,text=$("stt-tts-text").value.trim(),instruct=$("stt-tts-style-instruction").value.trim();if(!backend){toast("No available TTS backend","error");return}if(!voice){toast("Select a TTS voice","error");return}if(!text){toast("Transcribe or enter text first","error");return}const btn=$("stt-tts-generate-btn");btn.disabled=!0,$("stt-tts-save-mp3-btn").disabled=!0,$("stt-tts-save-wav-btn").disabled=!0;try{const source=await createTtsAudioSource(voice,text,backend,$("stt-tts-playback-mode").value,instruct);sttTtsOutputBlob=source.blob;const audio=$("stt-tts-output-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("stt-tts-save-mp3-btn").disabled=!1,$("stt-tts-save-wav-btn").disabled=source.streaming,toast(source.streaming?"Streaming synthesized speech":"Synthesized speech ready","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_k=$("stt-tts-save-mp3-btn"))==null||_k.addEventListener("click",async()=>{const backend=sttTtsSelectedBackend(),voice=$("stt-tts-voice-select").value,text=$("stt-tts-text").value.trim(),instruct=$("stt-tts-style-instruction").value.trim();if(!backend||!voice||!text)return;const btn=$("stt-tts-save-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend);sttTtsDownload(blob,(voice||"stt_tts")+"_stt_tts.mp3"),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),(_l=$("stt-tts-save-wav-btn"))==null||_l.addEventListener("click",()=>{sttTtsOutputBlob&&sttTtsDownload(sttTtsOutputBlob,($("stt-tts-voice-select").value||"stt_tts")+"_stt_tts.wav")}),Object.assign(root,{sttTtsSourceId,sttTtsOutputBlob,_sttBackends,sttTtsRecorder,sttTtsRecordStream,sttTtsRecordChunks,sttTtsRecordTimer,sttTtsRecordSecs,sttTtsSelectedSttBackend,sttBackendOptionHtml,updateSttBackendHelp,refreshSttBackends:refreshSttBackends2,sttTtsSelectedBackend,sttTtsDownload,sttTtsUploadFile,sttTtsFetchVoices,sttTtsSetRecording,sttTtsStopTracks}),Object.defineProperty(root,"_sttBackends",{configurable:!0,get:()=>_sttBackends,set:v=>{_sttBackends=v}}),root.TTVC=root.TTVC||{},root.TTVC.stt=root.TTVC.stt||{},Object.assign(root.TTVC.stt,{get backends(){return _sttBackends},refresh:refreshSttBackends2,fetchVoices:sttTtsFetchVoices,upload:sttTtsUploadFile})}(window),function(){const root=window.TTVC=window.TTVC||{};root.audiobook=root.audiobook||{},root.rehearser=root.rehearser||{},root.jobs=root.jobs||{},[["audiobook","fetchWithTimeout","audiobookFetchWithTimeout"],["audiobook","attributeStream","audiobookAttributeStream"],["audiobook","timeoutSeconds","audiobookTimeoutSeconds"],["rehearser","renderLibrary","renderLibraryList"],["rehearser","showPhase","showPhase"]].forEach(([namespace,publicName,legacyName])=>{typeof window[legacyName]!="undefined"&&root[namespace][publicName]==null&&(root[namespace][publicName]=window[legacyName])}),typeof rehState!="undefined"&&root.rehearser.state==null&&(root.rehearser.state=rehState)}();