diff --git a/core/magazine.py b/core/magazine.py
index 53ed738..e80c501 100644
--- a/core/magazine.py
+++ b/core/magazine.py
@@ -2600,7 +2600,7 @@ def extract_pages(pdf_path: Path, out_dir: Path, render_dpi: int = 110,
blocks: list[Block] = []
data = {} if extra_turn else page.get_text("dict")
for bi, blk in enumerate(data.get("blocks", [])):
- if blk.get("type") != 0: # 0 = text, 1 = image
+ if blk.get("type") != 0: # block type: text or image
continue
texts, sizes, fonts, bolds = [], [], [], []
for line in blk.get("lines", []):
diff --git a/core/tts_helpers.py b/core/tts_helpers.py
index 1978643..1e5211c 100644
--- a/core/tts_helpers.py
+++ b/core/tts_helpers.py
@@ -348,7 +348,7 @@ def _nvidia_clone_request_audio(
if audio_path is None:
raise RuntimeError(f"reference voice not found in library: {voice}")
prompt_wav = _to_wav_24k(audio_path)
- has_ref, saved_transcript = _read_reference_text(audio_path)
+ _, saved_transcript = _read_reference_text(audio_path)
prompt_transcript = str(reference_transcript or saved_transcript or "").strip()
if mode == "flow" and not prompt_transcript:
raise RuntimeError("NVIDIA Magpie Flow requires the selected voice to have an exact saved reference transcript")
diff --git a/routes/conversation.py b/routes/conversation.py
index 4650c3e..6aa98a4 100644
--- a/routes/conversation.py
+++ b/routes/conversation.py
@@ -2939,11 +2939,6 @@ def _build_compact_character_render_prompts(name: str, book: str, sheet: dict,
f"The scalp, crown, hairline, {ear_shape}, and complete natural hairstyle are visibly exposed and unobstructed."
)
bareheaded = not has_headwear
- head_lock = (
- f"HEAD CONTINUITY: In all three figures, clearly show the exposed scalp, crown, hairline, {ear_shape}, and complete natural hairstyle in front, profile, and rear views. "
- if bareheaded else
- "HEADWEAR LOCK: reproduce only the explicitly established period headwear, identically in all three views. "
- )
leather_only = _character_is_leather_only_armor(sheet)
period_materials = (
"wool, linen, leather, hand-worked fastenings, and flat stitched-leather footwear"
@@ -3095,12 +3090,6 @@ def _build_compact_character_render_prompts(name: str, book: str, sheet: dict,
f"{female_presentation_lock}"
f"{no_headwear}"
)
- if visual_style == "concept":
- portrait_style = "One fixed semi-realistic hand-painted fantasy production artist with grounded anatomy and weathered materials."
- sheet_style = portrait_style
- else:
- portrait_style = "Strict live-action photorealistic historical-fantasy production photography; real actor, honest skin, natural hair, restrained warm-neutral film color."
- sheet_style = "Strict live-action photorealistic costume-continuity studio photography by that same production photographer, with a real actor and physically believable period materials."
# Nothing previously told the model what a correctly-proportioned body
# looks like, and the turnaround triptych in particular squeezes each
# full-body figure into a narrow 512-wide third of the frame — confirmed
@@ -3112,7 +3101,7 @@ def _build_compact_character_render_prompts(name: str, book: str, sheet: dict,
# same reasoning as the armor lock above: this encoder still visualizes a
# noun mentioned after "no"/"not", so "adult proportions, not dwarfed"
# would still cue dwarfism rather than prevent it.
- subject_clause, identical_noun, proportion_lock = _age_body_lock(sheet)
+ _, identical_noun, proportion_lock = _age_body_lock(sheet)
# Short and framing-first. The long version (~3 000 characters, framing rules buried in the middle)
# was measured against this one on the same character, 5 seeds each, scored by the image check:
# full bodies 0/5 vs 5/5 on a 1536x1024 canvas (also 5/5 at 1536x1280), plain backdrop 2/5 vs 5/5.
@@ -4104,7 +4093,6 @@ def _comfyui_upload_data_uri(comfyui_url: str, data_uri: str) -> str:
except Exception as exc:
raise HTTPException(400, f"Identity image is not valid base64: {exc}")
mime = header.split(";", 1)[0].split(":", 1)[-1].lower()
- ext = ".jpg" if mime in {"image/jpeg", "image/jpg"} else ".webp" if mime == "image/webp" else ".png"
filename = f"voice_creator_identity_{uuid.uuid4().hex}.png"
try:
resp = requests.post(
diff --git a/routes/magazine.py b/routes/magazine.py
index 4994f5c..5bc24d8 100644
--- a/routes/magazine.py
+++ b/routes/magazine.py
@@ -3006,7 +3006,7 @@ def _spelling_agent_job(doc_id: str, job_id: str, article_id: str | None, page:
@router.post("/api/magazine/{doc_id}/spelling/agent")
async def magazine_spelling_agent(doc_id: str, request: Request):
"""Background job: the LLM fixes provable OCR misspellings; the rest stays marked."""
- d, _ = _require_magazine(doc_id)
+ _, _ = _require_magazine(doc_id)
body = await request.json()
body = body if isinstance(body, dict) else {}
article = _article_id(str(body["article"])) if body.get("article") else None
@@ -3213,7 +3213,7 @@ def _audio_generate_one_sync(doc_id: str, job_id: str, article_id: str, voice: s
async def magazine_generate_article_audio(doc_id: str, article_id: str, request: Request):
"""Render one approved article to a single tagged MP3 (title/author/album,
its own text as embedded lyrics, cover art) -- for taking it out of the app."""
- d, meta = _require_magazine(doc_id)
+ _, meta = _require_magazine(doc_id)
article_id = _article_id(article_id)
body = await request.json() if await request.body() else {}
voice = str(body.get("voice") or meta.get("default_voice") or "").strip()
@@ -3484,7 +3484,6 @@ async def magazine_audio_zip(doc_id: str, provenance: str = "", voice: str = "")
import tempfile
with tempfile.TemporaryDirectory(prefix="mag-prov-") as tagdir, zipfile.ZipFile(buf, "w", zipfile.ZIP_STORED) as zf:
for i, a in enumerate(ordered, start=1):
- page = (a.get("pages") or [0])[0]
name = _article_download_filename(meta, a)
# Zero-padded sequence prefix so extracting the zip and sorting
# by filename gives the reading order even where two articles
diff --git a/routes/reader.py b/routes/reader.py
index e0e7b36..0317970 100644
--- a/routes/reader.py
+++ b/routes/reader.py
@@ -227,7 +227,6 @@ def _cast_to_md(data: dict) -> str:
doesn't need to round-trip back through _md_to_cast (that function only
still matters for pre-migration legacy .md files)."""
segments = data.get("segments") or []
- roster = data.get("roster") or []
saved_at = data.get("savedAt")
saved_str = ""
if saved_at:
diff --git a/routes/settings.py b/routes/settings.py
index 071b644..c498f3c 100644
--- a/routes/settings.py
+++ b/routes/settings.py
@@ -25,16 +25,6 @@ async def get_settings(request: Request, response: Response):
# install shows a real usable key in Settings immediately without a
# migration step, whether or not the gate is actually turned on yet.
s["external_api_key"] = _ensure_external_api_key()
- host = request.headers.get("host", "")
- origin = request.headers.get("origin", "") or request.headers.get("referer", "")
- try:
- from urllib.parse import urlparse
- same_origin = (
- request.headers.get("sec-fetch-site", "").lower() == "same-origin"
- or bool(origin and host and urlparse(origin).netloc == host)
- )
- except Exception:
- same_origin = False
# Never return provider credentials to browser JavaScript. The mask lets
# the Settings UI indicate that a value exists without making it usable in
# client-side requests or browser storage.
diff --git a/static/dist/main.min.js b/static/dist/main.min.js
index 959f056..7582532 100644
--- a/static/dist/main.min.js
+++ b/static/dist/main.min.js
@@ -13,7 +13,7 @@
`,body.appendChild(panel);const q=sel=>panel.querySelector(sel),status2=q(".insp-persona-status"),say=(msg,err)=>{status2.textContent=msg,status2.style.color=err?"var(--danger, #dc2626)":""};q(".insp-prov-save").addEventListener("click",async()=>{try{const r=await fetch(`/api/voices/${encodeURIComponent(voiceId)}/provenance`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({basis:q(`#${id}-basis`).value,source:q(`#${id}-source`).value,licence:q(`#${id}-licence`).value,note:q(`#${id}-note`).value})}),d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.detail||r.statusText);v.provenance=d.provenance,typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),say("Saved.")}catch(e){say("Save failed: "+e.message,!0)}});const btn=q(".insp-prov-rec-btn");let rec=null;btn.addEventListener("click",async()=>{if(rec){rec.stop();return}if(!navigator.mediaDevices||!window.MediaRecorder){say("Recording is not supported in this browser. Type the consent statement in the note field instead.",!0);return}let stream;try{stream=await navigator.mediaDevices.getUserMedia({audio:!0})}catch(e){say(e&&e.name==="NotAllowedError"?"Microphone permission was denied. Allow it in the browser, or type the consent statement in the note field.":"No usable microphone found. Type the consent statement in the note field instead.",!0);return}const chunks=[];rec=new MediaRecorder(stream),rec.ondataavailable=ev=>{ev.data.size&&chunks.push(ev.data)},rec.onstop=async()=>{stream.getTracks().forEach(t=>t.stop()),rec=null,btn.textContent="Record spoken consent",btn.setAttribute("aria-pressed","false");try{const fd=new FormData;fd.append("file",new Blob(chunks,{type:"audio/webm"}),"consent.webm");const r=await fetch(`/api/voices/${encodeURIComponent(voiceId)}/consent-recording`,{method:"POST",body:fd}),d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.detail||r.statusText);v.provenance=d.provenance;const a=q(".insp-prov-audio");a.src=`/api/voices/${encodeURIComponent(voiceId)}/consent-recording?t=${Date.now()}`,a.hidden=!1,say("Consent recording saved.")}catch(e){say("Upload failed: "+e.message,!0)}},rec.start(),btn.textContent="Stop recording",btn.setAttribute("aria-pressed","true"),say("Recording... say who you are, that you consent to this voice being cloned, and for what use.")})}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.",_sfT=s=>typeof window.t=="function"?window.t(s):s;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>>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(_){logErr("seed-db-put",_)}}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(_){logErr("seed-db-delete",_)}}function attachSeedFinder2(voiceId,body){const panel=document.createElement("div");panel.className="opt-group seed-finder-group",panel.innerHTML=`
+
`,body.appendChild(panel);const q=sel=>panel.querySelector(sel),status2=q(".insp-persona-status"),say=(msg,err)=>{status2.textContent=msg,status2.style.color=err?"var(--danger, #dc2626)":""};q(".insp-prov-save").addEventListener("click",async()=>{try{const r=await fetch(`/api/voices/${encodeURIComponent(voiceId)}/provenance`,{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({basis:q(`#${id}-basis`).value,source:q(`#${id}-source`).value,licence:q(`#${id}-licence`).value,note:q(`#${id}-note`).value})}),d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.detail||r.statusText);v.provenance=d.provenance,typeof window.invalidateVoiceLibraryCache=="function"&&window.invalidateVoiceLibraryCache(),say("Saved.")}catch(e){say("Save failed: "+e.message,!0)}});const btn=q(".insp-prov-rec-btn");let rec=null;btn.addEventListener("click",async()=>{if(rec){rec.stop();return}if(!navigator.mediaDevices||!window.MediaRecorder){say("Recording is not supported in this browser. Type the consent statement in the note field instead.",!0);return}let stream;try{stream=await navigator.mediaDevices.getUserMedia({audio:!0})}catch(e){say(e&&e.name==="NotAllowedError"?"Microphone permission was denied. Allow it in the browser, or type the consent statement in the note field.":"No usable microphone found. Type the consent statement in the note field instead.",!0);return}const chunks=[];rec=new MediaRecorder(stream),rec.ondataavailable=ev=>{ev.data.size&&chunks.push(ev.data)},rec.onstop=async()=>{stream.getTracks().forEach(t=>t.stop()),rec=null,btn.textContent="Record spoken consent",btn.setAttribute("aria-pressed","false");try{const fd=new FormData;fd.append("file",new Blob(chunks,{type:"audio/webm"}),"consent.webm");const r=await fetch(`/api/voices/${encodeURIComponent(voiceId)}/consent-recording`,{method:"POST",body:fd}),d=await r.json().catch(()=>({}));if(!r.ok)throw new Error(d.detail||r.statusText);v.provenance=d.provenance;const a=q(".insp-prov-audio");a.src=`/api/voices/${encodeURIComponent(voiceId)}/consent-recording?t=${Date.now()}`,a.hidden=!1,say("Consent recording saved.")}catch(e){say("Upload failed: "+e.message,!0)}},rec.start(),btn.textContent="Stop recording",btn.setAttribute("aria-pressed","true"),say("Recording... say who you are, that you consent to this voice being cloned, and for what use.")})}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.",_sfT=s=>typeof window.t=="function"?window.t(s):s;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>>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(_){logErr("seed-db-put",_)}}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(_){logErr("seed-db-delete",_)}}function attachSeedFinder2(voiceId,body){const panel=document.createElement("div");panel.className="opt-group seed-finder-group",panel.innerHTML=`
\u{1F3B2} Seed Finder
@@ -456,7 +456,7 @@ Mia:Only if you promise not to spill coffee on my notes again... though I guess
- `,list.appendChild(card)}))}function applyQwenSample(sample){$("design-instruct").value=sample.description,$("design-sample-text").value=sample.text,$("design-language").value=sample.language,$("design-gender").value=sample.gender,currentDesignSource=sample,$("design-result").style.display="none",$("design-save-result").style.display="none",$("design-instruct").scrollIntoView({behavior:"smooth",block:"nearest"})}function isDialogueDesign(instruct,text,source=null){if(source&&source.dialogue)return!0;const speakers=new Set;if(String(instruct||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^"?([^":]+)"?\s*:\s*"?(.+?)"?$/);match&&speakers.add(match[1].trim())}),speakers.size<2)return!1;const turnSpeakers=new Set;return String(text||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^([^:]{1,40}):\s*(.+)$/);match&&speakers.has(match[1].trim())&&turnSpeakers.add(match[1].trim())}),turnSpeakers.size>=2}function voiceDesignPayload(instruct,sampleText,language,source=null,gender=null){var _a2;return{instruct,sample_text:sampleText,language,gender:gender||(source==null?void 0:source.gender)||((_a2=$("design-gender"))==null?void 0:_a2.value)||"",dialogue:isDialogueDesign(instruct,sampleText,source)}}let _dVoiceIdManual=!1;function designSafeName(name){const base=name||"VoiceDesign";return(typeof _umlautSafe=="function"?_umlautSafe(base):String(base)).replace(/^[A-Z]{2}_[FMN]_/,"").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,42)||"VoiceDesign"}function voiceIdSafePart(value,fallback="style"){return(typeof _umlautSafe=="function"?_umlautSafe(value||fallback):String(value||fallback)).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,32)||fallback}function suggestedStyleVoiceId2(baseId,style){const suffix=voiceIdSafePart(style||"style");return`${baseId}_${suffix}`.slice(0,96)}function _updateDVoiceId(){if(_dVoiceIdManual)return;const lang=$("d-lang").value,gender=$("d-gender").value,name=$("d-name").value.trim();$("d-voice-id").value=name?`${lang}_${gender}_${name}`:""}["d-lang","d-gender"].forEach(id=>["change","input"].forEach(ev=>$(id).addEventListener(ev,_updateDVoiceId))),$("d-name").addEventListener("input",()=>{_dVoiceIdManual=!1,_updateDVoiceId()}),$("d-voice-id").addEventListener("input",()=>{_dVoiceIdManual=!0}),seedDesignPresets(),refreshDesignPresetSelect(),renderQwenSampleCards(),syncDesignPresetsToServer(),$("design-preset-select").addEventListener("change",()=>{$("design-preset-select").value&&applyDesignPreset($("design-preset-select").value)}),$("design-preset-load").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}applyDesignPreset(name)}),$("design-preset-save").addEventListener("click",()=>{const name=$("design-preset-name").value.trim()||$("design-preset-select").value;if(!name){toast("Enter a preset name","error"),$("design-preset-name").focus();return}const presets=loadDesignPresets2();presets[name]={description:$("design-instruct").value,sample_text:$("design-sample-text").value,language:$("design-language").value,gender:$("design-gender").value,dialogue:isDialogueDesign($("design-instruct").value,$("design-sample-text").value,currentDesignSource)},saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-select").value=name,toast("Preset saved: "+name,"success")}),$("design-preset-delete").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}const presets=loadDesignPresets2();if(!presets[name]){toast("Preset not found","error");return}delete presets[name],saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-name").value="",toast("Preset deleted: "+name,"success")}),["design-instruct","design-sample-text"].forEach(id=>$(id).addEventListener("input",()=>{currentDesignSource=null,id==="design-sample-text"&&($("d-transcript").value=$("design-sample-text").value)}));const DESIGN_CHIPS=[["Gender",!0,["male voice","female voice","neutral voice"]],["Age",!0,["child","teenager","young adult","middle-aged","elderly"]],["Pitch",!0,["high pitch","mid pitch","low pitch"]],["Pace",!0,["fast pace","moderate pace","slow pace"]],["Accent / region",!1,["British accent","American accent","Australian accent","Irish accent","German accent","Southern accent"]],["Tone / timbre",!1,["warm","husky","crisp","smooth","rich","magnetic","authoritative"]],["Emotion",!1,["cheerful","calm","serious","gentle","enthusiastic","sad"]]],_chipRe=p=>new RegExp("(^|[,.;]\\s*)"+p.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"(?=\\s*(?:[,.;]|$))","i"),chipHas=(text,p)=>_chipRe(p).test(text);function chipRemove(text,p){return text.replace(_chipRe(p),"").replace(/^\s*[,;]\s*/,"")}function chipAdd(text,p){const t=text.trimEnd();return t?/[.,;]$/.test(t)?t+" "+p:t+", "+p:p}function syncDesignChips(){const v=$("design-instruct").value;document.querySelectorAll("#design-chips [data-phrase]").forEach(b=>b.setAttribute("aria-pressed",String(chipHas(v,b.dataset.phrase))))}function renderDesignChips(){const tr=window.t||(x=>x),box=$("design-chips");DESIGN_CHIPS.forEach(([label,exclusive,phrases],i)=>{const g=document.createElement("div");g.className="design-chip-group",g.setAttribute("role","group"),g.setAttribute("aria-label",tr(label)),g.innerHTML='',g.firstChild.textContent=tr(label),phrases.forEach(p=>{const b=document.createElement("button");b.type="button",b.className="design-chip",b.dataset.phrase=p,b.dataset.group=i,b.setAttribute("aria-pressed","false"),b.textContent=tr(p),g.appendChild(b)}),box.appendChild(g)}),box.addEventListener("click",e=>{const b=e.target.closest("[data-phrase]");if(!b)return;const ta=$("design-instruct");let v=ta.value;chipHas(v,b.dataset.phrase)?v=chipRemove(v,b.dataset.phrase):(DESIGN_CHIPS[b.dataset.group][1]&&DESIGN_CHIPS[b.dataset.group][2].forEach(o=>{v=chipRemove(v,o)}),v=chipAdd(v,b.dataset.phrase)),ta.value=v,ta.dispatchEvent(new Event("input",{bubbles:!0}))}),box.addEventListener("focusin",syncDesignChips),box.addEventListener("pointerenter",syncDesignChips),$("design-instruct").addEventListener("input",syncDesignChips),syncDesignChips()}renderDesignChips(),document.querySelectorAll(".qwen-sample").forEach(card=>{const sample=QWEN_DESIGN_SAMPLES[card.dataset.qwenSample],state=card.querySelector(".qwen-state"),audio=card.querySelector("audio");card.querySelector(".qwen-use").addEventListener("click",()=>{applyQwenSample(sample),toast("Voice Design sample loaded","success")}),card.querySelector(".qwen-preview").addEventListener("click",async e=>{const btn=e.currentTarget;btn.disabled=!0,state.textContent="Generating preview\u2026";try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(sample.description,sample.text,sample.language,sample))});if(!r.ok){const err=await r.json().catch(()=>({}));throw new Error(err.detail||r.statusText)}const d=await r.json();audio.src="/api/audio/"+d.id,audio.style.display="",audio.play().catch(()=>{}),state.textContent="Preview ready"}catch(err){state.textContent="Preview failed",toast("Sample preview failed: "+err.message,"error")}finally{btn.disabled=!1}})});async function runVoiceDesign(){const baseInstruct=$("design-instruct").value.trim(),sample=$("design-sample-text").value.trim(),dialogue=isDialogueDesign(baseInstruct,sample,currentDesignSource),instruct=baseInstruct;if(!instruct){toast("Enter a voice description first","error");return}$("design-generate-btn").disabled=!0,$("design-status").textContent="Generating\u2026",$("design-result").style.display="none",$("design-save-result").style.display="none",status("Generating voice design\u2026");try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(instruct,sample,$("design-language").value,currentDesignSource,$("design-gender").value))});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();designedFileId2=d.id,trimmedFileId2=null,editingVoiceId2=null,$("design-audio").src="/api/audio/"+d.id,$("design-result").style.display="flex",$("design-status").textContent="Done ("+d.duration.toFixed(1)+" s)";const langCode=DESIGN_LANG_CODE[$("design-language").value]||"EN";$("d-lang").value=langCode,$("d-gender").value=$("design-gender").value,$("d-name").value=designSafeName((currentDesignSource==null?void 0:currentDesignSource.title)||(currentDesignSource==null?void 0:currentDesignSource.name)||$("design-preset-name").value||"VoiceDesign"),_dVoiceIdManual=!1,_updateDVoiceId(),$("d-transcript").value=sample,$("trim-audio").src="/api/audio/"+d.id,$("trim-audio").style.display="",$("no-audio-hint").style.display="none",$("transcript-area").value||($("transcript-area").value=sample),$("design-audio").play().catch(()=>{}),$("design-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Voice generated and export fields filled.","success"),status("Voice design ready")}catch(e){$("design-status").textContent="Failed: "+e.message,toast("Voice design failed: "+e.message,"error"),status("Voice design failed")}finally{$("design-generate-btn").disabled=!1}}$("design-generate-btn").addEventListener("click",runVoiceDesign),$("design-retry-btn").addEventListener("click",runVoiceDesign),$("design-save-btn").addEventListener("click",async()=>{if(!designedFileId2){toast("No voice generated yet","error");return}const voiceId=$("d-voice-id").value.trim();if(!voiceId){toast("Enter a Voice ID first","error"),$("d-name").focus();return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("design-save-btn").disabled=!0;try{const r=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designedFileId2,voice_id:voiceId,transcript:$("d-transcript").value})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const saved=await r.json();await saveMeta(saved.voice_id,{gender:$("d-gender").value,flag:LANG_FLAG_DEFAULT[$("d-lang").value]||void 0,transcript:$("d-transcript").value,note:"Voice Design: "+$("design-instruct").value.slice(0,240),origin:"designed",voice_design_prompt:$("design-instruct").value}).catch(()=>{}),await loadVoiceLibrary().catch(()=>{}),$("design-save-result").style.display="flex",$("design-save-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Exported to Voice Clone Library: "+saved.voice_id,"success"),status("Exported to Voice Clone Library: "+saved.voice_id)}catch(e){toast("Save failed: "+e.message,"error")}finally{$("design-save-btn").disabled=!1}}),$("design-download-btn").addEventListener("click",()=>{if(!designedFileId2)return;const a=document.createElement("a");a.href="/api/audio/"+designedFileId2,a.download=($("d-voice-id").value.trim()||"voice_design")+".wav",a.click()}),(_d=$("clone-refresh-stt-btn"))==null||_d.addEventListener("click",async()=>{var _a2;$("clone-refresh-stt-btn").disabled=!0;try{await refreshSttBackends((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)}finally{$("clone-refresh-stt-btn").disabled=!1}}),$("transcribe-btn").addEventListener("click",async()=>{var _a2,_b2;const id=trimmedFileId2||designedFileId2||currentFileId2;if(!id){toast("No audio to transcribe","error");return}const btn=$("transcribe-btn"),status2=$("transcribe-status"),area=$("transcript-area"),orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Transcribing\u2026',status2&&(status2.className="clone-tr-status working",status2.innerHTML=' Listening to your recording\u2026'),area&&(area.classList.add("transcribing"),area.placeholder="Transcribing your audio \u2014 please wait\u2026");try{const backend=((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)||"configured",r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,backend})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();area&&(area.value=d.text),status2&&(status2.className="clone-tr-status done",status2.innerHTML=' Transcribed'),toast("Transcription complete","success"),(_b2=window._cloneScheduleAutoSave)==null||_b2.call(window)}catch(e){status2&&(status2.className="clone-tr-status error",status2.innerHTML=` Failed: ${escHtml(e.message||String(e))}`),toast("Transcription failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,area&&(area.classList.remove("transcribing"),area.placeholder="Type or auto-transcribe the spoken text\u2026")}}),function(){var _a2,_b2,_c2;const g=id=>document.getElementById(id);let _idManual=!1,_prevName="Sam",_autoSaveTimer=null,_lastAutoSaved="";function buildVoiceId(){var _a3,_b3,_c3;if(_idManual){scheduleAutoSave();return}const lang=((_a3=g("lang-select"))==null?void 0:_a3.value)||"EN",gender=((_b3=g("gender-select"))==null?void 0:_b3.value)||"N",name=(((_c3=g("name-input"))==null?void 0:_c3.value)||"").trim().replace(/\s+/g,""),vid=g("voice-id-input");vid&&name&&(vid.value=`${lang}_${gender}_${name}`,vid.dispatchEvent(new Event("input"))),scheduleAutoSave()}const nameField=g("clone-your-name");nameField==null||nameField.addEventListener("input",()=>{const name=nameField.value.trim();if(!name)return;const sample=g("clone-sample-text");if(sample){const prev=sample.dataset.sampleName||_prevName,re=new RegExp("\\b"+prev.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b");re.test(sample.value)&&(sample.value=sample.value.replace(re,name)),sample.dataset.sampleName=name}_prevName=name;const ni=g("name-input");ni&&(ni.value=name.replace(/\s+/g,"")),buildVoiceId()}),["lang-select","gender-select"].forEach(id=>["change","input"].forEach(ev=>{var _a3;return(_a3=g(id))==null?void 0:_a3.addEventListener(ev,buildVoiceId)})),(_a2=g("name-input"))==null||_a2.addEventListener("input",buildVoiceId),(_b2=g("voice-id-input"))==null||_b2.addEventListener("input",e=>{e.isTrusted&&(_idManual=!0),scheduleAutoSave()}),(_c2=g("transcript-area"))==null||_c2.addEventListener("input",scheduleAutoSave),window._cloneAutoTranscribe=function(){var _a3;const ta=g("transcript-area");if(ta&&ta.value.trim()){scheduleAutoSave();return}(_a3=g("transcribe-btn"))==null||_a3.click()};function canAutoSave(){var _a3,_b3;const vid=(((_a3=g("voice-id-input"))==null?void 0:_a3.value)||"").trim(),tr=(((_b3=g("transcript-area"))==null?void 0:_b3.value)||"").trim();return!!((typeof trimmedFileId2!="undefined"&&trimmedFileId2||typeof designedFileId2!="undefined"&&designedFileId2)&&vid&&tr&&(typeof validateVoiceId!="function"||validateVoiceId(vid)))}function scheduleAutoSave(){const toggle=g("clone-autosave-toggle");!toggle||!toggle.checked||(clearTimeout(_autoSaveTimer),_autoSaveTimer=setTimeout(()=>{var _a3;if(!canAutoSave())return;const sig=(g("voice-id-input").value+"|"+g("transcript-area").value).trim();sig!==_lastAutoSaved&&(_lastAutoSaved=sig,(_a3=g("save-btn"))==null||_a3.click())},1600))}window._cloneScheduleAutoSave=scheduleAutoSave}(),function(){const picker=document.getElementById("clone-src-picker");if(!picker)return;const cards=[...document.querySelectorAll(".clone-src-card")],tabs=[...picker.querySelectorAll(".clone-src-tab")],KEY="clone-src-choice";function show(src){cards.forEach(c=>{c.hidden=c.dataset.src!==src}),tabs.forEach(t=>t.classList.toggle("active",t.dataset.src===src));try{localStorage.setItem(KEY,src)}catch{}}tabs.forEach(t=>t.addEventListener("click",()=>show(t.dataset.src))),show(localStorage.getItem(KEY)||"mic")}(),Object.assign(root,{CLONE_SAMPLE_TEXTS,CLONE_SAMPLE_NAMES,cloneSampleForLang,ws,wsRegions,currentFileId:currentFileId2,trimmedFileId:trimmedFileId2,designedFileId:designedFileId2,editingVoiceId:editingVoiceId2,editingVoicePath:editingVoicePath2,initWaveSurfer,updateRegion,updateDurationLabel,loadAudioId:loadAudioId2,dropZone,fileInput,uploadFile,loadLibraryVoiceAudio:loadLibraryVoiceAudio2,RAW_MIC_CONSTRAINTS,visibleMicrophoneCount,microphoneErrorMessage:microphoneErrorMessage2,requestMicrophoneStream:requestMicrophoneStream2,_cloneMonState,_cloneRenderMeter,_cloneStartMeter,_cloneStartMonitor,_cloneStopMonitor,mediaRec,recChunks,recTimer,recSecs,DESIGN_LANG_CODE,DESIGN_GENDER_WORD,DESIGN_PRESET_KEY,DESIGN_PRESET_SEEDED_KEY,DEFAULT_DESIGN_PRESETS,QWEN_DESIGN_SAMPLES,currentDesignSource,loadDesignPresets:loadDesignPresets2,saveDesignPresets,syncDesignPresetsToServer,seedDesignPresets,refreshDesignPresetSelect,applyDesignPreset,renderDesignPresetLibrary,renderQwenSampleCards,applyQwenSample,isDialogueDesign,voiceDesignPayload,_dVoiceIdManual,designSafeName,voiceIdSafePart,suggestedStyleVoiceId:suggestedStyleVoiceId2,_updateDVoiceId,runVoiceDesign}),root.TTVC=root.TTVC||{},root.TTVC.voiceClone=root.TTVC.voiceClone||{},Object.assign(root.TTVC.voiceClone,{samples:{forLanguage:cloneSampleForLang},audio:{init:initWaveSurfer,load:loadAudioId2,upload:uploadFile},design:{run:runVoiceDesign,buildPayload:voiceDesignPayload}})})(window),setupVocalIsolation({btn:"yt-iso-btn",toggle:"yt-iso-toggle",msg:"yt-iso-msg",getId:()=>currentFileId,getDur:()=>parseFloat($("trim-end").value)||0,apply:(id,dur)=>loadAudioId(id,dur)}),function(){const det=document.getElementById("archetype-gallery");if(!det)return;const tr=s=>window.t?window.t(s):s,$a=id=>document.getElementById(id),PAGE=24,FAV_KEY="ttsvc-archetype-favs",FACETS=[["gender","Gender",["male","female","neutral"]],["age","Age",["child","teenager","young adult","middle-aged","elderly"]],["pitch","Pitch",["high","mid","low"]],["accent","Accent / region",["none","british","american","australian","irish","german","southern"]],["language","Language",["German","English"]]],st={gender:"",age:"",pitch:"",accent:"",language:"",category:"",q:"",favOnly:!1,offset:0,total:0};let favs=new Set,booted=!1,token=0,objUrl="";try{favs=new Set(JSON.parse(localStorage.getItem(FAV_KEY)||"[]"))}catch{}const saveFavs=()=>{try{localStorage.setItem(FAV_KEY,JSON.stringify([...favs]))}catch{}},archSay=m=>{$a("arch-status").textContent=m},lang=()=>window.__APP_LANG__==="de"?"de":"en";function buildChips(){const box=$a("arch-chips");FACETS.forEach(([key,label,vals])=>{const g=document.createElement("div");g.className="design-chip-group",g.setAttribute("role","group"),g.setAttribute("aria-label",tr(label));const l=document.createElement("span");l.className="design-chip-label",l.textContent=tr(label),g.appendChild(l),vals.forEach(v=>{const b=document.createElement("button");b.type="button",b.className="design-chip",b.dataset.k=key,b.dataset.v=v,b.setAttribute("aria-pressed","false"),b.textContent=tr(v),g.appendChild(b)}),box.appendChild(g)});const f=document.createElement("button");f.type="button",f.className="design-chip",f.id="arch-fav-only",f.setAttribute("aria-pressed","false"),f.textContent=tr("Favourites only"),box.appendChild(f),box.addEventListener("click",e=>{const b=e.target.closest("button");b&&(b.id==="arch-fav-only"?st.favOnly=!st.favOnly:st[b.dataset.k]=st[b.dataset.k]===b.dataset.v?"":b.dataset.v,box.querySelectorAll("[data-k]").forEach(x=>x.setAttribute("aria-pressed",String(st[x.dataset.k]===x.dataset.v))),f.setAttribute("aria-pressed",String(st.favOnly)),load(!0))})}function card(a){const name=lang()==="de"?a.name_de:a.name_en,el=document.createElement("article");el.className="arch-card",el.dataset.id=a.id;const h=document.createElement("h3");h.textContent=name;const f=document.createElement("div");f.className="arch-facets",f.textContent=[a.language,...Object.values(a.facets)].map(tr).join(" \xB7 ");const p=document.createElement("p");p.textContent=a.category;const row=document.createElement("div");row.className="arch-actions";const mk=(cls,text,label)=>{const b=document.createElement("button");return b.type="button",b.className=cls,b.textContent=text,label&&b.setAttribute("aria-label",label+": "+name),row.appendChild(b),b};return mk("btn-primary arch-play",tr("Preview"),tr("Preview")),mk("btn-secondary arch-use",tr("Use voice"),tr("Use voice")),mk("btn-secondary arch-fav",favs.has(a.id)?"\u2605":"\u2606",tr("Favourite")).setAttribute("aria-pressed",String(favs.has(a.id))),el.append(h,f,p,row),el}async function load(reset){const my=++token;reset&&(st.offset=0,$a("arch-grid").textContent="");const q=new URLSearchParams({offset:st.offset,limit:st.favOnly?100:PAGE});["gender","age","pitch","accent","language","category","q"].forEach(k=>st[k]&&q.set(k,st[k])),archSay(tr("Loading\u2026"));try{const r=await fetch("/api/archetypes?"+q);if(!r.ok)throw new Error(r.statusText);const d=await r.json();if(my!==token)return;if(!$a("arch-category").options.length){const sel=$a("arch-category");[["",tr("All categories")],...d.categories.map(c=>[c,c])].forEach(([v,t])=>sel.add(new Option(t,v)))}const items=st.favOnly?d.items.filter(a=>favs.has(a.id)):d.items,grid=$a("arch-grid");items.forEach(a=>grid.appendChild(card(a))),st.offset+=d.items.length,st.total=d.total,$a("arch-more").hidden=st.favOnly||st.offset>=d.total,archSay(grid.children.length?grid.children.length+" / "+(st.favOnly?grid.children.length:d.total):tr("No voices match."))}catch{my===token&&archSay(tr("Could not load the gallery."))}}async function play(a,btn){btn.disabled=!0,archSay(tr("Rendering preview\u2026"));try{const r=await fetch("/api/archetypes/"+encodeURIComponent(a)+"/preview",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}objUrl&&URL.revokeObjectURL(objUrl),objUrl=URL.createObjectURL(await r.blob());const au=$a("arch-audio");au.src=objUrl,await au.play().catch(()=>{}),archSay(tr("Preview ready"))}catch(e){archSay(tr("Preview failed")+": "+e.message)}finally{btn.disabled=!1}}async function use(id,btn){btn.disabled=!0,archSay(tr("Saving voice\u2026"));try{const r=await fetch("/api/archetypes/"+encodeURIComponent(id)+"/use",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();if(archSay(tr("Voice saved")+": "+d.voice_id),typeof toast=="function"&&toast(tr("Voice saved")+": "+d.voice_id,"success"),!btn.parentNode.querySelector(".arch-open")){const o=document.createElement("button");o.type="button",o.className="btn-secondary arch-open",o.textContent=tr("Open in Design"),o.dataset.instruct=d.instruct,o.dataset.language=d.language,btn.parentNode.insertBefore(o,btn.nextSibling),o.focus()}typeof loadVoiceLibrary=="function"&&loadVoiceLibrary().catch(()=>{})}catch(e){archSay(tr("Save failed")+": "+e.message),typeof toast=="function"&&toast(e.message,"error")}finally{btn.disabled=!1}}function boot(){if(booted)return;booted=!0,buildChips();let t;$a("arch-q").addEventListener("input",e=>{clearTimeout(t),t=setTimeout(()=>{st.q=e.target.value.trim(),load(!0)},250)}),$a("arch-category").addEventListener("change",e=>{st.category=e.target.value,load(!0)}),$a("arch-more").addEventListener("click",()=>load(!1)),$a("arch-grid").addEventListener("click",e=>{const b=e.target.closest("button");if(!b)return;const id=b.closest(".arch-card").dataset.id;if(b.classList.contains("arch-play"))play(id,b);else if(b.classList.contains("arch-use"))use(id,b);else if(b.classList.contains("arch-fav"))favs.has(id)?favs.delete(id):favs.add(id),saveFavs(),b.textContent=favs.has(id)?"\u2605":"\u2606",b.setAttribute("aria-pressed",String(favs.has(id)));else if(b.classList.contains("arch-open")){const ta=$a("design-instruct");ta.value=b.dataset.instruct,ta.dispatchEvent(new Event("input",{bubbles:!0}));const sel=$a("d-lang");if(sel)for(const o of sel.options)o.value===b.dataset.language&&(sel.value=o.value);ta.scrollIntoView({behavior:matchMedia("(prefers-reduced-motion: reduce)").matches?"auto":"smooth",block:"center"}),ta.focus()}}),load(!0)}det.addEventListener("toggle",()=>{det.open&&boot()}),det.open&&boot()}(),function(root){var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r;$("save-btn").addEventListener("click",async()=>{const id=trimmedFileId||designedFileId||currentFileId;if(!id){toast("No audio ready","error");return}const voiceId=$("voice-id-input").value.trim();if(!voiceId){toast("Enter a Voice ID","error");return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("save-btn").disabled=!0;try{const payload={id,voice_id:voiceId,path:editingVoicePath,transcript:$("transcript-area").value},sendSave=endpoint=>fetch(endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});let fallbackSave=!1,r=await sendSave(editingVoiceId?"/api/voice-replace":"/api/save");if(editingVoiceId&&(r.status===404||r.status===405)&&(fallbackSave=!0,status("Update endpoint unavailable; saving as a regular voice\u2026"),r=await sendSave("/api/save")),!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();$("save-result").style.display="",toast((editingVoiceId?"Voice updated: ":"Voice saved: ")+d.voice_id,"success"),fallbackSave&&editingVoiceId&&voiceId!==editingVoiceId&&((await fetch("/api/voice/"+encodeURIComponent(editingVoiceId),{method:"DELETE"})).ok||status("Saved renamed voice; old library entry may need manual deletion.")),editingVoiceId=null,editingVoicePath=null}catch(e){toast("Save failed: "+e.message,"error")}finally{$("save-btn").disabled=!1}});let _voices2=[],_pendingSelectId2=null,_sortField="id",_sortDir=1,_libraryIssueFilter="",_activePlayButton=null,_activePlayVoiceId=null,_activePlayUrl=null,_libraryLoadPromise2=null;const BENCHMARK_SAMPLE_STORAGE_KEY="vcf-benchmark-sample-text",_VL_CACHE_KEY="ttsvc_vc";function _vlCacheRead(){try{const value=JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY)||"null");return Array.isArray(value)?value:Array.isArray(value==null?void 0:value.voices)?value.voices:null}catch{return null}}function _vlCacheWrite(voices){try{sessionStorage.setItem(_VL_CACHE_KEY,JSON.stringify(voices))}catch{}}function _vlCacheClear(){try{sessionStorage.removeItem(_VL_CACHE_KEY)}catch{}}window._vlCacheClear=_vlCacheClear;const _libraryFilters={text:"",lang:"",sex:"",type:"",rating:""};let _libraryFilterOptionsSig="";const DEFAULT_BENCHMARK_SAMPLE_TEXT="Hello, how are you today? Please read this sample clearly for a fair voice benchmark.",BENCHMARK_PRESETS={de:"Die Welt ist voller Geschichten, die darauf warten, erz\xE4hlt zu werden \u2014 von mutigen Helden und stillen Tr\xE4umern.",en:"The old lighthouse stood firm against the crashing waves, its beam sweeping silently across the dark and restless sea.",de2:"Victor jagt zw\xF6lf Boxk\xE4mpfer quer \xFCber den gro\xDFen Sylter Deich. Im Winter ist es kalt und die Tage sind kurz.",en2:"She sells seashells by the seashore. Peter Piper picked a peck of pickled peppers on a perfectly pleasant afternoon.",reset:DEFAULT_BENCHMARK_SAMPLE_TEXT};function benchmarkSampleText(){const el=$("benchmark-sample-text");return el&&el.value.trim()||DEFAULT_BENCHMARK_SAMPLE_TEXT}function initBenchmarkSampleControls(){var _a2,_b2;const sample=$("benchmark-sample-text");if(!sample)return;sample.value=localStorage.getItem(BENCHMARK_SAMPLE_STORAGE_KEY)||DEFAULT_BENCHMARK_SAMPLE_TEXT,sample.addEventListener("input",debounce(()=>{localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value.trim()),status("Benchmark sample sentence saved")},500)),(_a2=$("benchmark-reset-sample-btn"))==null||_a2.addEventListener("click",()=>{sample.value=DEFAULT_BENCHMARK_SAMPLE_TEXT,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),status("Benchmark sample sentence reset")});const presetSel=$("benchmark-preset-select");presetSel&&presetSel.addEventListener("change",()=>{const key=presetSel.value;if(!key||!BENCHMARK_PRESETS[key]){presetSel.value="";return}sample.value=BENCHMARK_PRESETS[key],localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),presetSel.value="",status("Benchmark sample sentence loaded")}),(_b2=$("benchmark-use-preview-btn"))==null||_b2.addEventListener("click",()=>{var _a3;const text=(_a3=$("preview-text-area"))==null?void 0:_a3.value.trim();if(!text){toast("Preview text is empty","error");return}sample.value=text,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,text),status("Benchmark sample sentence copied from TTS preview")})}function _displaySource(v){if(v.origin)return v.origin;if(v.note){const m=v.note.match(/^Rehearser\s*·\s*(.+?)\s*·/);if(m)return m[1].trim()}if(v.tag){const tags=v.tag.split(",").map(t=>t.trim().toLowerCase());if(tags.includes("fish-audio")||tags.includes("fishaudio"))return"fish-audio"}return""}function getSortValue(v,field){var _a2,_b2,_c2,_d2,_e2,_f2;switch(field){case"has_picture":return v.has_picture?1:0;case"flag":return(v.flag||"").toLowerCase();case"gender":return(_a2={F:0,M:1,N:2}[v.gender])!=null?_a2:3;case"id":return v.id.toLowerCase();case"file_type":return voiceFileType(v);case"duration":return v.duration||0;case"dbfs":return(_b2=voiceDbfs(v))!=null?_b2:-999;case"benchmark":case"factor":return-((_c2=voiceFactor(v))!=null?_c2:-999);case"elapsed":return(_d2=voiceBenchmarkElapsed(v))!=null?_d2:999;case"bench_audio":return(_e2=voiceBenchmarkAudioSec(v))!=null?_e2:999;case"wpm":return(_f2=voiceWpm(v))!=null?_f2:-1;case"transcript":return(v.transcript||"").toLowerCase();case"note":return(v.note||"").toLowerCase();case"source":return(_displaySource(v)||"").toLowerCase();case"seed":return v.seed!=null?v.seed:9999999;case"tag":return(v.tag||"").toLowerCase();case"rating":return v.rating||0;case"enabled":return v.enabled===!1?0:1;default:return""}}function setSort(field){_sortDir=_sortField===field?_sortDir*-1:1,_sortField=field,syncSortHeaders(),renderVoiceList2()}function toggleSortDir(){_sortDir*=-1,syncSortHeaders(),renderVoiceList2()}function syncSortHeaders(){document.querySelectorAll(".vl-header [data-sort], .vl-table-header [data-sort]").forEach(el=>{el.classList.remove("sort-asc","sort-desc"),el.dataset.sort===_sortField&&el.classList.add(_sortDir===1?"sort-asc":"sort-desc")});const sel=document.getElementById("voice-sort-field");sel&&sel.value!==_sortField&&(sel.value=_sortField);const dirBtn=document.getElementById("voice-sort-dir");if(dirBtn){const icon=dirBtn.querySelector(".mdi");icon&&(icon.className=_sortDir===1?"mdi mdi-arrow-up":"mdi mdi-arrow-down"),dirBtn.title=_sortDir===1?"Ascending \u2014 click to reverse":"Descending \u2014 click to reverse"}}document.addEventListener("click",e=>{var _a2,_b2,_c2;if(e.target.closest("#voice-sort-dir")&&toggleSortDir(),e.target.closest("#voice-group-tag-btn")){window._voiceGroupByTag=!window._voiceGroupByTag;try{localStorage.setItem("vl-group-by-tag",window._voiceGroupByTag?"1":"0")}catch{}const btn=document.getElementById("voice-group-tag-btn");btn==null||btn.classList.toggle("active",window._voiceGroupByTag);const icon=btn==null?void 0:btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline"),renderVoiceList2()}else if(e.target.closest("#voice-table-view-btn")){window._voiceTableView=!window._voiceTableView,window._voiceTableView||(window._voiceTableEditMode=!1);try{localStorage.setItem("vl-table-view",window._voiceTableView?"1":"0")}catch{}const btn=document.getElementById("voice-table-view-btn");btn==null||btn.classList.toggle("active",window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");if(editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode)),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",window._voiceTableView),(_b2=document.querySelector(".voices-workbench"))==null||_b2.classList.toggle("table-edit-mode",!!window._voiceTableEditMode),window._voiceTableView){document.querySelectorAll(".edit-open").forEach(r=>r.classList.remove("edit-open"));const inspector=document.getElementById("voices-inspector");inspector&&(inspector.innerHTML='
Table View Mode Click a row to exit table view and edit.
@@ -476,7 +476,7 @@ Mia:Only if you promise not to spill coffee on my notes again... though I guess
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=`
';
-}
-
// Single shared floating preview element, positioned near whichever avatar
// is currently hovered — created lazily on first use rather than once per
// row (there can be dozens of rows in a production).
@@ -1037,24 +1014,11 @@ function _charCardHtml(rec, allChars) {
: tier === 'supporting' ? 'Neben' : '';
const gender = String(sh.gender || '').toLowerCase();
const genderIcon = gender.startsWith('f') ? 'mdi-gender-female' : gender.startsWith('m') ? 'mdi-gender-male' : 'mdi-gender-non-binary';
- const snippet = _libStr(sh.mannerisms || sh.voice_pattern || sh.motivation || sh.backstory || '').slice(0, 190);
const roleLine = _libStr(sh.profession || sh.archetype).trim();
const ageLabel = _charAgeLabel(sh);
- const langLabel = _charLangLabel(rec);
const genderLabel = _charGenderLabel(sh);
const bookLabel = _libStr(rec.book || '');
const lineLabel = sh.line_count != null ? String(sh.line_count) + ' Zeilen' : '';
- // Every character is auto-tagged with the book it was found in (see
- // clUpsert/clMergeTags) — surfacing the chips here is how you tell a
- // character recurring across several books apart from a same-named one
- // that's book-local, since tags (not the single `book` field) are what
- // carries multi-book membership.
- const tagList = String(rec.tags || '').split(',').map(function (t) { return t.trim(); }).filter(Boolean);
- const tagsHtml = tagList.length
- ? '