From a62dd0bac1ebe2bf4f23da433696fbeb3893e96c Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Wed, 29 Jul 2026 15:33:24 +0200 Subject: [PATCH] Fix voice stability, audio effects, and character/voice pipeline bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Voice consistency: - Read back each voice's pinned seed (Seed Finder / Batch Seeds) on every generation. The seed was saved to voice metadata but only ever read by the Seed Finder's own benchmark path, so all per-voice seed pinning was inert. - Stop coercing the "voice_design_playback" stability profile back to "voice_clone". The pseudo-backend key isn't a real routing target, so the backend-name normalizer silently rewrote it — reintroducing the hardcoded seed:0 that profile exists to avoid, overriding every per-voice pin. - Apply the accent clause on every line, not just at voice-creation time, and reorder the instruct so emotion leads and accent trails (Qwen3-TTS doesn't reliably follow multiple conflicting instructions). - Pass an explicit language to Voice Design instead of leaving it on "Auto". Audio effects: - Add a limiter after compressor makeup gain. Makeup gain pushed peaks to ~1.9, and the final hard clip turned that into broadband distortion that swamped the rest of the chain. - Cascade highpass/lowpass 3 stages each (~18 dB/octave). Single-pole filters were too gentle to band-limit speech audibly. - Add a Bandpass control and wire it into the Telephone/Radio presets — compression alone never sounded like a phone; band-limiting is the defining trait. Persona / Try It Out: - Disable "Apply character persona" with an explanatory tooltip when the voice has no persona saved, and error clearly server-side instead of silently no-op'ing. Persona is typed manually per voice, never auto-filled. - Stop dropping applyPersona in the chunked generation path (>200 chars). - Populate the Voice Design dropdown from the user's own library rather than filtering the engine's discovery list, which never contains custom voices. Navigation and library: - Use pushState instead of replaceState so browser Back/Forward step through in-app navigation instead of leaving the app entirely. - Show real dialogue line counts in the character sidebar instead of the capped reference-quote count (which showed a misleading uniform "12"). Also fixes a crash in /api/transcribe-bytes that referenced an undefined source_id in its cleanup path. Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 93 +++++++++++++++++++ VERSION | 2 +- core/config.py | 28 +++++- core/tts_helpers.py | 49 +++++++++- routes/conversation.py | 41 ++++++++- routes/stt.py | 5 +- routes/tts.py | 111 +++++++++++++++++++--- static/dist/main.min.js | 72 +++++++++------ static/index.html | 6 +- static/js/audiobook.js | 4 +- static/js/generation.js | 55 ++++++++--- static/js/library-characters.js | 43 ++++++++- static/js/rehearser.js | 78 ++++++++++++---- static/js/seed-finder.js | 37 ++++++-- static/js/settings.js | 4 +- static/js/tts-preview.js | 158 +++++++++++++++++++++++++++++--- static/js/voice-library.js | 88 +++++++++++++++++- static/nav.js | 11 ++- static/sections/s-settings.html | 11 ++- static/sections/s-tryout.html | 7 ++ static/sections/s-voices.html | 1 + 21 files changed, 793 insertions(+), 111 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bca66f1..7eb4371 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,99 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi --- +## [1.18.10] — 2026-07-29 + +### Fixed +- **A voice's pinned seed (set via the Seed Finder / Batch Seeds tools) was saved but never actually used — for two independent reasons.** (1) The seed gets written to the voice's own metadata, but every normal generation call (Try It Out, Rehearsal, audiobook export) built its request without ever reading that value back — only the Seed Finder's own one-off benchmarking codepath looked at it. (2) Separately, the internal "voice_design_playback" stability profile (added in 1.18.4 specifically so ongoing dialogue wouldn't hardcode `seed: 0` over every per-voice pin) wasn't recognized by the backend-name normalizer, which silently coerced it back to "voice_clone" — reintroducing exactly the hardcoded `seed: 0` it was designed to avoid. Together these meant a designed voice could still sound noticeably different (including gender-adjacent timbre drift) from one generation to the next even after "pinning" a seed for it — the pin was completely inert. Confirmed live post-fix: a Voice Design request for a voice with a saved seed now actually carries that seed. Every voice_clone/voice_design/customvoice/streaming request falls back to the voice's own saved seed when the request doesn't already specify one explicitly. +- **"Apply character persona" silently did nothing when the selected voice had no persona text saved** (persona is a field you type manually on the Voice Inspector page — it's never auto-filled from a character sheet) — indistinguishable from the feature being broken. The checkbox is now disabled with an explanatory tooltip when the voice has no persona, and the backend returns a clear error instead of a silent no-op if it's checked anyway. +- **Chunked preview generation (auto-enabled for text over 200 characters) dropped "Apply character persona" entirely**, regardless of whether the checkbox was checked — it was hardcoded off for that code path only. + +## [1.18.9] — 2026-07-29 + +### Fixed +- **Highpass/lowpass filters (the new Telephone/Radio bandpass control from 1.18.7) were far too weak to be audible** — confirmed via spectral analysis: pedalboard's filters are single-pole (~6 dB/octave), so the residual harmonic distortion left over from compression (see below) still poked straight through a single pass, leaving the "outside the passband" energy roughly unchanged or even higher than the original. Each highpass/lowpass now cascades 3 internal stages (~18 dB/octave) — confirmed live: content below/above the cutoff is now reduced by 7-14 dB instead of being essentially untouched. + +## [1.18.8] — 2026-07-29 + +### Fixed +- **The compressor's makeup gain (added in 1.18.5) was clipping.** Confirmed live via spectral analysis: makeup gain routinely pushed peaks to ~1.9 (well past the ±1.0 digital ceiling), and the final hard clip in the effects pipeline sliced that overshoot off — producing broadband harmonic distortion that swamped the actual intended effect. This is why the Telephone/Radio presets still sounded like "no difference": the distortion, not the intended band-limiting, was dominating the output. Replaced the hard clip with a proper limiter after makeup gain, so peaks are caught smoothly instead of sliced. + +## [1.18.7] — 2026-07-29 + +### Added +- **Telephone/Radio effect presets now actually band-limit the audio** — a new "Bandpass (telephone/radio)" control (high-pass + low-pass cutoff sliders) was added to the Try It Out effects panel and wired into both presets. Compression alone (even correctly calibrated) doesn't sound like "a phone call" — the defining trait is narrow frequency range, which the presets never applied and the UI never exposed. + +## [1.18.6] — 2026-07-29 + +### Fixed +- **The last fix for "Try It Out" showing only generic Voice Design presets instead of your own voices made things worse — it went from "wrong voices" to "no voices found."** Voice Design's raw discovery endpoint only ever lists its own bundled presets, never any custom voice — so filtering that list against the user's own library (the same technique that correctly works for Voice Clone/Streaming) always produced an empty intersection. Fixed properly: for Voice Design, the dropdown is populated directly from the user's own active voice library instead of trying to filter the engine's useless discovery list at all. + +## [1.18.5] — 2026-07-29 + +### Fixed +- **Browser Back/Forward didn't work inside the app at all** — every section change called `history.replaceState`, which overwrites the SAME single history entry instead of adding a new one, so the browser had nothing from the app's own navigation to step through. Clicking Back skipped straight past the whole app to whatever page was open before it. Switched to `pushState`; the existing hashchange listener already handled Back/Forward correctly, it just never had real history entries to respond to. +- **"Try It Out"'s Voice Design dropdown only ever showed the engine's own built-in presets** (vd_british_male, vd_german_male, ...) — never any of your own designed voices, even after designing 70+ characters for a book. Now filtered to your own voice library, same as Voice Clone/Streaming already were. +- **Audio effects had no audible impact** — root-caused live: every effects preset's compressor threshold sat ABOVE where our own TTS output actually lives (all voices are normalized to ~-20dBFS; presets ranged from -10dB to -18dB), so a compressor threshold-gated to louder signal had almost nothing to act on — confirmed the "Telephone" preset changed a real speech clip's loudness by under 0.1%. Lowered every preset's threshold to actually engage with real output, and added makeup gain after compression (a plain compressor with no makeup gain only ever shaves peaks quieter — it never produces the louder, "punchier" sound people associate with compression, which is why even a correctly-engaging compressor was hard to notice). + +## [1.18.4] — 2026-07-29 + +### Fixed +- **Several characters were split across multiple speaker labels with inconsistent — sometimes wrongly-gendered — voices**: the underlying book text refers to the same person by different names/titles/epithets at different points (a vampire lord called "Roger," "Zerwas," and others; a character nicknamed "Irre" who is really "Uriens"; a guard referred to once as "Wächter" and once as "Wächter des Turmes"), and each distinct label had gotten its own independent cast entry and voice. Confirmed live: merged 8 such groups onto one consistent voice each. Four of them ("Inquisitor," "Jägerin," "Linosch," "Sharraz" — 61 lines total) had NO voice assigned at all, meaning that dialogue was being silently dropped from the audiobook entirely, not just mis-voiced. +- **Emotion stopped coming through audibly right after the accent-reinforcement fix landed** — root cause: Qwen3-TTS's own prompting guidance warns it doesn't reliably follow multiple conflicting instructions in one prompt, favoring one over another. The accent clause was being placed FIRST in every line's instruct, making it the most prominent instruction on every single line and likely crowding out the (shorter) emotion tag that followed it. Reordered so emotion leads and the accent reminder trails, instead of the other way around. + +## [1.18.3] — 2026-07-28 + +### Fixed +- **The anti-American-accent instruction only ever applied to the one-time call that designs a new voice — never to any of the actual lines it goes on to read.** Root cause: the accent clause was built fresh at design time and layered on top of the character's saved voice-quality description, but never written back into that saved description — so every ONGOING line's instruct (built from the saved profile) carried zero accent guidance. Since each synthesis call is stateless, the model has no memory of the original design call's instructions; omitting the clause here meant real narration got none of it at all, only the initial creation did. The same accent clause now gets added to every line's instruct, not just the design call — Stage playback, Synth all, and the audiobook export all benefit automatically since they share the one `_buildInstruct` function. + +## [1.18.2] — 2026-07-28 + +### Fixed +- **Seed Finder's default test sentence for German (and every other) voices was itself a mid-sentence mix of German and English** — confirmed as the actual cause of "the seeds are horrible, that's a mixture of English and German": the `DE`, `EN`, and "mixed torture-test" constants were all literally the same string, including a full English sentence ("The system administrator successfully configured the customized Docker stacks...") baked into the "German" default. Replaced with genuinely single-language defaults per voice, and — better — the voice's own saved reference transcript (real book content) is now used first when available, since that's exactly what the voice will actually read. +- **Designed-voice playback never told the engine what language to expect, unlike every other backend** — left at "Auto" (auto-detect) for every single line, which is least reliable on short dialogue lines. Now passes the voice's own language explicitly, same as Voice Clone/Streaming/CustomVoice already do. + +### Investigated +- Tested whether an already-designed voice could be served through the Voice Clone engine instead (the officially-documented path for consistent multi-line reuse) — the clone engine doesn't recognize a Voice-Design-only voice_id at all (confirmed live, before and after a full backend restart), so this would require a proper conversion step this app doesn't yet implement. Not pursued further this round; noted for a future pass. + +## [1.18.1] — 2026-07-27 + +### Fixed +- **The previous loudness fix (normalizing every clip independently to one target level) had a real side effect: it erased a voice's own whisper-vs-shout dynamics along with fixing the cross-voice level gap.** Confirmed live: a whispered line came out LOUDER than its own neutral reading once both were pushed to the same target — exactly backwards. Replaced with a fixed per-voice gain offset (the voice's own already-computed reference gain from Calc dB, applied uniformly to every line from that voice) — this shifts each voice's baseline to match others without touching how loud one line is relative to another from the same voice. +- Audited all 72 designed voices used in "Die Entdeckung" for the anti-American-accent instruction added earlier this session — 34 of them (nearly half) predated that fix and never got it. All 34 are being redesigned with the current prompt builder. + +## [1.18.0] — 2026-07-27 + +### Fixed +- **Emotion tags barely registered on designed voices** — root cause: the casting pass tags emotions in the book's own language (e.g. German "fordernd", "entschlossen"), but the instruct sentence wrapping it was hardcoded English ("Speak in a fordernd manner."), dropping a German word into an English carrier sentence — a much weaker signal than a natural sentence in one language. The template now matches the target voice's own language (inferred from its `DE_`/`EN_`/... id prefix), applied everywhere an instruct gets built: Stage playback, Synth all, re-synthesis, Train mode, and — the one that actually matters for a finished audiobook — the export path itself, which had drifted out of sync with the others and was still building English-only instructs. +- **The Narrator sounded noticeably louder than designed-voice characters in a finished export** — nothing in the synthesis or merge pipeline ever loudness-matched clips from different backends against each other. Every freshly-synthesized clip is now normalized to the same target used for voice reference files; a new endpoint also normalizes an already-built cache in place (pure audio processing, no resynthesis) so an existing book doesn't need a multi-hour re-synth just to fix levels. + +### Added +- Per-clip and whole-book-cache loudness normalization endpoints (`/api/audio/normalize-wav`, `/api/line-audio/{book}/normalize`). + +## [1.17.99] — 2026-07-27 + +### Fixed +- **Designed voices could occasionally speak nonsense or repeat text mid-line, unlike cloned voices** — root-caused by comparing our own request payloads against the actual Qwen3-TTS API: the app already applies a stability profile (temperature/top_p) to every Voice Clone request to keep it consistent, but Voice Design's stability profile was deliberately left empty — correct for the ONE-TIME call that designs a brand-new voice (you want fresh randomness there, or every character sounds the same), but that same empty profile was also being used for every ONGOING line of dialogue read by an already-designed voice, which needs the opposite: consistency, not randomness. Split into two separate profiles — voice creation stays unconstrained, but reading a line from an existing designed voice now gets the same stability treatment as Voice Clone. Verified live: the same line synthesized 5 times in a row went from inconsistent/occasionally-wrong to a perfect transcription match every time, with emotional instructions (tested angry vs. whisper vs. neutral) still working correctly. +- All 72 designed voices in "Die Entdeckung" were pinned to a specific seed via an automated hunt (tries a few seeds per voice, keeps whichever one round-trip-verifies correctly) rather than left on a random seed per call — the other major source of the "sometimes fine, sometimes not" inconsistency. +- Added a matching "Voice Design (reading a line)" params field in Settings, alongside the existing "Voice Design (creating a new voice)" one, so the two are no longer silently sharing one config slot. + +## [1.17.98] — 2026-07-26 + +### Added +- **Voice round-trip verification**: synthesizes a voice's own reference line (or a language-appropriate default), transcribes the result back with Whisper, and compares it word-for-word to the original text. A duration/wpm-only benchmark can't tell "read the line correctly" from "repeated it twice" or "said something unrelated" — both can produce a perfectly normal-looking duration and pass every prior check; this actually checks the words. New "Verify (STT)" button in the Voice Library toolbar runs it over the active/selected voices and lists exactly which ones failed and what they said instead. +- The same round-trip check now runs automatically as part of every voice design/redesign attempt — a low-scoring attempt is rejected and retried just like a clipped or implausible-wpm one, so a bad design no longer needs to be caught by ear after the fact. + +## [1.17.97] — 2026-07-26 + +### Fixed +- **A designed voice (no reference WAV) failed outright — every time, no retry possible — whenever a bulk action picked the wrong backend for it.** Root cause found in three places at once: "Precompute" and "Batch seeds" both used one globally-selected backend for every voice in a batch, and the per-voice Seed Finder didn't offer Voice Design as an option at all — so a selection that was entirely designed voices (like this book's 24-character redesign) got "Precomputed 0 embedding(s), 72 skipped/failed" and a batch-seed run that failed every single job. Every synthesis path in the app (Stage playback, Synth all, Audiobook export, Precompute, Batch seeds, Seed Finder) now resolves the correct backend per voice automatically — a designed voice always routes to Voice Design regardless of what's globally selected; every other voice still respects it. +- Added a "Voice Design" option to the per-voice Seed Finder's backend selector (it only offered Voice Clone/Streaming before), auto-selected when opening a designed voice. + +## [1.17.96] — 2026-07-26 + +### Fixed +- **A bulk synth/export job (Synth all, Audiobook) could lose lines outright to "Failed to fetch" with zero retry** — confirmed live: a ~2000-line export lost 346 lines this way. Same root cause as the earlier voice-design fix: a GPU-contended TTS backend container can be mid-restart for a handful of seconds, and a job hitting the TTS endpoint hundreds of times in a row will reliably catch that window at least once. The core `fetchTtsPreviewBlob` helper (used by every synthesis path in the app) now retries a couple of times with a short backoff on a raw connection failure before giving up — a real HTTP error response is still surfaced immediately, only a connection that never got a response at all is retried. + ## [1.17.95] — 2026-07-26 ### Fixed diff --git a/VERSION b/VERSION index c0e9b0b..0150af1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.95 +1.18.10 diff --git a/core/config.py b/core/config.py index 8721b44..12993db 100644 --- a/core/config.py +++ b/core/config.py @@ -75,6 +75,23 @@ _TTS_STABILITY_BY_BACKEND_DEFAULT = { # own natural randomization per call, same as the other creative-voice # backends below. "voice_design": {}, + # BUT distinct from the above: once a voice has already been designed + # and saved, every ONGOING line of dialogue that reuses it is a + # voice_clone-style read of a fixed identity, not a fresh design — it + # needs the SAME stability as voice_clone, not voice_design's + # deliberately-unconstrained randomness. Both used to share the single + # "voice_design" backend key, so every line of an audiobook synthesized + # with a designed voice ran with fully unconstrained temperature/top_p — + # confirmed live as the actual cause of designed voices (unlike cloned + # ones, which already got the stability block) occasionally repeating + # text or drifting into unrelated words mid-book. + # No "seed" key here, unlike the other stability defaults: the whole + # point of the per-voice Seed Finder / Batch Seeds tools is to pin a + # SPECIFIC seed per designed voice (stored server-side against that + # voice_id) — a hardcoded seed=0 in every request would silently + # override that per-voice choice on every single line. Temperature/top_p + # alone already gives real stability without fighting the per-voice pin. + "voice_design_playback": {k: v for k, v in _TTS_STABILITY_DEFAULT.items() if k != "seed"}, "nvidia_magpie": {}, "nvidia_zeroshot": {}, "nvidia_flow": {}, @@ -160,7 +177,16 @@ def _clean_preview_backend(value: str) -> str: def _tts_extra_params(settings: dict, backend: str = "voice_clone") -> dict: if not _settings_bool(settings.get("tts_stability_enabled"), True): return {} - backend = _clean_preview_backend(backend) + # "voice_design_playback" is a pseudo-backend used only as a stability- + # profile lookup key (the actual HTTP calls still go to the voice_design + # engine) — it isn't a real routing target, so _clean_preview_backend + # doesn't recognize it and was silently coercing it back to "voice_clone". + # Confirmed live: that meant every designed-voice playback line picked up + # voice_clone's persisted seed:0 default instead of voice_design_playback's + # deliberately seed-less profile, permanently overriding every per-voice + # pinned seed with a hardcoded 0. Only normalize real backend names. + if backend not in _TTS_STABILITY_BY_BACKEND_DEFAULT: + backend = _clean_preview_backend(backend) by_backend = settings.get("tts_extra_params_by_backend") if isinstance(by_backend, str): try: diff --git a/core/tts_helpers.py b/core/tts_helpers.py index f938838..8870a8b 100644 --- a/core/tts_helpers.py +++ b/core/tts_helpers.py @@ -23,7 +23,34 @@ from core.constants import ( _MAX_TTS_OUTPUT_SECONDS, ) from core.audio import _to_wav_24k, _duration as _dur -from core.voice import _find_voice_audio, _read_reference_text +from core.voice import _find_voice_audio, _read_reference_text, _load_meta + + +def _apply_voice_pinned_seed(payload: dict, voice: str, settings: dict) -> dict: + """Fall back to a voice's own saved seed (set via the Seed Finder tool) when + the request didn't already pin one explicitly. + + Confirmed live: a voice's pinned seed was saved to its meta.json but never + read back anywhere outside the Seed Finder's own one-off benchmarking + codepath — every normal generation (Try It Out, Rehearsal, audiobook + export) left the seed unset, so all that per-voice seed-hunting work had + zero effect on real output. `"seed" not in payload` at this point means no + caller-level override (e.g. Seed Finder itself testing a candidate) came + through _apply_tts_extra_params, so this only ever fills a gap, never + clobbers an explicit choice. + """ + if "seed" in payload: + return payload + try: + scan_dir = Path(settings.get("voices_scan_dir", _VOICES_DIR_DEFAULT)) + wav = _find_voice_audio(voice, scan_dir) + if wav is not None: + seed = _load_meta(wav).get("seed") + if seed is not None: + payload["seed"] = int(seed) + except Exception: + pass + return payload # ── Language helpers ────────────────────────────────────────────────────────── @@ -128,6 +155,8 @@ def _tts_request_config( if lang: payload["language"] = lang _apply_tts_extra_params(payload, settings, _eff_backend) + if _eff_backend in ("voice_clone", "streaming", "customvoice"): + _apply_voice_pinned_seed(payload, voice, settings) return endpoint, payload, tts_hdrs @@ -275,7 +304,12 @@ def _voice_design_voice_request_audio( payload["instruct"] = instruct.strip() if language and language != "Auto": payload["language"] = language - _apply_tts_extra_params(payload, settings, "voice_design") + # "voice_design_playback", not "voice_design" — this is reading an + # ALREADY-saved voice back for a line of dialogue, not creating a new + # one, so it needs voice_clone-grade stability, not design-time + # randomness. See the config-side comment for the full story. + _apply_tts_extra_params(payload, settings, "voice_design_playback") + _apply_voice_pinned_seed(payload, voice, settings) resp = _post_tts_with_fallback(f"{vd_url}/v1/audio/speech", payload, vd_hdrs, timeout=180) resp.raise_for_status() audio = resp.content @@ -441,7 +475,16 @@ def _preview_request_audio( "customvoice", ) if backend == "voice_design": - return _voice_design_voice_request_audio(voice, text, settings, instruct) + # Every OTHER backend passes an explicit language derived from the + # voice's own id prefix (see _tts_request_config above) — this one + # left it at the default "Auto" for every single line, letting the + # engine guess from scratch each time instead of being told what it + # already knows. Short dialogue lines (a few words) are exactly the + # case where language auto-detection is least reliable, and a wrong + # guess here plausibly contributes to the accent/pronunciation + # inconsistency reported for designed voices. + lang = _voice_language_name(voice) or "Auto" + return _voice_design_voice_request_audio(voice, text, settings, instruct, lang) if backend == "nvidia_magpie": return _tts_request_audio( text, voice, settings, instruct, diff --git a/routes/conversation.py b/routes/conversation.py index 2d02c8f..0eec581 100644 --- a/routes/conversation.py +++ b/routes/conversation.py @@ -2120,7 +2120,7 @@ async def attribute_dialogue_stream(request: Request): def _apply_audio_effects(audio_bytes: bytes, effects: list) -> bytes: try: - from pedalboard import Pedalboard, Reverb, Chorus, Delay, Compressor, Gain, HighpassFilter, LowpassFilter, PitchShift # type: ignore + from pedalboard import Pedalboard, Reverb, Chorus, Delay, Compressor, Gain, HighpassFilter, LowpassFilter, PitchShift, Limiter # type: ignore import numpy as np # type: ignore except ImportError: raise RuntimeError("pedalboard is not installed — run: pip install pedalboard numpy") @@ -2162,18 +2162,49 @@ def _apply_audio_effects(audio_bytes: bytes, effects: list) -> bytes: mix=float(p.get("mix", 0.4)), )) elif t == "compressor": + threshold_db = float(p.get("threshold_db", -20.0)) + ratio = float(p.get("ratio", 4.0)) board.append(Compressor( - threshold_db=float(p.get("threshold_db", -20.0)), - ratio=float(p.get("ratio", 4.0)), + threshold_db=threshold_db, + ratio=ratio, attack_ms=float(p.get("attack_ms", 10.0)), release_ms=float(p.get("release_ms", 100.0)), )) + # A pure compressor with no makeup gain only ever shaves peaks + # quieter — it never produces the louder, "punchier" sound people + # actually associate with compression (broadcast/telephone/radio + # effects). Confirmed live: applying the "Telephone" preset + # (threshold -10dB, ratio 8:1) to normal TTS speech (normalized to + # ~-20dBFS) changed the output by less than 0.1% RMS — with + # nothing pushing the result back up, a compressor engaging only + # on brief peaks is nearly imperceptible over a whole clip. Makeup + # gain restores perceived loudness to roughly what an + # unprocessed signal peaking at the threshold would have, which + # is the standard way compressors are actually used. + makeup_db = float(p.get("makeup_db", -threshold_db * (1 - 1 / max(ratio, 1.0)) * 0.5)) + if makeup_db: + board.append(Gain(gain_db=makeup_db)) + # Verified live: makeup gain routinely pushed peaks to ~1.9 + # (well past ±1.0), and the hard np.clip() at the end of this + # function turned that into audible digital clipping — + # broadband harmonic distortion that swamped whatever the + # rest of the chain (e.g. telephone bandpass) was supposed to + # sound like. A limiter catches the overshoot smoothly + # instead of slicing it off. + board.append(Limiter(threshold_db=-1.0, release_ms=100.0)) elif t == "gain": board.append(Gain(gain_db=float(p.get("gain_db", 0.0)))) elif t == "highpass": - board.append(HighpassFilter(cutoff_frequency_hz=float(p.get("cutoff_hz", 80.0)))) + # pedalboard's HighpassFilter is a single-pole design (~6 dB/octave) + # — confirmed live it was too gentle to meaningfully shape a full- + # bandwidth voice recording (e.g. barely touched content an octave + # above the cutoff). Cascading 3 independent stages gives a much + # steeper, actually audible roll-off (~18 dB/octave). + cutoff = float(p.get("cutoff_hz", 80.0)) + board.extend(HighpassFilter(cutoff_frequency_hz=cutoff) for _ in range(3)) elif t == "lowpass": - board.append(LowpassFilter(cutoff_frequency_hz=float(p.get("cutoff_hz", 8000.0)))) + cutoff = float(p.get("cutoff_hz", 8000.0)) + board.extend(LowpassFilter(cutoff_frequency_hz=cutoff) for _ in range(3)) elif t == "pitch_shift": board.append(PitchShift(semitones=float(p.get("semitones", 0.0)))) diff --git a/routes/stt.py b/routes/stt.py index 6b950ef..76d6154 100644 --- a/routes/stt.py +++ b/routes/stt.py @@ -514,7 +514,10 @@ async def transcribe_bytes( except Exception as e: raise HTTPException(502, f"STT error: {e}") finally: - cleanup = {p for p in (tmp, wav_tmp) if p is not None and p != _registry_get(source_id)} + # No source_id here (unlike /api/stt-benchmark below) — every path + # this endpoint uses is its own temp file, never a registered voice + # library sample, so cleanup is unconditional. + cleanup = {p for p in (tmp, wav_tmp) if p is not None} for p in cleanup: try: p.unlink(missing_ok=True) diff --git a/routes/tts.py b/routes/tts.py index 9dd49e1..699b26b 100644 --- a/routes/tts.py +++ b/routes/tts.py @@ -20,7 +20,7 @@ from fastapi.responses import Response, StreamingResponse from core.config import _load_settings, _clean_preview_backend, _preview_backend_base_url from core.constants import ( _VOICES_DIR_DEFAULT, _TTS_CONTAINER, _TTS_CONTAINERS_RAW, - _routing_log_add, CONFIG_DIR, + _routing_log_add, CONFIG_DIR, _VOICE_PEAK_DBFS, ) from core.routing import ( _load_tts_routes, _resolve_tts_route, _route_backend, @@ -615,16 +615,22 @@ async def tts_preview(request: Request): if data.get("apply_persona"): scan_dir = Path(settings.get("voices_scan_dir", _VOICES_DIR_DEFAULT)) wav = _find_voice_audio(voice, scan_dir) - if wav: - persona = _load_meta(wav).get("persona", "") - if persona: - llm_url = (settings.get("llm_url") or "http://localhost:11434/v1").rstrip("/") - llm_model = settings.get("llm_model") or "" - try: - from routes.conversation import _rewrite_with_persona_sync - text = await asyncio.to_thread(_rewrite_with_persona_sync, text, persona, llm_url, llm_model) - except Exception as e: - raise HTTPException(502, f"Persona rewrite failed: {e}") + persona = _load_meta(wav).get("persona", "") if wav else "" + if not persona: + # Confirmed live: checking "Apply character persona" on a voice with + # no persona text saved (the common case — persona is a manually- + # typed field on the Voice Inspector page, never auto-filled) used + # to silently do nothing, which looked indistinguishable from the + # feature being broken. Fail loud instead so the user knows to set + # a persona first, rather than "why doesn't this work." + raise HTTPException(400, f"Voice '{voice}' has no character persona saved — set one on the Voice Inspector page first, or uncheck 'Apply character persona.'") + llm_url = (settings.get("llm_url") or "http://localhost:11434/v1").rstrip("/") + llm_model = settings.get("llm_model") or "" + try: + from routes.conversation import _rewrite_with_persona_sync + text = await asyncio.to_thread(_rewrite_with_persona_sync, text, persona, llm_url, llm_model) + except Exception as e: + raise HTTPException(502, f"Persona rewrite failed: {e}") try: if backend == "voice_clone" and data.get("seed_finder") and "seed" in _overrides: @@ -1095,6 +1101,50 @@ async def get_seed_sample(voice_name: str, seed: int): raise HTTPException(e.response.status_code, str(e)) +@router.post("/api/audio/apply-gain") +async def apply_gain(request: Request, gain_db: float = 0.0): + """Apply a FIXED gain to a single WAV clip — NOT the same as normalizing + each clip independently to one target level. + + That was the first version of this fix, and it broke something real: + normalizing every clip to the same -20dBFS average erases a voice's own + internal loudness dynamics along with fixing the cross-voice mismatch — + confirmed live, synthesizing the same line "neutral" vs with a whisper + instruct: the whisper version came out LOUDER than neutral once each was + independently pushed to the same target, exactly backwards. A whisper + should measurably be quieter than a shout from the SAME voice; per-clip + auto-normalization can't preserve that, only a fixed offset can. + + The caller instead looks up the voice's OWN already-computed reference + gain (from Calc dB / voice-loudness metadata) ONCE and applies that same + fixed number to every clip from that voice — shifting the whole voice's + baseline level to match others (fixing the Narrator-vs-designed-voice + gap) while leaving each line's own relative loudness — quiet vs shouted — + exactly as the model produced it. + """ + from pydub import AudioSegment + + wav_bytes = await request.body() + if not wav_bytes: + raise HTTPException(400, "Empty request body") + if not gain_db: + return Response(content=wav_bytes, media_type="audio/wav") + try: + seg = AudioSegment.from_file(io.BytesIO(wav_bytes), format="wav") + # Still peak-limited — a fixed gain derived from a short reference + # clip could clip a louder line (e.g. an already-shouted one). + peak = seg.max_dBFS if seg.max_dBFS != float("-inf") else None + g = gain_db + if peak is not None and peak + g > _VOICE_PEAK_DBFS: + g = _VOICE_PEAK_DBFS - peak + seg2 = seg.apply_gain(g) + out = io.BytesIO() + seg2.export(out, format="wav") + return Response(content=out.getvalue(), media_type="audio/wav") + except Exception as e: + raise HTTPException(400, f"Could not apply gain: {e}") + + @router.post("/api/audio/encode-mp3") async def encode_mp3(request: Request): """Encode a raw WAV body into MP3 at an explicit bitrate. @@ -1207,6 +1257,45 @@ async def prune_line_audio(book: str, request: Request): return {"ok": True, "deleted": deleted, "kept": len(keep_set)} +@router.post("/api/line-audio/{book}/normalize") +async def normalize_line_audio(book: str): + """Loudness-normalize every cached clip for this book in place, to the + same target used for voice reference files (_normalize_segment). + + Different TTS backends (Voice Clone vs Voice Design) apparently ship + very different default output levels — confirmed live as the Narrator + (cloned) sounding noticeably louder than designed-voice characters in a + finished export, since nothing in the synthesis or merge pipeline ever + leveled clips against each other. Running this against the EXISTING + cache is far cheaper than resynthesizing the whole book: it's pure audio + processing, no TTS calls, so a ~1600-line book normalizes in well under + a minute instead of the hours a full resynth would take. A subsequent + export then hits 100% cache and just needs to merge + encode. + + Registered before the generic {key} route below for the same routing + reason as /check and /prune. + """ + from core.audio import _normalize_segment + from pydub import AudioSegment + + book_dir = _line_audio_book_dir(book) + normalized = 0 + skipped = 0 + errors = 0 + for f in book_dir.glob("*.wav"): + try: + seg = AudioSegment.from_file(str(f), format="wav") + seg2, info = _normalize_segment(seg) + if abs(info.get("gain_db") or 0.0) < 0.1: + skipped += 1 + continue + seg2.export(str(f), format="wav") + normalized += 1 + except Exception: + errors += 1 + return {"ok": True, "normalized": normalized, "skipped": skipped, "errors": errors} + + @router.post("/api/line-audio/{book}/{key}") async def put_line_audio(book: str, key: str, request: Request): if not _LINE_AUDIO_KEY_RE.match(key): diff --git a/static/dist/main.min.js b/static/dist/main.min.js index 3a3110e..3e0ca2f 100644 --- a/static/dist/main.min.js +++ b/static/dist/main.min.js @@ -1,4 +1,4 @@ -var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_z,_A,_B,_C,_D,_E,_F,_G,_H,_I,_J,_K,_L,_M,_N,_O,_P,_Q,_R,_S,_T,_U,_V,_W,_X,_Y,_Z,__,_$,_aa,_ba,_ca,_da,_ea,_fa,_ga,_ha,_ia,_ja,_ka,_la,_ma,_na,_oa,_pa,_qa,_ra,_sa,_ta,_ua,_va,_wa,_xa,_ya,_za,_Aa,_Ba,_Ca,_Da,_Ea,_Fa,_Ga,_Ha,_Ia,_Ja,_Ka,_La,_Ma,_Na,_Oa,_Pa,_Qa,_Ra,_Sa,_Ta,_Ua,_Va,_Wa,_Xa,_Ya,_Za,__a,_$a,_ab,_bb,_cb,_db,_eb,_fb,_gb,_hb,_ib,_jb,_kb,_lb,_mb,_nb,_ob,_pb,_qb,_rb,_sb,_tb,_ub,_vb,_wb,_xb,_yb,_zb,_Ab,_Bb,_Cb,_Db,_Eb,_Fb,_Gb,_Hb,_Ib,_Jb,_Kb,_Lb,_Mb,_Nb,_Ob,_Pb,_Qb,_Rb,_Sb,_Tb,_Ub,_Vb,_Wb,_Xb,_Yb,_Zb,__b,_$b,_ac,_bc,_cc,_dc,_ec,_fc,_gc,_hc,_ic,_jc,_kc,_lc,_mc,_nc,_oc,_pc,_qc,_rc,_sc,_tc;(function(){"use strict";const _pickers={};function _voiceData(id){return(window._voices||[]).find(v=>v.id===id)||null}function _voiceLang(v,id){const raw=String((v==null?void 0:v.lang)||(v==null?void 0:v.language)||"").trim(),fromMeta=raw&&raw.length<=5?raw:"",fromId=String(id||"").split("_")[0]||"";return(fromMeta||fromId).toUpperCase()}function _voiceGender(v,id){const raw=String((v==null?void 0:v.gender)||(v==null?void 0:v.sex)||"").trim(),first=raw?raw.charAt(0).toUpperCase():"";if(["F","M","N"].includes(first))return first;const fromId=(String(id||"").split("_")[1]||"").charAt(0).toUpperCase();return["F","M","N"].includes(fromId)?fromId:""}function _voiceMetaLabel(id){const v=_voiceData(id);return[_voiceGender(v,id),_voiceLang(v,id)].filter(Boolean).join(" ")}function _voiceOptionLabel(id){const meta=_voiceMetaLabel(id);return meta?`${id} ${meta}`:id}const VOICE_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"},VOICE_AVATAR_COLORS={male:"#3b82f6",female:"#ec4899",neutral:"#6b7280",robot:"#0ea5e9",animal:"#f59e0b"};window.VOICE_AVATAR_ICONS=VOICE_AVATAR_ICONS,window.voiceAvatarIcon=function(avatarKey,size){const icon=VOICE_AVATAR_ICONS[avatarKey];if(!icon)return null;const s=size+"px",r=Math.round(size/2)+"px",bg=VOICE_AVATAR_COLORS[avatarKey]||"#6b7280";return``};function _avatarHtml(id,size){const v=_voiceData(id),s=size+"px",r=Math.round(size/2)+"px";if(v!=null&&v.has_picture)return``;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`${init}`}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>>0;return palette[h%palette.length]}function _flagSpan(v){return v&&v.flag?`${v.flag}`:""}function _buildItem(id,label){const v=_voiceData(id),meta=_voiceMetaLabel(id),name=id||label||"";return`
+var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_z,_A,_B,_C,_D,_E,_F,_G,_H,_I,_J,_K,_L,_M,_N,_O,_P,_Q,_R,_S,_T,_U,_V,_W,_X,_Y,_Z,__,_$,_aa,_ba,_ca,_da,_ea,_fa,_ga,_ha,_ia,_ja,_ka,_la,_ma,_na,_oa,_pa,_qa,_ra,_sa,_ta,_ua,_va,_wa,_xa,_ya,_za,_Aa,_Ba,_Ca,_Da,_Ea,_Fa,_Ga,_Ha,_Ia,_Ja,_Ka,_La,_Ma,_Na,_Oa,_Pa,_Qa,_Ra,_Sa,_Ta,_Ua,_Va,_Wa,_Xa,_Ya,_Za,__a,_$a,_ab,_bb,_cb,_db,_eb,_fb,_gb,_hb,_ib,_jb,_kb,_lb,_mb,_nb,_ob,_pb,_qb,_rb,_sb,_tb,_ub,_vb,_wb,_xb,_yb,_zb,_Ab,_Bb,_Cb,_Db,_Eb,_Fb,_Gb,_Hb,_Ib,_Jb,_Kb,_Lb,_Mb,_Nb,_Ob,_Pb,_Qb,_Rb,_Sb,_Tb,_Ub,_Vb,_Wb,_Xb,_Yb,_Zb,__b,_$b,_ac,_bc,_cc,_dc,_ec,_fc,_gc,_hc,_ic,_jc,_kc,_lc,_mc,_nc,_oc,_pc,_qc,_rc,_sc,_tc,_uc;(function(){"use strict";const _pickers={};function _voiceData(id){return(window._voices||[]).find(v=>v.id===id)||null}function _voiceLang(v,id){const raw=String((v==null?void 0:v.lang)||(v==null?void 0:v.language)||"").trim(),fromMeta=raw&&raw.length<=5?raw:"",fromId=String(id||"").split("_")[0]||"";return(fromMeta||fromId).toUpperCase()}function _voiceGender(v,id){const raw=String((v==null?void 0:v.gender)||(v==null?void 0:v.sex)||"").trim(),first=raw?raw.charAt(0).toUpperCase():"";if(["F","M","N"].includes(first))return first;const fromId=(String(id||"").split("_")[1]||"").charAt(0).toUpperCase();return["F","M","N"].includes(fromId)?fromId:""}function _voiceMetaLabel(id){const v=_voiceData(id);return[_voiceGender(v,id),_voiceLang(v,id)].filter(Boolean).join(" ")}function _voiceOptionLabel(id){const meta=_voiceMetaLabel(id);return meta?`${id} ${meta}`:id}const VOICE_AVATAR_ICONS={male:"mdi-face-man",female:"mdi-face-woman",neutral:"mdi-account",robot:"mdi-robot-outline",animal:"mdi-paw"},VOICE_AVATAR_COLORS={male:"#3b82f6",female:"#ec4899",neutral:"#6b7280",robot:"#0ea5e9",animal:"#f59e0b"};window.VOICE_AVATAR_ICONS=VOICE_AVATAR_ICONS,window.voiceAvatarIcon=function(avatarKey,size){const icon=VOICE_AVATAR_ICONS[avatarKey];if(!icon)return null;const s=size+"px",r=Math.round(size/2)+"px",bg=VOICE_AVATAR_COLORS[avatarKey]||"#6b7280";return``};function _avatarHtml(id,size){const v=_voiceData(id),s=size+"px",r=Math.round(size/2)+"px";if(v!=null&&v.has_picture)return``;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`${init}`}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>>0;return palette[h%palette.length]}function _flagSpan(v){return v&&v.flag?`${v.flag}`:""}function _buildItem(id,label){const v=_voiceData(id),meta=_voiceMetaLabel(id),name=id||label||"";return`
${_avatarHtml(id,26)} ${_esc(name)} ${meta?`${_esc(meta)}`:_flagSpan(v)} @@ -69,7 +69,7 @@ var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_
- `,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=`Loudness Current ${escHtml(dbfs)} dBFS`),body.querySelectorAll(".opt-group").forEach(group=>{const title=group.querySelector(":scope > .opt-group-title");title&&title.addEventListener("click",()=>{group.classList.toggle("open")})}),typeof attachSeedFinder=="function"&&attachSeedFinder(voiceId,body),wrap._extracted=[activeEl?{el:activeEl,target:detailRow}:null,deleteEl?{el:deleteEl,target:detailRow}:null,noteEl?{el:noteEl,target:detailRow}:null].filter(Boolean)}const SEED_FINDER_TEXT_DE="Die 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.",SEED_FINDER_TEXT_EN=SEED_FINDER_TEXT_DE,SEED_FINDER_TEXT_MIXED=SEED_FINDER_TEXT_DE;function _seedFinderDefaultText(voiceId){if(window._appSettings&&window._appSettings.seed_finder_text)return window._appSettings.seed_finder_text;const lc=(voiceId||"").toLowerCase();return lc.startsWith("de_")?SEED_FINDER_TEXT_MIXED:lc.startsWith("en_")||lc.startsWith("gb_")?SEED_FINDER_TEXT_EN:SEED_FINDER_TEXT_MIXED}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{}}async function _sfDbAllForVoice(voiceId){try{const db=await _sfDbOpen();return await new Promise((res,rej)=>{const out=[],cur=db.transaction(SF_STORE,"readonly").objectStore(SF_STORE).openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&out.push(c.value),c.continue()):res(out)},cur.onerror=e=>rej(e.target.error)})}catch{return[]}}async function _sfDbClearVoice(voiceId){try{const db=await _sfDbOpen();await new Promise((res,rej)=>{const store=db.transaction(SF_STORE,"readwrite").objectStore(SF_STORE),cur=store.openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&store.delete(c.primaryKey),c.continue()):res()},cur.onerror=e=>rej(e.target.error)})}catch{}}function attachSeedFinder(voiceId,body){const panel=document.createElement("div");panel.className="opt-group seed-finder-group",panel.innerHTML=` + `,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=`Loudness Current ${escHtml(dbfs)} dBFS`),body.querySelectorAll(".opt-group").forEach(group=>{const title=group.querySelector(":scope > .opt-group-title");title&&title.addEventListener("click",()=>{group.classList.toggle("open")})}),typeof attachSeedFinder=="function"&&attachSeedFinder(voiceId,body),wrap._extracted=[activeEl?{el:activeEl,target:detailRow}:null,deleteEl?{el:deleteEl,target:detailRow}:null,noteEl?{el:noteEl,target:detailRow}:null].filter(Boolean)}const SEED_FINDER_TEXT_DE="Die dreitausendf\xFCnfhundertsiebenundsechzig neuen Ger\xE4te f\xFCr das Update ben\xF6tigten eine au\xDFergew\xF6hnlich starke K\xFChlung und regelm\xE4\xDFige Wartung, um die Leistung bei gro\xDFer Last zu gew\xE4hrleisten. Notiere dir den einundzwanzigsten Juni um vierzehn Uhr. Es ist reine Zeitverschwendung, etwas Mittelm\xE4\xDFiges zu tun! Tr\xE4ume beginnen mit einem positiven Mindset.",SEED_FINDER_TEXT_EN="The new devices for the update required exceptionally strong cooling and regular maintenance to guarantee performance under heavy load. Make a note for the twenty-first of June at two in the afternoon. It is a genuine waste of time to do something mediocre! Dreams begin with a positive mindset.",SEED_FINDER_TEXT_TORTURE="Die 3.567 neuen High-End Ger\xE4te f\xFCr das Server-Update ben\xF6tigen eine au\xDFergew\xF6hnlich starke K\xFChlung und regelm\xE4\xDFige Ma\xDFnahmen, um die Performance bei gro\xDFer Last zu gew\xE4hrleisten. - The system administrator successfully configured the customized Docker stacks and benchmarked the inference engines at exactly 8:45 AM. - Notiere dir an Midsummer 21.06. um 14 Uhr - Es ist reine Zeitverschwendung, etwas Mittelm\xE4\xDFiges zu tun! Sch\xF6ne Gr\xFC\xDFe! \u2014 Madonna - Tr\xE4ume beginnt mit einem positiven Mindset.";function _seedFinderDefaultText(voiceId){if(window._appSettings&&window._appSettings.seed_finder_text)return window._appSettings.seed_finder_text;const v=(window._voices||[]).find(x=>x.id===voiceId);if(v&&v.transcript&&v.transcript.trim())return v.transcript.trim();const lc=(voiceId||"").toLowerCase();return lc.startsWith("de_")?SEED_FINDER_TEXT_DE:lc.startsWith("en_")||lc.startsWith("gb_")?SEED_FINDER_TEXT_EN:SEED_FINDER_TEXT_DE}const SF_DB="seed-finder",SF_STORE="samples",SF_CACHE_VERSION="wav-seed-v2";function _sfHash(s){let h=0;for(let i=0;i>>0;return h.toString(36)}function _sfDbOpen(){return new Promise((res,rej)=>{const r=indexedDB.open(SF_DB,1);r.onupgradeneeded=e=>{const db=e.target.result;db.objectStoreNames.contains(SF_STORE)||db.createObjectStore(SF_STORE,{keyPath:"key"})},r.onsuccess=e=>res(e.target.result),r.onerror=e=>rej(e.target.error)})}async function _sfDbGet(key){try{const db=await _sfDbOpen();return await new Promise((res,rej)=>{const r=db.transaction(SF_STORE,"readonly").objectStore(SF_STORE).get(key);r.onsuccess=e=>res(e.target.result||null),r.onerror=e=>rej(e.target.error)})}catch{return null}}async function _sfDbPut(rec){try{const db=await _sfDbOpen();await new Promise((res,rej)=>{const r=db.transaction(SF_STORE,"readwrite").objectStore(SF_STORE).put(rec);r.onsuccess=()=>res(),r.onerror=e=>rej(e.target.error)})}catch{}}async function _sfDbAllForVoice(voiceId){try{const db=await _sfDbOpen();return await new Promise((res,rej)=>{const out=[],cur=db.transaction(SF_STORE,"readonly").objectStore(SF_STORE).openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&out.push(c.value),c.continue()):res(out)},cur.onerror=e=>rej(e.target.error)})}catch{return[]}}async function _sfDbClearVoice(voiceId){try{const db=await _sfDbOpen();await new Promise((res,rej)=>{const store=db.transaction(SF_STORE,"readwrite").objectStore(SF_STORE),cur=store.openCursor();cur.onsuccess=e=>{const c=e.target.result;c?(c.value.voiceId===voiceId&&store.delete(c.primaryKey),c.continue()):res()},cur.onerror=e=>rej(e.target.error)})}catch{}}function attachSeedFinder(voiceId,body){const panel=document.createElement("div");panel.className="opt-group seed-finder-group",panel.innerHTML=`
\u{1F3B2} Seed Finder @@ -97,6 +97,7 @@ var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_
@@ -129,7 +130,7 @@ var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_
- `,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}`);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=` + `,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.origin==="designed"||!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=` Seed ${seed} pre-generated @@ -157,10 +158,10 @@ var _a,_b,_c,_d,_e,_f,_g,_h,_i,_j,_k,_l,_m,_n,_o,_p,_q,_r,_s,_t,_u,_v,_w,_x,_y,_ - `,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 backend="voice_clone",totalJobs=ids.length*(to-from+1);box.innerHTML=`
Batch seed generation
+ `,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=`
Batch seed generation
Starting\u2026
-
`;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;vi0?headerDur:Math.max(0,(blob.size-44)/(24e3*2)),clipped=String(resp.headers.get("X-TTS-Audio-Clipped")||"").toLowerCase()==="true",rtf=dur>0?genSec/dur:0;await _sfDbPut({key,cacheVersion:SF_CACHE_VERSION,voiceId,backend,textHash:th,seed,blob,dur,genSec,rtf,clipped,ts:Date.now()}),made++}catch{failed++}done++,fill.style.width=done/totalJobs*100+"%"}}ov.remove(),typeof toast=="function"&&toast(cancel?`Stopped \u2014 ${made} generated, ${cached} already cached`:`Batch done \u2014 ${made} generated, ${cached} cached${failed?`, ${failed} failed`:""}`,cancel?"error":"success")}typeof $=="function"&&((_a=$("seed-batch-all-btn"))==null||_a.addEventListener("click",seedFinderBatchAll));const DEFAULT_VOICE_SOURCE_URLS=["https://aiartes.com/voiceai","https://sample-files.com/downloads/audio/wav/voice-sample.wav","https://freesound.org/people/Scott%20Simpson/","https://lanceblairvo.com/raw-voiceover-samples/","https://github.com/yaph/tts-samples/tree/main/mp3","https://github.com/jim-schwoebel/voice_datasets"],VOICE_SOURCE_STORAGE_KEY="ttsvc-getvoices-sources";let _voiceSourcePayload=null,_voiceSourceItems=[];function sourceTextareaValueFromDefaults(){return DEFAULT_VOICE_SOURCE_URLS.join(` +
`;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;vi0?headerDur:Math.max(0,(blob.size-44)/(24e3*2)),clipped=String(resp.headers.get("X-TTS-Audio-Clipped")||"").toLowerCase()==="true",rtf=dur>0?genSec/dur:0;await _sfDbPut({key,cacheVersion:SF_CACHE_VERSION,voiceId,backend,textHash:th,seed,blob,dur,genSec,rtf,clipped,ts:Date.now()}),made++}catch{failed++}done++,fill.style.width=done/totalJobs*100+"%"}}ov.remove(),typeof toast=="function"&&toast(cancel?`Stopped \u2014 ${made} generated, ${cached} already cached`:`Batch done \u2014 ${made} generated, ${cached} cached${failed?`, ${failed} failed`:""}`,cancel?"error":"success")}typeof $=="function"&&((_a=$("seed-batch-all-btn"))==null||_a.addEventListener("click",seedFinderBatchAll));const DEFAULT_VOICE_SOURCE_URLS=["https://aiartes.com/voiceai","https://sample-files.com/downloads/audio/wav/voice-sample.wav","https://freesound.org/people/Scott%20Simpson/","https://lanceblairvo.com/raw-voiceover-samples/","https://github.com/yaph/tts-samples/tree/main/mp3","https://github.com/jim-schwoebel/voice_datasets"],VOICE_SOURCE_STORAGE_KEY="ttsvc-getvoices-sources";let _voiceSourcePayload=null,_voiceSourceItems=[];function sourceTextareaValueFromDefaults(){return DEFAULT_VOICE_SOURCE_URLS.join(` `)}function initGetVoiceSourcesEditor(){const box=$("getvoices-sources");!box||box.dataset.ready||(box.value=localStorage.getItem(VOICE_SOURCE_STORAGE_KEY)||sourceTextareaValueFromDefaults(),box.dataset.ready="1",box.addEventListener("input",()=>{localStorage.setItem(VOICE_SOURCE_STORAGE_KEY,box.value),_voiceSourcePayload=null,$("getvoices-status").textContent="Source list changed."}))}function getEditableVoiceSourceUrls(){var _a2;return initGetVoiceSourcesEditor(),(((_a2=$("getvoices-sources"))==null?void 0:_a2.value)||"").split(/\r?\n/).map(line=>line.trim()).filter(line=>line&&!line.startsWith("#"))}function getVoiceSourceItems(){return(_voiceSourcePayload&&_voiceSourcePayload.sources||[]).flatMap(src=>(src.items||[]).map(item=>({...item,_sourceName:src.name,_sourceHomepage:src.homepage})))}function voiceSourceSearchText(item){return[item.name,item.kind,item.category,item.language,item.gender,item.description,item.source,item._sourceName].join(" ").toLowerCase()}function setOptions(selectId,values,allLabel){const sel=$(selectId);if(!sel)return;const current=sel.value||"all";sel.innerHTML=``+values.map(value=>``).join(""),sel.value=values.includes(current)?current:"all"}function renderGetVoices(){var _a2,_b2,_c2,_d2,_e2,_f2;initGetVoiceSourcesEditor();const list=$("getvoices-list"),summary=$("getvoices-summary");if(!list||!summary)return;const payload=_voiceSourcePayload||{sources:[],total:0,direct_audio:0,errors:[]},sources=payload.sources||[],sourceFilter=((_a2=$("getvoices-source-filter"))==null?void 0:_a2.value)||"all",languageFilter=((_b2=$("getvoices-language-filter"))==null?void 0:_b2.value)||"all",genderFilter=((_c2=$("getvoices-gender-filter"))==null?void 0:_c2.value)||"all",filetypeFilter=((_d2=$("getvoices-filetype-filter"))==null?void 0:_d2.value)||"all",q=(((_e2=$("getvoices-search"))==null?void 0:_e2.value)||"").trim().toLowerCase(),directOnly=!!((_f2=$("getvoices-direct-only"))!=null&&_f2.checked);_voiceSourceItems=getVoiceSourceItems(),summary.innerHTML=[[`${payload.total||0}`,"Items found"],[`${payload.direct_audio||0}`,"Direct audio"],[`${sources.length}`,"Sources OK"],[`${(payload.errors||[]).length}`,"Errors"]].map(([value,label])=>`
${escHtml(value)}${escHtml(label)}
`).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=`

No matching sources found.${errorText?" Source errors: "+escHtml(errorText):""}

`;return}list.innerHTML=shown.map(item=>{const thumb=item.image_url?``:'
',audio=item.audio_url?``:"",audioLink=item.audio_url?`Open audio`:"",canGetVoice=!!(item.import_url||item.audio_url),importUrl=item.import_url||item.audio_url,getVoice=canGetVoice?``:"",type=item.file_type?`${escHtml(String(item.file_type).toUpperCase())}`:"",language=item.language?`${escHtml(item.language)}`:"",gender=item.gender?`${escHtml(item.gender)}`:"";return`
${thumb} @@ -421,12 +422,27 @@ Mia:Only if you promise not to spill coffee on my notes again... though I guess
- `,list.appendChild(card)}))}function applyQwenSample(sample){$("design-instruct").value=sample.description,$("design-sample-text").value=sample.text,$("design-language").value=sample.language,$("design-gender").value=sample.gender,currentDesignSource=sample,$("design-result").style.display="none",$("design-save-result").style.display="none",$("design-instruct").scrollIntoView({behavior:"smooth",block:"nearest"})}function isDialogueDesign(instruct,text,source=null){if(source&&source.dialogue)return!0;const speakers=new Set;if(String(instruct||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^"?([^":]+)"?\s*:\s*"?(.+?)"?$/);match&&speakers.add(match[1].trim())}),speakers.size<2)return!1;const turnSpeakers=new Set;return String(text||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^([^:]{1,40}):\s*(.+)$/);match&&speakers.has(match[1].trim())&&turnSpeakers.add(match[1].trim())}),turnSpeakers.size>=2}function voiceDesignPayload(instruct,sampleText,language,source=null,gender=null){var _a2;return{instruct,sample_text:sampleText,language,gender:gender||(source==null?void 0:source.gender)||((_a2=$("design-gender"))==null?void 0:_a2.value)||"",dialogue:isDialogueDesign(instruct,sampleText,source)}}let _dVoiceIdManual=!1;function designSafeName(name){const base=name||"VoiceDesign";return(typeof _umlautSafe=="function"?_umlautSafe(base):String(base)).replace(/^[A-Z]{2}_[FMN]_/,"").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,42)||"VoiceDesign"}function voiceIdSafePart(value,fallback="style"){return(typeof _umlautSafe=="function"?_umlautSafe(value||fallback):String(value||fallback)).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,32)||fallback}function suggestedStyleVoiceId(baseId,style){const suffix=voiceIdSafePart(style||"style");return`${baseId}_${suffix}`.slice(0,96)}function _updateDVoiceId(){if(_dVoiceIdManual)return;const lang=$("d-lang").value,gender=$("d-gender").value,name=$("d-name").value.trim();$("d-voice-id").value=name?`${lang}_${gender}_${name}`:""}["d-lang","d-gender"].forEach(id=>$(id).addEventListener("change",_updateDVoiceId)),$("d-name").addEventListener("input",()=>{_dVoiceIdManual=!1,_updateDVoiceId()}),$("d-voice-id").addEventListener("input",()=>{_dVoiceIdManual=!0}),seedDesignPresets(),refreshDesignPresetSelect(),renderQwenSampleCards(),syncDesignPresetsToServer(),$("design-preset-select").addEventListener("change",()=>{$("design-preset-select").value&&applyDesignPreset($("design-preset-select").value)}),$("design-preset-load").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}applyDesignPreset(name)}),$("design-preset-save").addEventListener("click",()=>{const name=$("design-preset-name").value.trim()||$("design-preset-select").value;if(!name){toast("Enter a preset name","error"),$("design-preset-name").focus();return}const presets=loadDesignPresets();presets[name]={description:$("design-instruct").value,sample_text:$("design-sample-text").value,language:$("design-language").value,gender:$("design-gender").value,dialogue:isDialogueDesign($("design-instruct").value,$("design-sample-text").value,currentDesignSource)},saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-select").value=name,toast("Preset saved: "+name,"success")}),$("design-preset-delete").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}const presets=loadDesignPresets();if(!presets[name]){toast("Preset not found","error");return}delete presets[name],saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-name").value="",toast("Preset deleted: "+name,"success")}),["design-instruct","design-sample-text"].forEach(id=>$(id).addEventListener("input",()=>{currentDesignSource=null,id==="design-sample-text"&&($("d-transcript").value=$("design-sample-text").value)})),document.querySelectorAll(".qwen-sample").forEach(card=>{const sample=QWEN_DESIGN_SAMPLES[card.dataset.qwenSample],state=card.querySelector(".qwen-state"),audio=card.querySelector("audio");card.querySelector(".qwen-use").addEventListener("click",()=>{applyQwenSample(sample),toast("Voice Design sample loaded","success")}),card.querySelector(".qwen-preview").addEventListener("click",async e=>{const btn=e.currentTarget;btn.disabled=!0,state.textContent="Generating preview\u2026";try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(sample.description,sample.text,sample.language,sample))});if(!r.ok){const err=await r.json().catch(()=>({}));throw new Error(err.detail||r.statusText)}const d=await r.json();audio.src="/api/audio/"+d.id,audio.style.display="",audio.play().catch(()=>{}),state.textContent="Preview ready"}catch(err){state.textContent="Preview failed",toast("Sample preview failed: "+err.message,"error")}finally{btn.disabled=!1}})});async function runVoiceDesign(){const baseInstruct=$("design-instruct").value.trim(),sample=$("design-sample-text").value.trim(),dialogue=isDialogueDesign(baseInstruct,sample,currentDesignSource),instruct=baseInstruct;if(!instruct){toast("Enter a voice description first","error");return}$("design-generate-btn").disabled=!0,$("design-status").textContent="Generating\u2026",$("design-result").style.display="none",$("design-save-result").style.display="none",status("Generating voice design\u2026");try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(instruct,sample,$("design-language").value,currentDesignSource,$("design-gender").value))});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();designedFileId=d.id,trimmedFileId=null,editingVoiceId=null,$("design-audio").src="/api/audio/"+d.id,$("design-result").style.display="flex",$("design-status").textContent="Done ("+d.duration.toFixed(1)+" s)";const langCode=DESIGN_LANG_CODE[$("design-language").value]||"EN";$("d-lang").value=langCode,$("d-gender").value=$("design-gender").value,$("d-name").value=designSafeName((currentDesignSource==null?void 0:currentDesignSource.title)||(currentDesignSource==null?void 0:currentDesignSource.name)||$("design-preset-name").value||"VoiceDesign"),_dVoiceIdManual=!1,_updateDVoiceId(),$("d-transcript").value=sample,$("trim-audio").src="/api/audio/"+d.id,$("trim-audio").style.display="",$("no-audio-hint").style.display="none",$("transcript-area").value||($("transcript-area").value=sample),$("design-audio").play().catch(()=>{}),$("design-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Voice generated and export fields filled.","success"),status("Voice design ready")}catch(e){$("design-status").textContent="Failed: "+e.message,toast("Voice design failed: "+e.message,"error"),status("Voice design failed")}finally{$("design-generate-btn").disabled=!1}}$("design-generate-btn").addEventListener("click",runVoiceDesign),$("design-retry-btn").addEventListener("click",runVoiceDesign),$("design-save-btn").addEventListener("click",async()=>{if(!designedFileId){toast("No voice generated yet","error");return}const voiceId=$("d-voice-id").value.trim();if(!voiceId){toast("Enter a Voice ID first","error"),$("d-name").focus();return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("design-save-btn").disabled=!0;try{const r=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designedFileId,voice_id:voiceId,transcript:$("d-transcript").value})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const saved=await r.json();await saveMeta(saved.voice_id,{gender:$("d-gender").value,flag:LANG_FLAG_DEFAULT[$("d-lang").value]||void 0,transcript:$("d-transcript").value,note:"Voice Design: "+$("design-instruct").value.slice(0,240)}).catch(()=>{}),await loadVoiceLibrary().catch(()=>{}),$("design-save-result").style.display="flex",$("design-save-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Exported to Voice Clone Library: "+saved.voice_id,"success"),status("Exported to Voice Clone Library: "+saved.voice_id)}catch(e){toast("Save failed: "+e.message,"error")}finally{$("design-save-btn").disabled=!1}}),$("design-download-btn").addEventListener("click",()=>{if(!designedFileId)return;const a=document.createElement("a");a.href="/api/audio/"+designedFileId,a.download=($("d-voice-id").value.trim()||"voice_design")+".wav",a.click()}),(_y=$("clone-refresh-stt-btn"))==null||_y.addEventListener("click",async()=>{var _a2;$("clone-refresh-stt-btn").disabled=!0;try{await refreshSttBackends((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)}finally{$("clone-refresh-stt-btn").disabled=!1}}),$("transcribe-btn").addEventListener("click",async()=>{var _a2,_b2;const id=trimmedFileId||designedFileId||currentFileId;if(!id){toast("No audio to transcribe","error");return}const btn=$("transcribe-btn"),status2=$("transcribe-status"),area=$("transcript-area"),orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Transcribing\u2026',status2&&(status2.className="clone-tr-status working",status2.innerHTML=' Listening to your recording\u2026'),area&&(area.classList.add("transcribing"),area.placeholder="Transcribing your audio \u2014 please wait\u2026");try{const backend=((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)||"configured",r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,backend})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();area&&(area.value=d.text),status2&&(status2.className="clone-tr-status done",status2.innerHTML=' Transcribed'),toast("Transcription complete","success"),(_b2=window._cloneScheduleAutoSave)==null||_b2.call(window)}catch(e){status2&&(status2.className="clone-tr-status error",status2.innerHTML=` Failed: ${escHtml(e.message||String(e))}`),toast("Transcription failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,area&&(area.classList.remove("transcribing"),area.placeholder="Type or auto-transcribe the spoken text\u2026")}}),function(){var _a2,_b2,_c2;const g=id=>document.getElementById(id);let _idManual=!1,_prevName="Sam",_autoSaveTimer=null,_lastAutoSaved="";function buildVoiceId(){var _a3,_b3,_c3;if(_idManual){scheduleAutoSave();return}const lang=((_a3=g("lang-select"))==null?void 0:_a3.value)||"EN",gender=((_b3=g("gender-select"))==null?void 0:_b3.value)||"N",name=(((_c3=g("name-input"))==null?void 0:_c3.value)||"").trim().replace(/\s+/g,""),vid=g("voice-id-input");vid&&name&&(vid.value=`${lang}_${gender}_${name}`,vid.dispatchEvent(new Event("input"))),scheduleAutoSave()}const nameField=g("clone-your-name");nameField==null||nameField.addEventListener("input",()=>{const name=nameField.value.trim();if(!name)return;const sample=g("clone-sample-text");if(sample){const prev=sample.dataset.sampleName||_prevName,re=new RegExp("\\b"+prev.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b");re.test(sample.value)&&(sample.value=sample.value.replace(re,name)),sample.dataset.sampleName=name}_prevName=name;const ni=g("name-input");ni&&(ni.value=name.replace(/\s+/g,"")),buildVoiceId()}),["lang-select","gender-select"].forEach(id=>{var _a3;return(_a3=g(id))==null?void 0:_a3.addEventListener("change",buildVoiceId)}),(_a2=g("name-input"))==null||_a2.addEventListener("input",buildVoiceId),(_b2=g("voice-id-input"))==null||_b2.addEventListener("input",e=>{e.isTrusted&&(_idManual=!0),scheduleAutoSave()}),(_c2=g("transcript-area"))==null||_c2.addEventListener("input",scheduleAutoSave),window._cloneAutoTranscribe=function(){var _a3;const ta=g("transcript-area");if(ta&&ta.value.trim()){scheduleAutoSave();return}(_a3=g("transcribe-btn"))==null||_a3.click()};function canAutoSave(){var _a3,_b3;const vid=(((_a3=g("voice-id-input"))==null?void 0:_a3.value)||"").trim(),tr=(((_b3=g("transcript-area"))==null?void 0:_b3.value)||"").trim();return!!((typeof trimmedFileId!="undefined"&&trimmedFileId||typeof designedFileId!="undefined"&&designedFileId)&&vid&&tr&&(typeof validateVoiceId!="function"||validateVoiceId(vid)))}function scheduleAutoSave(){const toggle=g("clone-autosave-toggle");!toggle||!toggle.checked||(clearTimeout(_autoSaveTimer),_autoSaveTimer=setTimeout(()=>{var _a3;if(!canAutoSave())return;const sig=(g("voice-id-input").value+"|"+g("transcript-area").value).trim();sig!==_lastAutoSaved&&(_lastAutoSaved=sig,(_a3=g("save-btn"))==null||_a3.click())},1600))}window._cloneScheduleAutoSave=scheduleAutoSave}(),function(){const picker=document.getElementById("clone-src-picker");if(!picker)return;const cards=[...document.querySelectorAll(".clone-src-card")],tabs=[...picker.querySelectorAll(".clone-src-tab")],KEY="clone-src-choice";function show(src){cards.forEach(c=>{c.hidden=c.dataset.src!==src}),tabs.forEach(t=>t.classList.toggle("active",t.dataset.src===src));try{localStorage.setItem(KEY,src)}catch{}}tabs.forEach(t=>t.addEventListener("click",()=>show(t.dataset.src))),show(localStorage.getItem(KEY)||"mic")}(),$("save-btn").addEventListener("click",async()=>{const id=trimmedFileId||designedFileId||currentFileId;if(!id){toast("No audio ready","error");return}const voiceId=$("voice-id-input").value.trim();if(!voiceId){toast("Enter a Voice ID","error");return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("save-btn").disabled=!0;try{const payload={id,voice_id:voiceId,path:editingVoicePath,transcript:$("transcript-area").value},sendSave=endpoint=>fetch(endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});let fallbackSave=!1,r=await sendSave(editingVoiceId?"/api/voice-replace":"/api/save");if(editingVoiceId&&(r.status===404||r.status===405)&&(fallbackSave=!0,status("Update endpoint unavailable; saving as a regular voice\u2026"),r=await sendSave("/api/save")),!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();$("save-result").style.display="",toast((editingVoiceId?"Voice updated: ":"Voice saved: ")+d.voice_id,"success"),fallbackSave&&editingVoiceId&&voiceId!==editingVoiceId&&((await fetch("/api/voice/"+encodeURIComponent(editingVoiceId),{method:"DELETE"})).ok||status("Saved renamed voice; old library entry may need manual deletion.")),editingVoiceId=null,editingVoicePath=null}catch(e){toast("Save failed: "+e.message,"error")}finally{$("save-btn").disabled=!1}});let _voices=[],_pendingSelectId=null,_sortField="id",_sortDir=1,_libraryIssueFilter="",_activePlayButton=null,_activePlayVoiceId=null,_activePlayUrl=null,_libraryLoadPromise=null;const BENCHMARK_SAMPLE_STORAGE_KEY="vcf-benchmark-sample-text",_VL_CACHE_KEY="ttsvc_vc";function _vlCacheRead(){try{return JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY)||"null")}catch{return null}}function _vlCacheWrite(voices){try{sessionStorage.setItem(_VL_CACHE_KEY,JSON.stringify(voices))}catch{}}function _vlCacheClear(){try{sessionStorage.removeItem(_VL_CACHE_KEY)}catch{}}window._vlCacheClear=_vlCacheClear;const _libraryFilters={text:"",lang:"",sex:"",type:"",rating:""};let _libraryFilterOptionsSig="";const DEFAULT_BENCHMARK_SAMPLE_TEXT="Hello, how are you today? Please read this sample clearly for a fair voice benchmark.",BENCHMARK_PRESETS={de:"Die Welt ist voller Geschichten, die darauf warten, erz\xE4hlt zu werden \u2014 von mutigen Helden und stillen Tr\xE4umern.",en:"The old lighthouse stood firm against the crashing waves, its beam sweeping silently across the dark and restless sea.",de2:"Victor jagt zw\xF6lf Boxk\xE4mpfer quer \xFCber den gro\xDFen Sylter Deich. Im Winter ist es kalt und die Tage sind kurz.",en2:"She sells seashells by the seashore. Peter Piper picked a peck of pickled peppers on a perfectly pleasant afternoon.",reset:DEFAULT_BENCHMARK_SAMPLE_TEXT};function benchmarkSampleText(){const el=$("benchmark-sample-text");return el&&el.value.trim()||DEFAULT_BENCHMARK_SAMPLE_TEXT}function initBenchmarkSampleControls(){var _a2,_b2;const sample=$("benchmark-sample-text");if(!sample)return;sample.value=localStorage.getItem(BENCHMARK_SAMPLE_STORAGE_KEY)||DEFAULT_BENCHMARK_SAMPLE_TEXT,sample.addEventListener("input",debounce(()=>{localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value.trim()),status("Benchmark sample sentence saved")},500)),(_a2=$("benchmark-reset-sample-btn"))==null||_a2.addEventListener("click",()=>{sample.value=DEFAULT_BENCHMARK_SAMPLE_TEXT,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),status("Benchmark sample sentence reset")});const presetSel=$("benchmark-preset-select");presetSel&&presetSel.addEventListener("change",()=>{const key=presetSel.value;if(!key||!BENCHMARK_PRESETS[key]){presetSel.value="";return}sample.value=BENCHMARK_PRESETS[key],localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),presetSel.value="",status("Benchmark sample sentence loaded")}),(_b2=$("benchmark-use-preview-btn"))==null||_b2.addEventListener("click",()=>{var _a3;const text=(_a3=$("preview-text-area"))==null?void 0:_a3.value.trim();if(!text){toast("Preview text is empty","error");return}sample.value=text,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,text),status("Benchmark sample sentence copied from TTS preview")})}function _displaySource(v){if(v.origin)return v.origin;if(v.note){const m=v.note.match(/^Rehearser\s*·\s*(.+?)\s*·/);if(m)return m[1].trim()}if(v.tag){const tags=v.tag.split(",").map(t=>t.trim().toLowerCase());if(tags.includes("fish-audio")||tags.includes("fishaudio"))return"fish-audio"}return""}function getSortValue(v,field){var _a2,_b2,_c2,_d2,_e2,_f2;switch(field){case"has_picture":return v.has_picture?1:0;case"flag":return(v.flag||"").toLowerCase();case"gender":return(_a2={F:0,M:1,N:2}[v.gender])!=null?_a2:3;case"id":return v.id.toLowerCase();case"file_type":return voiceFileType(v);case"duration":return v.duration||0;case"dbfs":return(_b2=voiceDbfs(v))!=null?_b2:-999;case"benchmark":case"factor":return-((_c2=voiceFactor(v))!=null?_c2:-999);case"elapsed":return(_d2=voiceBenchmarkElapsed(v))!=null?_d2:999;case"bench_audio":return(_e2=voiceBenchmarkAudioSec(v))!=null?_e2:999;case"wpm":return(_f2=voiceWpm(v))!=null?_f2:-1;case"transcript":return(v.transcript||"").toLowerCase();case"note":return(v.note||"").toLowerCase();case"source":return(_displaySource(v)||"").toLowerCase();case"seed":return v.seed!=null?v.seed:9999999;case"tag":return(v.tag||"").toLowerCase();case"rating":return v.rating||0;case"enabled":return v.enabled===!1?0:1;default:return""}}function setSort(field){_sortDir=_sortField===field?_sortDir*-1:1,_sortField=field,syncSortHeaders(),renderVoiceList()}function toggleSortDir(){_sortDir*=-1,syncSortHeaders(),renderVoiceList()}function syncSortHeaders(){document.querySelectorAll(".vl-header [data-sort], .vl-table-header [data-sort]").forEach(el=>{el.classList.remove("sort-asc","sort-desc"),el.dataset.sort===_sortField&&el.classList.add(_sortDir===1?"sort-asc":"sort-desc")});const sel=document.getElementById("voice-sort-field");sel&&sel.value!==_sortField&&(sel.value=_sortField);const dirBtn=document.getElementById("voice-sort-dir");if(dirBtn){const icon=dirBtn.querySelector(".mdi");icon&&(icon.className=_sortDir===1?"mdi mdi-arrow-up":"mdi mdi-arrow-down"),dirBtn.title=_sortDir===1?"Ascending \u2014 click to reverse":"Descending \u2014 click to reverse"}}document.addEventListener("click",e=>{var _a2,_b2,_c2;if(e.target.closest("#voice-sort-dir")&&toggleSortDir(),e.target.closest("#voice-group-tag-btn")){window._voiceGroupByTag=!window._voiceGroupByTag;try{localStorage.setItem("vl-group-by-tag",window._voiceGroupByTag?"1":"0")}catch{}const btn=document.getElementById("voice-group-tag-btn");btn==null||btn.classList.toggle("active",window._voiceGroupByTag);const icon=btn==null?void 0:btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline"),renderVoiceList()}else if(e.target.closest("#voice-table-view-btn")){window._voiceTableView=!window._voiceTableView,window._voiceTableView||(window._voiceTableEditMode=!1);try{localStorage.setItem("vl-table-view",window._voiceTableView?"1":"0")}catch{}const btn=document.getElementById("voice-table-view-btn");btn==null||btn.classList.toggle("active",window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");if(editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode)),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",window._voiceTableView),(_b2=document.querySelector(".voices-workbench"))==null||_b2.classList.toggle("table-edit-mode",!!window._voiceTableEditMode),window._voiceTableView){document.querySelectorAll(".edit-open").forEach(r=>r.classList.remove("edit-open"));const inspector=document.getElementById("voices-inspector");inspector&&(inspector.innerHTML='

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

')}renderVoiceList()}else if(e.target.closest("#voice-table-edit-btn")){window._voiceTableEditMode=!window._voiceTableEditMode;const editBtn=document.getElementById("voice-table-edit-btn");editBtn==null||editBtn.classList.toggle("active",window._voiceTableEditMode),(_c2=document.querySelector(".voices-workbench"))==null||_c2.classList.toggle("table-edit-mode",window._voiceTableEditMode),renderVoiceList()}});try{window._voiceGroupByTag=localStorage.getItem("vl-group-by-tag")==="1"}catch{}try{window._voiceTableView=localStorage.getItem("vl-table-view")==="1"}catch{}document.addEventListener("click",e=>{const th=e.target.closest(".vl-th-sortable");if(th){const field=th.dataset.sort;if(field){const sel=document.getElementById("voice-sort-field");sel&&(sel.value=field),setSort(field)}}}),document.addEventListener("change",e=>{e.target.id==="voice-sort-field"&&setSort(e.target.value)}),document.addEventListener("change",async e=>{if(e.target.classList.contains("vl-inline-edit")){const row=e.target.closest(".vl-row");if(!row)return;const voiceId=row.dataset.id,field=e.target.dataset.field;let value=e.target.type==="checkbox"?e.target.checked:e.target.value;field==="rating"&&(value=parseInt(value)||0);const payload={};payload[field]=value;try{await saveMeta(voiceId,payload);const v=_voices.find(vv=>vv.id===voiceId);v&&(v[field]=value),typeof toast=="function"&&toast("Saved "+field,"success")}catch{typeof toast=="function"&&toast("Failed to save "+field,"error")}}});const FLAG_LANGUAGE_CANDIDATES={GB:["EN"],US:["EN"],AU:["EN"],NZ:["EN"],IE:["EN"],ZA:["EN"],NG:["EN"],KE:["EN"],GH:["EN"],JM:["EN"],TT:["EN"],CA:["EN","FR"],IN:["EN","HI"],SG:["EN","ZH"],PH:["EN","FIL"],MT:["EN","MT"],DE:["DE"],AT:["DE"],CH:["DE","FR","IT"],FR:["FR"],BE:["FR","NL"],LU:["FR","DE"],ES:["ES"],MX:["ES"],AR:["ES"],CO:["ES"],CL:["ES"],PE:["ES"],VE:["ES"],UY:["ES"],EC:["ES"],BO:["ES"],CR:["ES"],CU:["ES"],DO:["ES"],PT:["PT"],BR:["PT"],IT:["IT"],NL:["NL"],PL:["PL"],SE:["SV"],DK:["DA"],NO:["NO"],FI:["FI"],IS:["IS"],GR:["EL"],CY:["EL","TR"],CZ:["CS"],SK:["SK"],HU:["HU"],RO:["RO"],BG:["BG"],HR:["HR"],SI:["SL"],RS:["SR"],BA:["BS"],ME:["SR"],MK:["MK"],AL:["SQ"],EE:["ET"],LV:["LV"],LT:["LT"],UA:["UK"],RU:["RU"],BY:["RU"],MD:["RO"],TR:["TR"],CN:["ZH"],TW:["ZH"],HK:["ZH"],MO:["ZH"],JP:["JA"],KR:["KO"],VN:["VI"],TH:["TH"],ID:["ID"],MY:["MS"],PK:["UR"],BD:["BN"],LK:["SI"],NP:["NE"],SA:["AR"],EG:["AR"],AE:["AR"],MA:["AR"],QA:["AR"],KW:["AR"],OM:["AR"],JO:["AR"],LB:["AR"],IQ:["AR"],IR:["FA"],IL:["HE"]},FLAG_LANGUAGE=Object.fromEntries(Object.entries(FLAG_LANGUAGE_CANDIDATES).map(([cc,langs])=>[cc,langs[0]])),LANGUAGE_LABELS={EN:"English",DE:"German",FR:"French",ES:"Spanish",PT:"Portuguese",IT:"Italian",NL:"Dutch",PL:"Polish",SV:"Swedish",DA:"Danish",NO:"Norwegian",FI:"Finnish",IS:"Icelandic",EL:"Greek",MT:"Maltese",CS:"Czech",SK:"Slovak",HU:"Hungarian",RO:"Romanian",BG:"Bulgarian",HR:"Croatian",SL:"Slovenian",SR:"Serbian",BS:"Bosnian",MK:"Macedonian",SQ:"Albanian",ET:"Estonian",LV:"Latvian",LT:"Lithuanian",UK:"Ukrainian",RU:"Russian",ZH:"Chinese",JA:"Japanese",KO:"Korean",VI:"Vietnamese",TH:"Thai",ID:"Indonesian",MS:"Malay",FIL:"Filipino",HI:"Hindi",UR:"Urdu",BN:"Bengali",SI:"Sinhala",NE:"Nepali",AR:"Arabic",FA:"Persian",HE:"Hebrew",TR:"Turkish"},SEX_FILTER_LABELS={F:"\u2640 Female",M:"\u2642 Male",N:"\u26A5 Diverse / neutral"};function voiceLangFromName(v){return(v.lang||String(v.id||"").split("_")[0]||"").toUpperCase()}function libraryVoiceLang(v){const fromName=voiceLangFromName(v),candidates=FLAG_LANGUAGE_CANDIDATES[String(v.flag||"").toUpperCase()];return candidates!=null&&candidates.length?candidates.includes(fromName)?fromName:candidates[0]:fromName}function libraryLanguageLabel(code){return LANGUAGE_LABELS[code]||code}function populateLibraryFilters(){const langSel=$("library-filter-lang"),sexSel=$("library-filter-sex"),typeSel=$("library-filter-type"),tagSel=$("library-filter-tag"),groupSel=$("library-filter-group");if(!langSel||!sexSel||!typeSel)return;const langSet=new Set,sexSet=new Set,typeSet=new Set,tagSet=new Set,groupSet=new Set;(_voices||[]).forEach(v=>{const lang=libraryVoiceLang(v);lang&&langSet.add(lang),v.gender&&sexSet.add(v.gender);const type=voiceFileType(v);type&&typeSet.add(type),String(v.tag||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>tagSet.add(t));const g=(v.group||"").trim();g&&groupSet.add(g)});const langs=[...langSet].sort((a,b)=>libraryLanguageLabel(a).localeCompare(libraryLanguageLabel(b))),sexOrder=["F","M","N"],sexes=[...sexSet].sort((a,b)=>(sexOrder.indexOf(a)<0?99:sexOrder.indexOf(a))-(sexOrder.indexOf(b)<0?99:sexOrder.indexOf(b))),types=[...typeSet].sort(),tags=[...tagSet].sort((a,b)=>a.localeCompare(b)),groups=[...groupSet].sort((a,b)=>a.localeCompare(b)),sig=JSON.stringify([langs,sexes,types,tags,groups]);if(sig===_libraryFilterOptionsSig)return;_libraryFilterOptionsSig=sig;const keep={lang:langSel.value,sex:sexSel.value,type:typeSel.value,tag:tagSel==null?void 0:tagSel.value,group:groupSel==null?void 0:groupSel.value};langSel.innerHTML=''+langs.map(x=>``).join(""),sexSel.innerHTML=''+sexes.map(x=>``).join(""),typeSel.innerHTML=''+types.map(x=>``).join(""),tagSel&&(tagSel.innerHTML=''+tags.map(x=>``).join("")),groupSel&&(groupSel.innerHTML=''+groups.map(x=>``).join("")),langSel.value=langs.includes(keep.lang)?keep.lang:"",sexSel.value=sexes.includes(keep.sex)?keep.sex:"",typeSel.value=types.includes(keep.type)?keep.type:"",tagSel&&(tagSel.value=tags.includes(keep.tag)?keep.tag:""),groupSel&&(groupSel.value=groups.includes(keep.group)?keep.group:"")}function readLibraryFilters(){var _a2,_b2,_c2,_d2,_e2;_libraryFilters.text=(((_a2=$("library-filter-text"))==null?void 0:_a2.value)||"").trim().toLowerCase(),_libraryFilters.lang=((_b2=$("library-filter-lang"))==null?void 0:_b2.value)||"",_libraryFilters.sex=((_c2=$("library-filter-sex"))==null?void 0:_c2.value)||"",_libraryFilters.type=((_d2=$("library-filter-type"))==null?void 0:_d2.value)||"",_libraryFilters.rating=((_e2=$("library-filter-rating"))==null?void 0:_e2.value)||""}function libraryFilterMatch(v){const f=_libraryFilters;if(f.lang&&libraryVoiceLang(v)!==f.lang||f.sex&&(v.gender||"")!==f.sex||f.type&&voiceFileType(v)!==f.type)return!1;if(f.rating){const r=Number(v.rating||0),wanted=Number(f.rating);if(wanted===0&&r!==0||wanted===1&&r<1||wanted>1&&rString(x||"").toLowerCase()).join(" ").includes(f.text))}function clearLibraryFilters(){["library-filter-text","library-filter-lang","library-filter-sex","library-filter-type","library-filter-rating"].forEach(id=>{const el=$(id);el&&(el.value="")}),readLibraryFilters(),renderVoiceList()}function libraryTtsBackend(){var _a2;return((_a2=$("library-tts-backend-select"))==null?void 0:_a2.value)||"voice_clone"}function needsDuration(v){return v.duration==null||Number.isNaN(Number(v.duration))}function voiceFileType(v){if(v.file_type)return String(v.file_type).replace(/^\./,"").toLowerCase();const match=String(v.path||v.filename||"").match(/\.([A-Za-z0-9]+)(?:$|[?#])/);return match?match[1].toLowerCase():"wav"}function voiceDbfs(v){var _a2;const value=v.loudness&&((_a2=v.loudness.dbfs)!=null?_a2:v.loudness.after_dbfs);return value==null||Number.isNaN(Number(value))?null:Number(value)}function fmtDbfs(v){const db=voiceDbfs(v);return db==null?"-":db.toFixed(1)}function voiceBenchmark(v){return v.benchmark&&typeof v.benchmark=="object"&&Object.keys(v.benchmark).length>0?v.benchmark:null}function voiceBenchmarkElapsed(v){const b=voiceBenchmark(v),value=b&&b.elapsed_sec;return value==null||Number.isNaN(Number(value))?null:Number(value)}function voiceFactor(v){const b=voiceBenchmark(v);return b&&b.ok&&b.speed!=null?Number(b.speed):null}function fmtFactor(v){const f=voiceFactor(v);return f!=null?f.toFixed(2)+"x":"-"}function fmtElapsed(v){const e=voiceBenchmarkElapsed(v),b=voiceBenchmark(v);return!b||!b.ok?b&&!b.ok?"ERR":"-":e!=null?e.toFixed(1)+"s":"-"}function fmtBenchmark(v){const elapsed=fmtElapsed(v),factor=fmtFactor(v);return elapsed==="-"&&factor==="-"?"-":[elapsed,factor].filter(x=>x!=="-").join(" \xB7 ")}function benchmarkClass(v){const b=voiceBenchmark(v);if(!b)return"";if(!b.ok||b.clipped||b.realtime_ok===!1)return"bench-bad";const elapsed=voiceBenchmarkElapsed(v);return elapsed!=null&&elapsed<=4?"bench-ok":"bench-warn"}function voiceBenchmarkAudioSec(v){const b=voiceBenchmark(v);return b&&b.ok&&b.audio_sec!=null?Number(b.audio_sec):null}function fmtBenchmarkAudio(v){const sec=voiceBenchmarkAudioSec(v);return sec!=null?sec.toFixed(1)+"s":"-"}function voiceWpm(v){const b=voiceBenchmark(v);if(!b||!b.ok||!b.audio_sec||!b.text)return null;const words=b.text.trim().split(/\s+/).length;return Math.round(words/(b.audio_sec/60))}function fmtWpm(v){const wpm=voiceWpm(v);return wpm!=null?wpm+" wpm":"-"}function voiceFileUrl(v){const bust=v._audioVersion||v.updated_at||v.benchmarked_at||""||Date.now();return`/api/voice-file?path=${encodeURIComponent(v.path)}&v=${encodeURIComponent(bust)}`}function markVoiceAudioChanged(v){v._audioVersion=Date.now()}function benchmarkTitle(v){const b=voiceBenchmark(v);if(!b)return"Not benchmarked yet";const parts=[];return b.ok?(parts.push(`total ${Number(b.elapsed_sec||0).toFixed(2)}s`),b.ttfa_ms!=null&&parts.push(`TTFA ${Number(b.ttfa_ms).toFixed(0)}ms`),b.audio_sec!=null&&parts.push(`audio ${Number(b.audio_sec).toFixed(2)}s`),b.rtf!=null&&parts.push(`RTF ${Number(b.rtf).toFixed(2)}`),b.speed!=null&&parts.push(`speed ${Number(b.speed).toFixed(2)}x real-time`),b.clipped&&parts.push("output clipped")):(parts.push("benchmark failed"),b.error&&parts.push(b.error)),Array.isArray(b.advice)&&b.advice.length&&parts.push(b.advice.join(" | ")),b.benchmarked_at&&parts.push(`saved ${b.benchmarked_at}`),parts.join(" \xB7 ")}async function clientVoiceLoudness(v){if(!v.path)throw new Error("No audio path");const resp=await fetch(voiceFileUrl(v),{cache:"no-store"});if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const audioData=await resp.arrayBuffer(),buffer=await new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(audioData.slice(0));let sum=0,peak=0,count=0;for(let ch=0;ch0?20*Math.log10(rms):null,peakDbfs=peak>0?20*Math.log10(peak):null;return{dbfs:dbfs==null?null:Number(dbfs.toFixed(2)),peak_dbfs:peakDbfs==null?null:Number(peakDbfs.toFixed(2))}}async function clientCalculateVoiceDb(){const voices=_bulkSelected&&_bulkSelected.size>0?(_voices||[]).filter(v=>_bulkSelected.has(v.id)):visibleLibraryVoices(),errors=[];let calculated=0;const stats={startedAt:Date.now(),ok:0,slow:0,errors:0,middleLabel:"Skipped"};setBenchmarkProgress(0,voices.length,"Preparing dB scan...",stats);for(const v of voices){setBenchmarkProgress(calculated+errors.length,voices.length,`Calculating dB: ${v.id}`,stats);try{v.loudness=await clientVoiceLoudness(v),await saveMeta(v.id,{loudness:v.loudness}).catch(()=>{}),calculated++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs(v)} dBFS`,status(`Calculated dB: ${calculated} / ${voices.length}`)}catch(e){errors.push({voice_id:v.id,detail:e.message}),stats.errors++,stats.last=`${v.id}: ${e.message}`}setBenchmarkProgress(calculated+errors.length,voices.length,`Calculating dB: ${v.id}`,stats),await new Promise(resolve=>setTimeout(resolve,0))}return setBenchmarkProgress(voices.length,voices.length,"dB scan complete",stats),{calculated,errors,voices:voices.map(v=>({voice_id:v.id,loudness:v.loudness}))}}async function hydrateVoiceDuration(v,el){if(!(!v.path||!needsDuration(v)||v._durationLoading)){v._durationLoading=!0;try{const audio=new Audio;audio.preload="metadata",audio.src=voiceFileUrl(v),await new Promise((resolve,reject)=>{audio.onloadedmetadata=resolve,audio.onerror=()=>reject(new Error("Could not read duration"))}),Number.isFinite(audio.duration)&&audio.duration>0&&(v.duration=audio.duration,el&&document.body.contains(el)&&(el.textContent=fmtDuration(v.duration),el.title=String(v.duration.toFixed(2)))),audio.removeAttribute("src"),audio.load()}catch(e){el&&document.body.contains(el)&&(el.title=e.message)}finally{v._durationLoading=!1}}}document.querySelectorAll(".vl-header [data-sort]").forEach(el=>el.addEventListener("click",()=>setSort(el.dataset.sort)));function dominantLanguages(limit=3){const counts=new Map;return(_voices||[]).forEach(v=>{const lang=(v.lang||String(v.id||"").split("_")[0]||"?").toUpperCase();counts.set(lang,(counts.get(lang)||0)+1)}),[...counts.entries()].sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0])).slice(0,limit).map(([lang,count])=>`${lang} ${count}`).join(" \xB7 ")||"-"}function updateLibraryInsights(state="ready"){const el=$("library-insights");if(!el)return;if(state==="loading"){el.innerHTML=[["\u2026","Loading"],["\u2026","Active"],["\u2026","Languages"],["\u2026","Benchmarks"],["\u2026","Quality"],["\u2026","Actions"]].map(([value,label])=>`
${value}${label}
`).join("");return}if(state==="error"){el.innerHTML='
FailedLibrary load
';return}const total=_voices.length,active=_voices.filter(v=>v.enabled!==!1).length,hidden=total-active,bench=_voices.map(voiceBenchmark).filter(Boolean),slow=bench.filter(b=>b&&b.ok&&b.realtime_ok===!1).length,dbValues=_voices.map(voiceDbfs).filter(v=>v!=null),avgDb=dbValues.length?(dbValues.reduce((a,b)=>a+b,0)/dbValues.length).toFixed(1):"-",missingRef=_voices.filter(v=>!v.transcript).length,restart=_voices.filter(v=>v.needs_tts_restart).length,tiles=[{value:`${_voices.filter(v=>$("show-disabled-cb").checked||v.enabled!==!1).length}/${total}`,label:"Visible"},{value:`${active} on`,label:hidden?`${hidden} hidden`:"Active"},{value:dominantLanguages(),label:"Languages"},{value:bench.length?`${bench.length} done`:"-",label:slow?`${slow} slow`:"Benchmarks",filter:slow?"slow":"",title:slow?describeIssueVoices("slow"):"No slow voices"},{value:avgDb==="-"?"-":`${avgDb} dB`,label:missingRef?`${missingRef} no text`:"Avg loudness",filter:missingRef?"no_text":"",title:missingRef?describeIssueVoices("no_text"):"All visible voices have reference text"},{value:restart||"-",label:restart?"Need restart":"Restart flags",filter:restart?"restart":"",title:restart?describeIssueVoices("restart"):"No voices need restart"}];el.innerHTML=tiles.map(item=>{const filter=item.filter?` data-filter="${escHtml(item.filter)}" role="button" tabindex="0"`:"",activeCls=item.filter&&item.filter===_libraryIssueFilter?" active":"",title=item.title?` title="${escHtml(item.title)}"`:"";return`
${escHtml(item.value)}${escHtml(item.label)}
`}).join(""),el.querySelectorAll("[data-filter]").forEach(tile=>{const activate=()=>setLibraryIssueFilter(tile.dataset.filter||"");tile.addEventListener("click",activate),tile.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),activate())})})}function shouldRenderVoiceLibrary(){const section=$("s-voices");return!section||section.classList.contains("is-active")}async function loadVoiceLibrary(options={}){const forceRefresh=!!(options&&options.refresh);return _libraryLoadPromise?_libraryLoadPromise.then(()=>{shouldRenderVoiceLibrary()&&_voices.length&&renderVoiceList()}):(_libraryLoadPromise=(async()=>{setBusyButton("refresh-voices-btn",!0);const list=$("voice-list"),renderVisibleList=shouldRenderVoiceLibrary(),cached=_voices.length===0?_vlCacheRead():null;cached&&Array.isArray(cached)&&cached.length&&(_voices=cached,window._voices=_voices,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices),renderVisibleList&&renderVoiceList(),updatePreviewVoiceMatchPanel(),status(`Loaded ${_voices.length} voices`));const silent=_voices.length>0;list&&!silent&&renderVisibleList&&(list.innerHTML=loadingMarkup("Loading voice library","Scanning voices, reference text, metadata, ratings, and benchmark results.",8)),!silent&&renderVisibleList&&($("voice-count").textContent="Loading voices\u2026",updateLibraryInsights("loading"),status("Loading voice library\u2026"));try{const r=await fetch("/api/voices"+(forceRefresh?"?refresh=1":""));if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const fresh=await r.json();_vlCacheWrite(fresh),_voices=fresh,window._voices=_voices,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices),shouldRenderVoiceLibrary()&&renderVoiceList(),updatePreviewVoiceMatchPanel(),typeof renderPerfHistory=="function"&&renderPerfHistory(),status(`Loaded ${_voices.length} voices`)}catch(e){if(!silent&&renderVisibleList&&(list&&(list.innerHTML='
Failed to load voices: '+(e.message||String(e))+"
"),$("voice-count").textContent="Load failed",updateLibraryInsights("error")),status("Voice library load failed: "+e.message),!silent)throw e}finally{setBusyButton("refresh-voices-btn",!1),_libraryLoadPromise=null}})(),_libraryLoadPromise)}$("refresh-voices-btn").addEventListener("click",()=>{_vlCacheClear(),loadVoiceLibrary({refresh:!0})}),$("sync-voice-folders-btn").addEventListener("click",async()=>{$("sync-voice-folders-btn").disabled=!0,status("Syncing active_voices and hidden_voices\u2026");try{const r=await fetch("/api/voices/sync-folders",{method:"POST"});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();await loadVoiceLibrary();const conflicts=d.conflicts&&d.conflicts.length?`, ${d.conflicts.length} conflicts`:"";toast(`Synced: ${d.moved.active} active, ${d.moved.hidden} hidden${conflicts}`,d.conflicts&&d.conflicts.length?"error":"success"),status("Synced folders. Restart Qwen3-TTS after changing active voices.")}catch(e){toast("Sync failed: "+e.message,"error"),status("Folder sync failed")}finally{$("sync-voice-folders-btn").disabled=!1}});function visibleLibraryVoices(){const showDisabled=$("show-disabled-cb").checked;return _voices.filter(v=>showDisabled||v.enabled!==!1)}function libraryIssueMatch(v,filter=_libraryIssueFilter){const b=voiceBenchmark(v);return filter==="slow"?!!(b&&b.ok&&b.realtime_ok===!1):filter==="no_text"?!String(v.transcript||"").trim():filter==="restart"?!!v.needs_tts_restart:!0}function libraryIssueLabel(filter=_libraryIssueFilter){return{slow:"slow benchmark voices",no_text:"voices without reference text",restart:"voices needing TTS restart"}[filter]||"all voices"}function libraryIssueVoices(filter=_libraryIssueFilter){return visibleLibraryVoices().filter(v=>libraryIssueMatch(v,filter))}function describeIssueVoices(filter=_libraryIssueFilter,limit=12){const voices=libraryIssueVoices(filter).map(v=>v.id);if(!voices.length)return"No matching voices";const extra=voices.length>limit?`, +${voices.length-limit} more`:"";return voices.slice(0,limit).join(", ")+extra}function setLibraryIssueFilter(filter=""){_libraryIssueFilter=_libraryIssueFilter===filter?"":filter,renderVoiceList(),status(_libraryIssueFilter?`${libraryIssueLabel()}: ${describeIssueVoices()}`:"Showing all visible voices")}function libraryTargetDb(){var _a2;const input=$("library-target-db"),raw=Number((_a2=input==null?void 0:input.value)!=null?_a2:-20),value=Number.isFinite(raw)?Math.min(-1,Math.max(-60,raw)):-20;return input&&(input.value=String(value)),value}$("calculate-db-btn").addEventListener("click",async()=>{$("calculate-db-btn").disabled=!0;const _calcTarget=_bulkSelected&&_bulkSelected.size>0?`${_bulkSelected.size} selected`:"visible";status(`Calculating voice loudness (${_calcTarget})\u2026`);try{const d=await clientCalculateVoiceDb();renderVoiceList();const extra=d.errors&&d.errors.length?`, ${d.errors.length} errors`:"";toast(`Calculated dB for ${d.calculated} voices${extra}`,d.errors&&d.errors.length?"error":"success"),status("Calculated voice loudness. Use Normalize volume for visible WAV voices.")}catch(e){toast("Calculate dB failed: "+e.message,"error"),status("dB calculation failed")}finally{$("calculate-db-btn").disabled=!1}}),$("normalize-volume-btn").addEventListener("click",async()=>{var _a2;const target=libraryTargetDb(),visible=visibleLibraryVoices(),voices=visible.filter(v=>voiceFileType(v)==="wav"),skipped=visible.length-voices.length;if(!voices.length){toast("No visible WAV voices to normalize","error");return}if(!confirm(`Normalize ${voices.length} visible WAV voices to ${target} dBFS?${skipped?` ${skipped} non-WAV voices will be skipped.`:""}`))return;$("normalize-volume-btn").disabled=!0,$("calculate-db-btn").disabled=!0;const stats={startedAt:Date.now(),ok:0,slow:skipped,errors:0,middleLabel:"Skipped"},errors=[];let normalized=0;setBenchmarkProgress(0,voices.length,`Normalizing to ${target} dBFS...`,stats),status(`Normalizing ${voices.length} voices to ${target} dBFS...`);try{for(const v of voices){setBenchmarkProgress(normalized+errors.length,voices.length,`Normalizing: ${v.id}`,stats);try{const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:target})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a2=d.duration)!=null?_a2:v.duration,v.file_type=d.file_type||v.file_type,v.path=d.path||v.path,v.needs_tts_restart=!0,markVoiceAudioChanged(v),normalized++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs(v)} dBFS`}catch(e){errors.push({voice_id:v.id,detail:e.message}),stats.errors++,stats.last=`${v.id}: ${e.message}`}setBenchmarkProgress(normalized+errors.length,voices.length,`Normalizing: ${v.id}`,stats),status(`Normalized ${normalized} / ${voices.length}`),await new Promise(resolve=>setTimeout(resolve,0))}setBenchmarkProgress(voices.length,voices.length,"Volume normalization complete",stats),renderVoiceList(),updateLibraryInsights();const extra=`${skipped?`, ${skipped} skipped`:""}${errors.length?`, ${errors.length} errors`:""}`;toast(`Normalized ${normalized} voices${extra}`,errors.length?"error":"success"),status("Volume normalized. Restart TTS before rebenchmarking these voices.")}catch(e){toast("Normalize volume failed: "+e.message,"error"),status("Normalize volume failed")}finally{$("normalize-volume-btn").disabled=!1,$("calculate-db-btn").disabled=!1}});function fmtClock(ms){if(!Number.isFinite(ms)||ms<0)return"-";const total=Math.round(ms/1e3),m=Math.floor(total/60),s=total%60;return`${m}:${String(s).padStart(2,"0")}`}function setBenchmarkProgress(done,total,label="",stats={}){const panel=$("benchmark-progress"),track=panel.querySelector(".benchmark-progress-track"),pct=total?Math.round(done/total*100):0;panel.hidden=!1,$("benchmark-progress-label").textContent=label||(done>=total?"Benchmark complete":"Benchmarking voices..."),$("benchmark-progress-count").textContent=`${done} / ${total}`,$("benchmark-progress-bar").style.width=pct+"%",track.setAttribute("aria-valuenow",String(pct));const live=$("benchmark-live-stats");if(live){const elapsed=stats.startedAt?Date.now()-stats.startedAt:0,avg=done>0?elapsed/done:0,eta=done>0&&total>done?avg*(total-done):0;live.innerHTML=[`Elapsed ${fmtClock(elapsed)}`,`Avg ${done?(avg/1e3).toFixed(1)+"s":"-"}`,`ETA ${done&&total>done?fmtClock(eta):"-"}`,`OK ${stats.ok||0}`,`${stats.middleLabel||"Slow"} ${stats.slow||0}`,`${stats.errorLabel||"Errors"} ${stats.errors||0}`].map(x=>`${escHtml(x)}`).join("")}const last=$("benchmark-live-last");last&&stats.last&&(last.textContent=stats.last)}function hideBenchmarkProgress(){$("benchmark-progress").hidden=!0,$("benchmark-progress-bar").style.width="0%",$("benchmark-live-last")&&($("benchmark-live-last").textContent="")}async function clearTtsRestartFlags(){const r=await fetch("/api/tts/restart-flags/clear",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return _voices.forEach(voice=>{voice.needs_tts_restart=!1}),document.querySelectorAll(".vl-row.edit-open").forEach(row=>row.classList.remove("opt-restart-needed")),updateLibraryInsights(),d}async function runVoiceBenchmark(voiceId="",opts={}){var _a2;const text=(_a2=opts.text)!=null?_a2:benchmarkSampleText();if(!text)return toast("Enter a benchmark sample sentence","error"),null;const payload={active_only:!0,text};voiceId&&(payload.voice_id=voiceId);const r=await fetch("/api/voices/benchmark",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return r.json()}async function runVoiceBenchmarkBatch(){const voices=benchmarkTargetVoices(),text=benchmarkSampleText();if(!text)return toast("Enter a benchmark sample sentence","error"),null;if(!voices.length)return toast("No voices to benchmark","error"),null;const total=voices.length,aggregate={benchmarked:0,errors:[],voices:[],text,active_only:!0},stats={startedAt:Date.now(),ok:0,slow:0,errors:0,last:""};voices.forEach(v=>{const row=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);row&&(row.classList.remove("benchmarking-active","benchmarking-done"),row.classList.add("benchmarking-pending"))}),setBenchmarkProgress(0,total,"Starting benchmark...",stats);for(let i=0;ix.voice_id===voice.id),b=hit&&hit.benchmark;if(b&&b.ok){if(stats.ok++,b.realtime_ok===!1&&stats.slow++,stats.last=`${voice.id}: ${Number(b.elapsed_sec||0).toFixed(1)}s${b.speed!=null?` \xB7 ${Number(b.speed).toFixed(2)}x`:""}${b.realtime_ok===!1?" \xB7 slow":""}`,row){const bc=benchmarkClass(voice),btitle=benchmarkTitle(voice),durCell=row.querySelector(".vl-tbl-dur"),factorCell=row.querySelector(".vl-tbl-factor"),timeCell=row.querySelector(".vl-tbl-time"),wpmCell=row.querySelector(".vl-tbl-wpm");if(durCell&&(durCell.textContent=fmtBenchmarkAudio(voice),durCell.title=`${fmtBenchmarkAudio(voice)} \u2014 length of synthesised benchmark audio`),factorCell&&(factorCell.textContent=fmtFactor(voice),factorCell.className=`vl-tbl-factor ${bc}`,factorCell.title=btitle),timeCell&&(timeCell.textContent=fmtElapsed(voice),timeCell.className=`vl-tbl-time ${bc}`,timeCell.title=btitle),wpmCell){const wpm=voiceWpm(voice);wpmCell.textContent=fmtWpm(voice),wpmCell.title=wpm!=null?`${wpm} wpm \u2014 130\u2013180 wpm is natural for long listening`:""}}}else stats.errors++,stats.last=`${voice.id}: failed${b&&b.error?" \xB7 "+b.error:""}`}}catch(e){aggregate.errors.push({voice_id:voice.id,detail:e.message}),stats.errors++,stats.last=`${voice.id}: failed \xB7 ${e.message}`}row&&(row.classList.remove("benchmarking-active"),row.classList.add("benchmarking-done")),setBenchmarkProgress(i+1,total,`Finished ${voice.id}`,stats)}return voices.forEach(v=>{const row=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);row&&row.classList.remove("benchmarking-pending")}),setBenchmarkProgress(total,total,"Benchmark complete",stats),aggregate}function mergeBenchmarkResults(d){const byId=new Map((d.voices||[]).map(x=>[x.voice_id,x]));_voices.forEach(v=>{const hit=byId.get(v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark)})}window.loadVoiceLibrary=loadVoiceLibrary,window.mergeBenchmarkResults=mergeBenchmarkResults;function activeBenchmarkVoices(){return(_voices||[]).filter(v=>v.enabled!==!1)}function benchmarkTargetVoices(){return typeof _bulkSelected!="undefined"&&_bulkSelected.size>0?(_voices||[]).filter(v=>_bulkSelected.has(v.id)):activeBenchmarkVoices()}function showBenchmarkConfirm(){const voices=benchmarkTargetVoices(),onlySelected=typeof _bulkSelected!="undefined"&&_bulkSelected.size>0;if(!benchmarkSampleText()){toast("Enter a benchmark sample sentence","error");return}if(!voices.length){toast("No voices to benchmark","error");return}const staleCount=voices.filter(v=>v.needs_tts_restart).length;$("benchmark-confirm-title").textContent=`Benchmark ${voices.length} ${onlySelected?"selected":"active"} voice${voices.length===1?"":"s"}?`,$("benchmark-confirm-text").textContent="This sends the sample sentence to each active voice and can keep the GPU busy for a while. Progress updates after every voice."+(staleCount?` ${staleCount} edited voice${staleCount===1?"":"s"} should be restarted first, otherwise cached old voices may be benchmarked.`:""),$("benchmark-confirm").hidden=!1,$("benchmark-confirm-start").focus()}function hideBenchmarkConfirm(){const panel=$("benchmark-confirm");panel&&(panel.hidden=!0)}$("benchmark-voices-btn").addEventListener("click",showBenchmarkConfirm),(_z=$("benchmark-confirm-cancel"))==null||_z.addEventListener("click",hideBenchmarkConfirm),(_A=$("benchmark-confirm-start"))==null||_A.addEventListener("click",async()=>{hideBenchmarkConfirm(),$("benchmark-voices-btn").disabled=!0,$("benchmark-confirm-start").disabled=!0,status("Benchmarking active voices...");try{const d=await runVoiceBenchmarkBatch();if(!d)return;mergeBenchmarkResults(d),await loadVoiceLibrary();const slow=(d.voices||[]).filter(x=>x.benchmark&&x.benchmark.realtime_ok===!1).length,extra=d.errors&&d.errors.length?`, ${d.errors.length} errors`:"";toast(`Benchmarked ${d.benchmarked} voices${slow?`, ${slow} slow`:""}${extra}`,d.errors&&d.errors.length?"error":"success"),status("Benchmark saved with TTFA, total time, RTF, and speed.")}catch(e){toast("Benchmark failed: "+e.message,"error"),status("Benchmark failed")}finally{$("benchmark-voices-btn").disabled=!1,$("benchmark-confirm-start").disabled=!1}}),$("copy-active-voices-btn").addEventListener("click",async()=>{const useSelected=_bulkSelected&&_bulkSelected.size>0,ids=useSelected?[..._bulkSelected]:activeVoiceIds(),label=useSelected?`${ids.length} selected`:`${ids.length} active`;if(!ids.length){toast("No voices to copy","error");return}await copyText(ids.join(", ")),toast("Copied "+label+" voices","success"),status("Copied "+label+" voices to clipboard")}),(_B=$("precompute-embeddings-btn"))==null||_B.addEventListener("click",async()=>{const backend=libraryTtsBackend(),_useSelected=_bulkSelected&&_bulkSelected.size>0,ids=_useSelected?[..._bulkSelected]:activeVoiceIds(),_scopeLabel=_useSelected?`${ids.length} selected`:`${ids.length} active`;if(!ids.length){toast("No voices to precompute","error");return}if(!confirm(`Precompute speaker embeddings for ${_scopeLabel} voice(s) via \u201C${backend}\u201D? + `,list.appendChild(card)}))}function applyQwenSample(sample){$("design-instruct").value=sample.description,$("design-sample-text").value=sample.text,$("design-language").value=sample.language,$("design-gender").value=sample.gender,currentDesignSource=sample,$("design-result").style.display="none",$("design-save-result").style.display="none",$("design-instruct").scrollIntoView({behavior:"smooth",block:"nearest"})}function isDialogueDesign(instruct,text,source=null){if(source&&source.dialogue)return!0;const speakers=new Set;if(String(instruct||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^"?([^":]+)"?\s*:\s*"?(.+?)"?$/);match&&speakers.add(match[1].trim())}),speakers.size<2)return!1;const turnSpeakers=new Set;return String(text||"").split(/\n+/).forEach(line=>{const match=line.trim().match(/^([^:]{1,40}):\s*(.+)$/);match&&speakers.has(match[1].trim())&&turnSpeakers.add(match[1].trim())}),turnSpeakers.size>=2}function voiceDesignPayload(instruct,sampleText,language,source=null,gender=null){var _a2;return{instruct,sample_text:sampleText,language,gender:gender||(source==null?void 0:source.gender)||((_a2=$("design-gender"))==null?void 0:_a2.value)||"",dialogue:isDialogueDesign(instruct,sampleText,source)}}let _dVoiceIdManual=!1;function designSafeName(name){const base=name||"VoiceDesign";return(typeof _umlautSafe=="function"?_umlautSafe(base):String(base)).replace(/^[A-Z]{2}_[FMN]_/,"").replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,42)||"VoiceDesign"}function voiceIdSafePart(value,fallback="style"){return(typeof _umlautSafe=="function"?_umlautSafe(value||fallback):String(value||fallback)).replace(/[^A-Za-z0-9]+/g,"_").replace(/^_+|_+$/g,"").slice(0,32)||fallback}function suggestedStyleVoiceId(baseId,style){const suffix=voiceIdSafePart(style||"style");return`${baseId}_${suffix}`.slice(0,96)}function _updateDVoiceId(){if(_dVoiceIdManual)return;const lang=$("d-lang").value,gender=$("d-gender").value,name=$("d-name").value.trim();$("d-voice-id").value=name?`${lang}_${gender}_${name}`:""}["d-lang","d-gender"].forEach(id=>$(id).addEventListener("change",_updateDVoiceId)),$("d-name").addEventListener("input",()=>{_dVoiceIdManual=!1,_updateDVoiceId()}),$("d-voice-id").addEventListener("input",()=>{_dVoiceIdManual=!0}),seedDesignPresets(),refreshDesignPresetSelect(),renderQwenSampleCards(),syncDesignPresetsToServer(),$("design-preset-select").addEventListener("change",()=>{$("design-preset-select").value&&applyDesignPreset($("design-preset-select").value)}),$("design-preset-load").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}applyDesignPreset(name)}),$("design-preset-save").addEventListener("click",()=>{const name=$("design-preset-name").value.trim()||$("design-preset-select").value;if(!name){toast("Enter a preset name","error"),$("design-preset-name").focus();return}const presets=loadDesignPresets();presets[name]={description:$("design-instruct").value,sample_text:$("design-sample-text").value,language:$("design-language").value,gender:$("design-gender").value,dialogue:isDialogueDesign($("design-instruct").value,$("design-sample-text").value,currentDesignSource)},saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-select").value=name,toast("Preset saved: "+name,"success")}),$("design-preset-delete").addEventListener("click",()=>{const name=$("design-preset-select").value||$("design-preset-name").value.trim();if(!name){toast("Select a preset first","error");return}const presets=loadDesignPresets();if(!presets[name]){toast("Preset not found","error");return}delete presets[name],saveDesignPresets(presets),syncDesignPresetsToServer(),refreshDesignPresetSelect(),$("design-preset-name").value="",toast("Preset deleted: "+name,"success")}),["design-instruct","design-sample-text"].forEach(id=>$(id).addEventListener("input",()=>{currentDesignSource=null,id==="design-sample-text"&&($("d-transcript").value=$("design-sample-text").value)})),document.querySelectorAll(".qwen-sample").forEach(card=>{const sample=QWEN_DESIGN_SAMPLES[card.dataset.qwenSample],state=card.querySelector(".qwen-state"),audio=card.querySelector("audio");card.querySelector(".qwen-use").addEventListener("click",()=>{applyQwenSample(sample),toast("Voice Design sample loaded","success")}),card.querySelector(".qwen-preview").addEventListener("click",async e=>{const btn=e.currentTarget;btn.disabled=!0,state.textContent="Generating preview\u2026";try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(sample.description,sample.text,sample.language,sample))});if(!r.ok){const err=await r.json().catch(()=>({}));throw new Error(err.detail||r.statusText)}const d=await r.json();audio.src="/api/audio/"+d.id,audio.style.display="",audio.play().catch(()=>{}),state.textContent="Preview ready"}catch(err){state.textContent="Preview failed",toast("Sample preview failed: "+err.message,"error")}finally{btn.disabled=!1}})});async function runVoiceDesign(){const baseInstruct=$("design-instruct").value.trim(),sample=$("design-sample-text").value.trim(),dialogue=isDialogueDesign(baseInstruct,sample,currentDesignSource),instruct=baseInstruct;if(!instruct){toast("Enter a voice description first","error");return}$("design-generate-btn").disabled=!0,$("design-status").textContent="Generating\u2026",$("design-result").style.display="none",$("design-save-result").style.display="none",status("Generating voice design\u2026");try{const r=await fetch("/api/voice-design",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(voiceDesignPayload(instruct,sample,$("design-language").value,currentDesignSource,$("design-gender").value))});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();designedFileId=d.id,trimmedFileId=null,editingVoiceId=null,$("design-audio").src="/api/audio/"+d.id,$("design-result").style.display="flex",$("design-status").textContent="Done ("+d.duration.toFixed(1)+" s)";const langCode=DESIGN_LANG_CODE[$("design-language").value]||"EN";$("d-lang").value=langCode,$("d-gender").value=$("design-gender").value,$("d-name").value=designSafeName((currentDesignSource==null?void 0:currentDesignSource.title)||(currentDesignSource==null?void 0:currentDesignSource.name)||$("design-preset-name").value||"VoiceDesign"),_dVoiceIdManual=!1,_updateDVoiceId(),$("d-transcript").value=sample,$("trim-audio").src="/api/audio/"+d.id,$("trim-audio").style.display="",$("no-audio-hint").style.display="none",$("transcript-area").value||($("transcript-area").value=sample),$("design-audio").play().catch(()=>{}),$("design-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Voice generated and export fields filled.","success"),status("Voice design ready")}catch(e){$("design-status").textContent="Failed: "+e.message,toast("Voice design failed: "+e.message,"error"),status("Voice design failed")}finally{$("design-generate-btn").disabled=!1}}$("design-generate-btn").addEventListener("click",runVoiceDesign),$("design-retry-btn").addEventListener("click",runVoiceDesign),$("design-save-btn").addEventListener("click",async()=>{if(!designedFileId){toast("No voice generated yet","error");return}const voiceId=$("d-voice-id").value.trim();if(!voiceId){toast("Enter a Voice ID first","error"),$("d-name").focus();return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("design-save-btn").disabled=!0;try{const r=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:designedFileId,voice_id:voiceId,transcript:$("d-transcript").value})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const saved=await r.json();await saveMeta(saved.voice_id,{gender:$("d-gender").value,flag:LANG_FLAG_DEFAULT[$("d-lang").value]||void 0,transcript:$("d-transcript").value,note:"Voice Design: "+$("design-instruct").value.slice(0,240)}).catch(()=>{}),await loadVoiceLibrary().catch(()=>{}),$("design-save-result").style.display="flex",$("design-save-result").scrollIntoView({behavior:"smooth",block:"nearest"}),toast("Exported to Voice Clone Library: "+saved.voice_id,"success"),status("Exported to Voice Clone Library: "+saved.voice_id)}catch(e){toast("Save failed: "+e.message,"error")}finally{$("design-save-btn").disabled=!1}}),$("design-download-btn").addEventListener("click",()=>{if(!designedFileId)return;const a=document.createElement("a");a.href="/api/audio/"+designedFileId,a.download=($("d-voice-id").value.trim()||"voice_design")+".wav",a.click()}),(_y=$("clone-refresh-stt-btn"))==null||_y.addEventListener("click",async()=>{var _a2;$("clone-refresh-stt-btn").disabled=!0;try{await refreshSttBackends((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)}finally{$("clone-refresh-stt-btn").disabled=!1}}),$("transcribe-btn").addEventListener("click",async()=>{var _a2,_b2;const id=trimmedFileId||designedFileId||currentFileId;if(!id){toast("No audio to transcribe","error");return}const btn=$("transcribe-btn"),status2=$("transcribe-status"),area=$("transcript-area"),orig=btn.innerHTML;btn.disabled=!0,btn.innerHTML=' Transcribing\u2026',status2&&(status2.className="clone-tr-status working",status2.innerHTML=' Listening to your recording\u2026'),area&&(area.classList.add("transcribing"),area.placeholder="Transcribing your audio \u2014 please wait\u2026");try{const backend=((_a2=$("clone-stt-backend"))==null?void 0:_a2.value)||"configured",r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id,backend})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();area&&(area.value=d.text),status2&&(status2.className="clone-tr-status done",status2.innerHTML=' Transcribed'),toast("Transcription complete","success"),(_b2=window._cloneScheduleAutoSave)==null||_b2.call(window)}catch(e){status2&&(status2.className="clone-tr-status error",status2.innerHTML=` Failed: ${escHtml(e.message||String(e))}`),toast("Transcription failed: "+e.message,"error")}finally{btn.disabled=!1,btn.innerHTML=orig,area&&(area.classList.remove("transcribing"),area.placeholder="Type or auto-transcribe the spoken text\u2026")}}),function(){var _a2,_b2,_c2;const g=id=>document.getElementById(id);let _idManual=!1,_prevName="Sam",_autoSaveTimer=null,_lastAutoSaved="";function buildVoiceId(){var _a3,_b3,_c3;if(_idManual){scheduleAutoSave();return}const lang=((_a3=g("lang-select"))==null?void 0:_a3.value)||"EN",gender=((_b3=g("gender-select"))==null?void 0:_b3.value)||"N",name=(((_c3=g("name-input"))==null?void 0:_c3.value)||"").trim().replace(/\s+/g,""),vid=g("voice-id-input");vid&&name&&(vid.value=`${lang}_${gender}_${name}`,vid.dispatchEvent(new Event("input"))),scheduleAutoSave()}const nameField=g("clone-your-name");nameField==null||nameField.addEventListener("input",()=>{const name=nameField.value.trim();if(!name)return;const sample=g("clone-sample-text");if(sample){const prev=sample.dataset.sampleName||_prevName,re=new RegExp("\\b"+prev.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"\\b");re.test(sample.value)&&(sample.value=sample.value.replace(re,name)),sample.dataset.sampleName=name}_prevName=name;const ni=g("name-input");ni&&(ni.value=name.replace(/\s+/g,"")),buildVoiceId()}),["lang-select","gender-select"].forEach(id=>{var _a3;return(_a3=g(id))==null?void 0:_a3.addEventListener("change",buildVoiceId)}),(_a2=g("name-input"))==null||_a2.addEventListener("input",buildVoiceId),(_b2=g("voice-id-input"))==null||_b2.addEventListener("input",e=>{e.isTrusted&&(_idManual=!0),scheduleAutoSave()}),(_c2=g("transcript-area"))==null||_c2.addEventListener("input",scheduleAutoSave),window._cloneAutoTranscribe=function(){var _a3;const ta=g("transcript-area");if(ta&&ta.value.trim()){scheduleAutoSave();return}(_a3=g("transcribe-btn"))==null||_a3.click()};function canAutoSave(){var _a3,_b3;const vid=(((_a3=g("voice-id-input"))==null?void 0:_a3.value)||"").trim(),tr=(((_b3=g("transcript-area"))==null?void 0:_b3.value)||"").trim();return!!((typeof trimmedFileId!="undefined"&&trimmedFileId||typeof designedFileId!="undefined"&&designedFileId)&&vid&&tr&&(typeof validateVoiceId!="function"||validateVoiceId(vid)))}function scheduleAutoSave(){const toggle=g("clone-autosave-toggle");!toggle||!toggle.checked||(clearTimeout(_autoSaveTimer),_autoSaveTimer=setTimeout(()=>{var _a3;if(!canAutoSave())return;const sig=(g("voice-id-input").value+"|"+g("transcript-area").value).trim();sig!==_lastAutoSaved&&(_lastAutoSaved=sig,(_a3=g("save-btn"))==null||_a3.click())},1600))}window._cloneScheduleAutoSave=scheduleAutoSave}(),function(){const picker=document.getElementById("clone-src-picker");if(!picker)return;const cards=[...document.querySelectorAll(".clone-src-card")],tabs=[...picker.querySelectorAll(".clone-src-tab")],KEY="clone-src-choice";function show(src){cards.forEach(c=>{c.hidden=c.dataset.src!==src}),tabs.forEach(t=>t.classList.toggle("active",t.dataset.src===src));try{localStorage.setItem(KEY,src)}catch{}}tabs.forEach(t=>t.addEventListener("click",()=>show(t.dataset.src))),show(localStorage.getItem(KEY)||"mic")}(),$("save-btn").addEventListener("click",async()=>{const id=trimmedFileId||designedFileId||currentFileId;if(!id){toast("No audio ready","error");return}const voiceId=$("voice-id-input").value.trim();if(!voiceId){toast("Enter a Voice ID","error");return}if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}$("save-btn").disabled=!0;try{const payload={id,voice_id:voiceId,path:editingVoicePath,transcript:$("transcript-area").value},sendSave=endpoint=>fetch(endpoint,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});let fallbackSave=!1,r=await sendSave(editingVoiceId?"/api/voice-replace":"/api/save");if(editingVoiceId&&(r.status===404||r.status===405)&&(fallbackSave=!0,status("Update endpoint unavailable; saving as a regular voice\u2026"),r=await sendSave("/api/save")),!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();$("save-result").style.display="",toast((editingVoiceId?"Voice updated: ":"Voice saved: ")+d.voice_id,"success"),fallbackSave&&editingVoiceId&&voiceId!==editingVoiceId&&((await fetch("/api/voice/"+encodeURIComponent(editingVoiceId),{method:"DELETE"})).ok||status("Saved renamed voice; old library entry may need manual deletion.")),editingVoiceId=null,editingVoicePath=null}catch(e){toast("Save failed: "+e.message,"error")}finally{$("save-btn").disabled=!1}});let _voices=[],_pendingSelectId=null,_sortField="id",_sortDir=1,_libraryIssueFilter="",_activePlayButton=null,_activePlayVoiceId=null,_activePlayUrl=null,_libraryLoadPromise=null;const BENCHMARK_SAMPLE_STORAGE_KEY="vcf-benchmark-sample-text",_VL_CACHE_KEY="ttsvc_vc";function _vlCacheRead(){try{return JSON.parse(sessionStorage.getItem(_VL_CACHE_KEY)||"null")}catch{return null}}function _vlCacheWrite(voices){try{sessionStorage.setItem(_VL_CACHE_KEY,JSON.stringify(voices))}catch{}}function _vlCacheClear(){try{sessionStorage.removeItem(_VL_CACHE_KEY)}catch{}}window._vlCacheClear=_vlCacheClear;const _libraryFilters={text:"",lang:"",sex:"",type:"",rating:""};let _libraryFilterOptionsSig="";const DEFAULT_BENCHMARK_SAMPLE_TEXT="Hello, how are you today? Please read this sample clearly for a fair voice benchmark.",BENCHMARK_PRESETS={de:"Die Welt ist voller Geschichten, die darauf warten, erz\xE4hlt zu werden \u2014 von mutigen Helden und stillen Tr\xE4umern.",en:"The old lighthouse stood firm against the crashing waves, its beam sweeping silently across the dark and restless sea.",de2:"Victor jagt zw\xF6lf Boxk\xE4mpfer quer \xFCber den gro\xDFen Sylter Deich. Im Winter ist es kalt und die Tage sind kurz.",en2:"She sells seashells by the seashore. Peter Piper picked a peck of pickled peppers on a perfectly pleasant afternoon.",reset:DEFAULT_BENCHMARK_SAMPLE_TEXT};function benchmarkSampleText(){const el=$("benchmark-sample-text");return el&&el.value.trim()||DEFAULT_BENCHMARK_SAMPLE_TEXT}function initBenchmarkSampleControls(){var _a2,_b2;const sample=$("benchmark-sample-text");if(!sample)return;sample.value=localStorage.getItem(BENCHMARK_SAMPLE_STORAGE_KEY)||DEFAULT_BENCHMARK_SAMPLE_TEXT,sample.addEventListener("input",debounce(()=>{localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value.trim()),status("Benchmark sample sentence saved")},500)),(_a2=$("benchmark-reset-sample-btn"))==null||_a2.addEventListener("click",()=>{sample.value=DEFAULT_BENCHMARK_SAMPLE_TEXT,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),status("Benchmark sample sentence reset")});const presetSel=$("benchmark-preset-select");presetSel&&presetSel.addEventListener("change",()=>{const key=presetSel.value;if(!key||!BENCHMARK_PRESETS[key]){presetSel.value="";return}sample.value=BENCHMARK_PRESETS[key],localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,sample.value),presetSel.value="",status("Benchmark sample sentence loaded")}),(_b2=$("benchmark-use-preview-btn"))==null||_b2.addEventListener("click",()=>{var _a3;const text=(_a3=$("preview-text-area"))==null?void 0:_a3.value.trim();if(!text){toast("Preview text is empty","error");return}sample.value=text,localStorage.setItem(BENCHMARK_SAMPLE_STORAGE_KEY,text),status("Benchmark sample sentence copied from TTS preview")})}function _displaySource(v){if(v.origin)return v.origin;if(v.note){const m=v.note.match(/^Rehearser\s*·\s*(.+?)\s*·/);if(m)return m[1].trim()}if(v.tag){const tags=v.tag.split(",").map(t=>t.trim().toLowerCase());if(tags.includes("fish-audio")||tags.includes("fishaudio"))return"fish-audio"}return""}function getSortValue(v,field){var _a2,_b2,_c2,_d2,_e2,_f2;switch(field){case"has_picture":return v.has_picture?1:0;case"flag":return(v.flag||"").toLowerCase();case"gender":return(_a2={F:0,M:1,N:2}[v.gender])!=null?_a2:3;case"id":return v.id.toLowerCase();case"file_type":return voiceFileType(v);case"duration":return v.duration||0;case"dbfs":return(_b2=voiceDbfs(v))!=null?_b2:-999;case"benchmark":case"factor":return-((_c2=voiceFactor(v))!=null?_c2:-999);case"elapsed":return(_d2=voiceBenchmarkElapsed(v))!=null?_d2:999;case"bench_audio":return(_e2=voiceBenchmarkAudioSec(v))!=null?_e2:999;case"wpm":return(_f2=voiceWpm(v))!=null?_f2:-1;case"transcript":return(v.transcript||"").toLowerCase();case"note":return(v.note||"").toLowerCase();case"source":return(_displaySource(v)||"").toLowerCase();case"seed":return v.seed!=null?v.seed:9999999;case"tag":return(v.tag||"").toLowerCase();case"rating":return v.rating||0;case"enabled":return v.enabled===!1?0:1;default:return""}}function setSort(field){_sortDir=_sortField===field?_sortDir*-1:1,_sortField=field,syncSortHeaders(),renderVoiceList()}function toggleSortDir(){_sortDir*=-1,syncSortHeaders(),renderVoiceList()}function syncSortHeaders(){document.querySelectorAll(".vl-header [data-sort], .vl-table-header [data-sort]").forEach(el=>{el.classList.remove("sort-asc","sort-desc"),el.dataset.sort===_sortField&&el.classList.add(_sortDir===1?"sort-asc":"sort-desc")});const sel=document.getElementById("voice-sort-field");sel&&sel.value!==_sortField&&(sel.value=_sortField);const dirBtn=document.getElementById("voice-sort-dir");if(dirBtn){const icon=dirBtn.querySelector(".mdi");icon&&(icon.className=_sortDir===1?"mdi mdi-arrow-up":"mdi mdi-arrow-down"),dirBtn.title=_sortDir===1?"Ascending \u2014 click to reverse":"Descending \u2014 click to reverse"}}document.addEventListener("click",e=>{var _a2,_b2,_c2;if(e.target.closest("#voice-sort-dir")&&toggleSortDir(),e.target.closest("#voice-group-tag-btn")){window._voiceGroupByTag=!window._voiceGroupByTag;try{localStorage.setItem("vl-group-by-tag",window._voiceGroupByTag?"1":"0")}catch{}const btn=document.getElementById("voice-group-tag-btn");btn==null||btn.classList.toggle("active",window._voiceGroupByTag);const icon=btn==null?void 0:btn.querySelector(".mdi");icon&&(icon.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline"),renderVoiceList()}else if(e.target.closest("#voice-table-view-btn")){window._voiceTableView=!window._voiceTableView,window._voiceTableView||(window._voiceTableEditMode=!1);try{localStorage.setItem("vl-table-view",window._voiceTableView?"1":"0")}catch{}const btn=document.getElementById("voice-table-view-btn");btn==null||btn.classList.toggle("active",window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");if(editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode)),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",window._voiceTableView),(_b2=document.querySelector(".voices-workbench"))==null||_b2.classList.toggle("table-edit-mode",!!window._voiceTableEditMode),window._voiceTableView){document.querySelectorAll(".edit-open").forEach(r=>r.classList.remove("edit-open"));const inspector=document.getElementById("voices-inspector");inspector&&(inspector.innerHTML='

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

')}renderVoiceList()}else if(e.target.closest("#voice-table-edit-btn")){window._voiceTableEditMode=!window._voiceTableEditMode;const editBtn=document.getElementById("voice-table-edit-btn");editBtn==null||editBtn.classList.toggle("active",window._voiceTableEditMode),(_c2=document.querySelector(".voices-workbench"))==null||_c2.classList.toggle("table-edit-mode",window._voiceTableEditMode),renderVoiceList()}});try{window._voiceGroupByTag=localStorage.getItem("vl-group-by-tag")==="1"}catch{}try{window._voiceTableView=localStorage.getItem("vl-table-view")==="1"}catch{}document.addEventListener("click",e=>{const th=e.target.closest(".vl-th-sortable");if(th){const field=th.dataset.sort;if(field){const sel=document.getElementById("voice-sort-field");sel&&(sel.value=field),setSort(field)}}}),document.addEventListener("change",e=>{e.target.id==="voice-sort-field"&&setSort(e.target.value)}),document.addEventListener("change",async e=>{if(e.target.classList.contains("vl-inline-edit")){const row=e.target.closest(".vl-row");if(!row)return;const voiceId=row.dataset.id,field=e.target.dataset.field;let value=e.target.type==="checkbox"?e.target.checked:e.target.value;field==="rating"&&(value=parseInt(value)||0);const payload={};payload[field]=value;try{await saveMeta(voiceId,payload);const v=_voices.find(vv=>vv.id===voiceId);v&&(v[field]=value),typeof toast=="function"&&toast("Saved "+field,"success")}catch{typeof toast=="function"&&toast("Failed to save "+field,"error")}}});const FLAG_LANGUAGE_CANDIDATES={GB:["EN"],US:["EN"],AU:["EN"],NZ:["EN"],IE:["EN"],ZA:["EN"],NG:["EN"],KE:["EN"],GH:["EN"],JM:["EN"],TT:["EN"],CA:["EN","FR"],IN:["EN","HI"],SG:["EN","ZH"],PH:["EN","FIL"],MT:["EN","MT"],DE:["DE"],AT:["DE"],CH:["DE","FR","IT"],FR:["FR"],BE:["FR","NL"],LU:["FR","DE"],ES:["ES"],MX:["ES"],AR:["ES"],CO:["ES"],CL:["ES"],PE:["ES"],VE:["ES"],UY:["ES"],EC:["ES"],BO:["ES"],CR:["ES"],CU:["ES"],DO:["ES"],PT:["PT"],BR:["PT"],IT:["IT"],NL:["NL"],PL:["PL"],SE:["SV"],DK:["DA"],NO:["NO"],FI:["FI"],IS:["IS"],GR:["EL"],CY:["EL","TR"],CZ:["CS"],SK:["SK"],HU:["HU"],RO:["RO"],BG:["BG"],HR:["HR"],SI:["SL"],RS:["SR"],BA:["BS"],ME:["SR"],MK:["MK"],AL:["SQ"],EE:["ET"],LV:["LV"],LT:["LT"],UA:["UK"],RU:["RU"],BY:["RU"],MD:["RO"],TR:["TR"],CN:["ZH"],TW:["ZH"],HK:["ZH"],MO:["ZH"],JP:["JA"],KR:["KO"],VN:["VI"],TH:["TH"],ID:["ID"],MY:["MS"],PK:["UR"],BD:["BN"],LK:["SI"],NP:["NE"],SA:["AR"],EG:["AR"],AE:["AR"],MA:["AR"],QA:["AR"],KW:["AR"],OM:["AR"],JO:["AR"],LB:["AR"],IQ:["AR"],IR:["FA"],IL:["HE"]},FLAG_LANGUAGE=Object.fromEntries(Object.entries(FLAG_LANGUAGE_CANDIDATES).map(([cc,langs])=>[cc,langs[0]])),LANGUAGE_LABELS={EN:"English",DE:"German",FR:"French",ES:"Spanish",PT:"Portuguese",IT:"Italian",NL:"Dutch",PL:"Polish",SV:"Swedish",DA:"Danish",NO:"Norwegian",FI:"Finnish",IS:"Icelandic",EL:"Greek",MT:"Maltese",CS:"Czech",SK:"Slovak",HU:"Hungarian",RO:"Romanian",BG:"Bulgarian",HR:"Croatian",SL:"Slovenian",SR:"Serbian",BS:"Bosnian",MK:"Macedonian",SQ:"Albanian",ET:"Estonian",LV:"Latvian",LT:"Lithuanian",UK:"Ukrainian",RU:"Russian",ZH:"Chinese",JA:"Japanese",KO:"Korean",VI:"Vietnamese",TH:"Thai",ID:"Indonesian",MS:"Malay",FIL:"Filipino",HI:"Hindi",UR:"Urdu",BN:"Bengali",SI:"Sinhala",NE:"Nepali",AR:"Arabic",FA:"Persian",HE:"Hebrew",TR:"Turkish"},SEX_FILTER_LABELS={F:"\u2640 Female",M:"\u2642 Male",N:"\u26A5 Diverse / neutral"};function voiceLangFromName(v){return(v.lang||String(v.id||"").split("_")[0]||"").toUpperCase()}function libraryVoiceLang(v){const fromName=voiceLangFromName(v),candidates=FLAG_LANGUAGE_CANDIDATES[String(v.flag||"").toUpperCase()];return candidates!=null&&candidates.length?candidates.includes(fromName)?fromName:candidates[0]:fromName}function libraryLanguageLabel(code){return LANGUAGE_LABELS[code]||code}function populateLibraryFilters(){const langSel=$("library-filter-lang"),sexSel=$("library-filter-sex"),typeSel=$("library-filter-type"),tagSel=$("library-filter-tag"),groupSel=$("library-filter-group");if(!langSel||!sexSel||!typeSel)return;const langSet=new Set,sexSet=new Set,typeSet=new Set,tagSet=new Set,groupSet=new Set;(_voices||[]).forEach(v=>{const lang=libraryVoiceLang(v);lang&&langSet.add(lang),v.gender&&sexSet.add(v.gender);const type=voiceFileType(v);type&&typeSet.add(type),String(v.tag||"").split(",").map(t=>t.trim()).filter(Boolean).forEach(t=>tagSet.add(t));const g=(v.group||"").trim();g&&groupSet.add(g)});const langs=[...langSet].sort((a,b)=>libraryLanguageLabel(a).localeCompare(libraryLanguageLabel(b))),sexOrder=["F","M","N"],sexes=[...sexSet].sort((a,b)=>(sexOrder.indexOf(a)<0?99:sexOrder.indexOf(a))-(sexOrder.indexOf(b)<0?99:sexOrder.indexOf(b))),types=[...typeSet].sort(),tags=[...tagSet].sort((a,b)=>a.localeCompare(b)),groups=[...groupSet].sort((a,b)=>a.localeCompare(b)),sig=JSON.stringify([langs,sexes,types,tags,groups]);if(sig===_libraryFilterOptionsSig)return;_libraryFilterOptionsSig=sig;const keep={lang:langSel.value,sex:sexSel.value,type:typeSel.value,tag:tagSel==null?void 0:tagSel.value,group:groupSel==null?void 0:groupSel.value};langSel.innerHTML=''+langs.map(x=>``).join(""),sexSel.innerHTML=''+sexes.map(x=>``).join(""),typeSel.innerHTML=''+types.map(x=>``).join(""),tagSel&&(tagSel.innerHTML=''+tags.map(x=>``).join("")),groupSel&&(groupSel.innerHTML=''+groups.map(x=>``).join("")),langSel.value=langs.includes(keep.lang)?keep.lang:"",sexSel.value=sexes.includes(keep.sex)?keep.sex:"",typeSel.value=types.includes(keep.type)?keep.type:"",tagSel&&(tagSel.value=tags.includes(keep.tag)?keep.tag:""),groupSel&&(groupSel.value=groups.includes(keep.group)?keep.group:"")}function readLibraryFilters(){var _a2,_b2,_c2,_d2,_e2;_libraryFilters.text=(((_a2=$("library-filter-text"))==null?void 0:_a2.value)||"").trim().toLowerCase(),_libraryFilters.lang=((_b2=$("library-filter-lang"))==null?void 0:_b2.value)||"",_libraryFilters.sex=((_c2=$("library-filter-sex"))==null?void 0:_c2.value)||"",_libraryFilters.type=((_d2=$("library-filter-type"))==null?void 0:_d2.value)||"",_libraryFilters.rating=((_e2=$("library-filter-rating"))==null?void 0:_e2.value)||""}function libraryFilterMatch(v){const f=_libraryFilters;if(f.lang&&libraryVoiceLang(v)!==f.lang||f.sex&&(v.gender||"")!==f.sex||f.type&&voiceFileType(v)!==f.type)return!1;if(f.rating){const r=Number(v.rating||0),wanted=Number(f.rating);if(wanted===0&&r!==0||wanted===1&&r<1||wanted>1&&rString(x||"").toLowerCase()).join(" ").includes(f.text))}function clearLibraryFilters(){["library-filter-text","library-filter-lang","library-filter-sex","library-filter-type","library-filter-rating"].forEach(id=>{const el=$(id);el&&(el.value="")}),readLibraryFilters(),renderVoiceList()}function libraryTtsBackend(){var _a2;return((_a2=$("library-tts-backend-select"))==null?void 0:_a2.value)||"voice_clone"}function needsDuration(v){return v.duration==null||Number.isNaN(Number(v.duration))}function voiceFileType(v){if(v.file_type)return String(v.file_type).replace(/^\./,"").toLowerCase();const match=String(v.path||v.filename||"").match(/\.([A-Za-z0-9]+)(?:$|[?#])/);return match?match[1].toLowerCase():"wav"}function voiceDbfs(v){var _a2;const value=v.loudness&&((_a2=v.loudness.dbfs)!=null?_a2:v.loudness.after_dbfs);return value==null||Number.isNaN(Number(value))?null:Number(value)}function fmtDbfs(v){const db=voiceDbfs(v);return db==null?"-":db.toFixed(1)}function voiceBenchmark(v){return v.benchmark&&typeof v.benchmark=="object"&&Object.keys(v.benchmark).length>0?v.benchmark:null}function voiceBenchmarkElapsed(v){const b=voiceBenchmark(v),value=b&&b.elapsed_sec;return value==null||Number.isNaN(Number(value))?null:Number(value)}function voiceFactor(v){const b=voiceBenchmark(v);return b&&b.ok&&b.speed!=null?Number(b.speed):null}function fmtFactor(v){const f=voiceFactor(v);return f!=null?f.toFixed(2)+"x":"-"}function fmtElapsed(v){const e=voiceBenchmarkElapsed(v),b=voiceBenchmark(v);return!b||!b.ok?b&&!b.ok?"ERR":"-":e!=null?e.toFixed(1)+"s":"-"}function fmtBenchmark(v){const elapsed=fmtElapsed(v),factor=fmtFactor(v);return elapsed==="-"&&factor==="-"?"-":[elapsed,factor].filter(x=>x!=="-").join(" \xB7 ")}function benchmarkClass(v){const b=voiceBenchmark(v);if(!b)return"";if(!b.ok||b.clipped||b.realtime_ok===!1)return"bench-bad";const elapsed=voiceBenchmarkElapsed(v);return elapsed!=null&&elapsed<=4?"bench-ok":"bench-warn"}function voiceBenchmarkAudioSec(v){const b=voiceBenchmark(v);return b&&b.ok&&b.audio_sec!=null?Number(b.audio_sec):null}function fmtBenchmarkAudio(v){const sec=voiceBenchmarkAudioSec(v);return sec!=null?sec.toFixed(1)+"s":"-"}function voiceWpm(v){const b=voiceBenchmark(v);if(!b||!b.ok||!b.audio_sec||!b.text)return null;const words=b.text.trim().split(/\s+/).length;return Math.round(words/(b.audio_sec/60))}function fmtWpm(v){const wpm=voiceWpm(v);return wpm!=null?wpm+" wpm":"-"}function voiceFileUrl(v){const bust=v._audioVersion||v.updated_at||v.benchmarked_at||""||Date.now();return`/api/voice-file?path=${encodeURIComponent(v.path)}&v=${encodeURIComponent(bust)}`}function markVoiceAudioChanged(v){v._audioVersion=Date.now()}function benchmarkTitle(v){const b=voiceBenchmark(v);if(!b)return"Not benchmarked yet";const parts=[];return b.ok?(parts.push(`total ${Number(b.elapsed_sec||0).toFixed(2)}s`),b.ttfa_ms!=null&&parts.push(`TTFA ${Number(b.ttfa_ms).toFixed(0)}ms`),b.audio_sec!=null&&parts.push(`audio ${Number(b.audio_sec).toFixed(2)}s`),b.rtf!=null&&parts.push(`RTF ${Number(b.rtf).toFixed(2)}`),b.speed!=null&&parts.push(`speed ${Number(b.speed).toFixed(2)}x real-time`),b.clipped&&parts.push("output clipped")):(parts.push("benchmark failed"),b.error&&parts.push(b.error)),Array.isArray(b.advice)&&b.advice.length&&parts.push(b.advice.join(" | ")),b.benchmarked_at&&parts.push(`saved ${b.benchmarked_at}`),parts.join(" \xB7 ")}async function clientVoiceLoudness(v){if(!v.path)throw new Error("No audio path");const resp=await fetch(voiceFileUrl(v),{cache:"no-store"});if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const audioData=await resp.arrayBuffer(),buffer=await new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(audioData.slice(0));let sum=0,peak=0,count=0;for(let ch=0;ch0?20*Math.log10(rms):null,peakDbfs=peak>0?20*Math.log10(peak):null;return{dbfs:dbfs==null?null:Number(dbfs.toFixed(2)),peak_dbfs:peakDbfs==null?null:Number(peakDbfs.toFixed(2))}}async function clientCalculateVoiceDb(){const voices=_bulkSelected&&_bulkSelected.size>0?(_voices||[]).filter(v=>_bulkSelected.has(v.id)):visibleLibraryVoices(),errors=[];let calculated=0;const stats={startedAt:Date.now(),ok:0,slow:0,errors:0,middleLabel:"Skipped"};setBenchmarkProgress(0,voices.length,"Preparing dB scan...",stats);for(const v of voices){setBenchmarkProgress(calculated+errors.length,voices.length,`Calculating dB: ${v.id}`,stats);try{v.loudness=await clientVoiceLoudness(v),await saveMeta(v.id,{loudness:v.loudness}).catch(()=>{}),calculated++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs(v)} dBFS`,status(`Calculated dB: ${calculated} / ${voices.length}`)}catch(e){errors.push({voice_id:v.id,detail:e.message}),stats.errors++,stats.last=`${v.id}: ${e.message}`}setBenchmarkProgress(calculated+errors.length,voices.length,`Calculating dB: ${v.id}`,stats),await new Promise(resolve=>setTimeout(resolve,0))}return setBenchmarkProgress(voices.length,voices.length,"dB scan complete",stats),{calculated,errors,voices:voices.map(v=>({voice_id:v.id,loudness:v.loudness}))}}async function hydrateVoiceDuration(v,el){if(!(!v.path||!needsDuration(v)||v._durationLoading)){v._durationLoading=!0;try{const audio=new Audio;audio.preload="metadata",audio.src=voiceFileUrl(v),await new Promise((resolve,reject)=>{audio.onloadedmetadata=resolve,audio.onerror=()=>reject(new Error("Could not read duration"))}),Number.isFinite(audio.duration)&&audio.duration>0&&(v.duration=audio.duration,el&&document.body.contains(el)&&(el.textContent=fmtDuration(v.duration),el.title=String(v.duration.toFixed(2)))),audio.removeAttribute("src"),audio.load()}catch(e){el&&document.body.contains(el)&&(el.title=e.message)}finally{v._durationLoading=!1}}}document.querySelectorAll(".vl-header [data-sort]").forEach(el=>el.addEventListener("click",()=>setSort(el.dataset.sort)));function dominantLanguages(limit=3){const counts=new Map;return(_voices||[]).forEach(v=>{const lang=(v.lang||String(v.id||"").split("_")[0]||"?").toUpperCase();counts.set(lang,(counts.get(lang)||0)+1)}),[...counts.entries()].sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0])).slice(0,limit).map(([lang,count])=>`${lang} ${count}`).join(" \xB7 ")||"-"}function updateLibraryInsights(state="ready"){const el=$("library-insights");if(!el)return;if(state==="loading"){el.innerHTML=[["\u2026","Loading"],["\u2026","Active"],["\u2026","Languages"],["\u2026","Benchmarks"],["\u2026","Quality"],["\u2026","Actions"]].map(([value,label])=>`
${value}${label}
`).join("");return}if(state==="error"){el.innerHTML='
FailedLibrary load
';return}const total=_voices.length,active=_voices.filter(v=>v.enabled!==!1).length,hidden=total-active,bench=_voices.map(voiceBenchmark).filter(Boolean),slow=bench.filter(b=>b&&b.ok&&b.realtime_ok===!1).length,dbValues=_voices.map(voiceDbfs).filter(v=>v!=null),avgDb=dbValues.length?(dbValues.reduce((a,b)=>a+b,0)/dbValues.length).toFixed(1):"-",missingRef=_voices.filter(v=>!v.transcript).length,restart=_voices.filter(v=>v.needs_tts_restart).length,tiles=[{value:`${_voices.filter(v=>$("show-disabled-cb").checked||v.enabled!==!1).length}/${total}`,label:"Visible"},{value:`${active} on`,label:hidden?`${hidden} hidden`:"Active"},{value:dominantLanguages(),label:"Languages"},{value:bench.length?`${bench.length} done`:"-",label:slow?`${slow} slow`:"Benchmarks",filter:slow?"slow":"",title:slow?describeIssueVoices("slow"):"No slow voices"},{value:avgDb==="-"?"-":`${avgDb} dB`,label:missingRef?`${missingRef} no text`:"Avg loudness",filter:missingRef?"no_text":"",title:missingRef?describeIssueVoices("no_text"):"All visible voices have reference text"},{value:restart||"-",label:restart?"Need restart":"Restart flags",filter:restart?"restart":"",title:restart?describeIssueVoices("restart"):"No voices need restart"}];el.innerHTML=tiles.map(item=>{const filter=item.filter?` data-filter="${escHtml(item.filter)}" role="button" tabindex="0"`:"",activeCls=item.filter&&item.filter===_libraryIssueFilter?" active":"",title=item.title?` title="${escHtml(item.title)}"`:"";return`
${escHtml(item.value)}${escHtml(item.label)}
`}).join(""),el.querySelectorAll("[data-filter]").forEach(tile=>{const activate=()=>setLibraryIssueFilter(tile.dataset.filter||"");tile.addEventListener("click",activate),tile.addEventListener("keydown",e=>{(e.key==="Enter"||e.key===" ")&&(e.preventDefault(),activate())})})}function shouldRenderVoiceLibrary(){const section=$("s-voices");return!section||section.classList.contains("is-active")}async function loadVoiceLibrary(options={}){const forceRefresh=!!(options&&options.refresh);return _libraryLoadPromise?_libraryLoadPromise.then(()=>{shouldRenderVoiceLibrary()&&_voices.length&&renderVoiceList()}):(_libraryLoadPromise=(async()=>{setBusyButton("refresh-voices-btn",!0);const list=$("voice-list"),renderVisibleList=shouldRenderVoiceLibrary(),cached=_voices.length===0?_vlCacheRead():null;cached&&Array.isArray(cached)&&cached.length&&(_voices=cached,window._voices=_voices,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices),renderVisibleList&&renderVoiceList(),updatePreviewVoiceMatchPanel(),status(`Loaded ${_voices.length} voices`));const silent=_voices.length>0;list&&!silent&&renderVisibleList&&(list.innerHTML=loadingMarkup("Loading voice library","Scanning voices, reference text, metadata, ratings, and benchmark results.",8)),!silent&&renderVisibleList&&($("voice-count").textContent="Loading voices\u2026",updateLibraryInsights("loading"),status("Loading voice library\u2026"));try{const r=await fetch("/api/voices"+(forceRefresh?"?refresh=1":""));if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const fresh=await r.json();_vlCacheWrite(fresh),_voices=fresh,window._voices=_voices,typeof window.updateVoiceTree=="function"&&window.updateVoiceTree(_voices),shouldRenderVoiceLibrary()&&renderVoiceList(),updatePreviewVoiceMatchPanel(),typeof renderPerfHistory=="function"&&renderPerfHistory(),status(`Loaded ${_voices.length} voices`)}catch(e){if(!silent&&renderVisibleList&&(list&&(list.innerHTML='
Failed to load voices: '+(e.message||String(e))+"
"),$("voice-count").textContent="Load failed",updateLibraryInsights("error")),status("Voice library load failed: "+e.message),!silent)throw e}finally{setBusyButton("refresh-voices-btn",!1),_libraryLoadPromise=null}})(),_libraryLoadPromise)}$("refresh-voices-btn").addEventListener("click",()=>{_vlCacheClear(),loadVoiceLibrary({refresh:!0})}),$("sync-voice-folders-btn").addEventListener("click",async()=>{$("sync-voice-folders-btn").disabled=!0,status("Syncing active_voices and hidden_voices\u2026");try{const r=await fetch("/api/voices/sync-folders",{method:"POST"});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();await loadVoiceLibrary();const conflicts=d.conflicts&&d.conflicts.length?`, ${d.conflicts.length} conflicts`:"";toast(`Synced: ${d.moved.active} active, ${d.moved.hidden} hidden${conflicts}`,d.conflicts&&d.conflicts.length?"error":"success"),status("Synced folders. Restart Qwen3-TTS after changing active voices.")}catch(e){toast("Sync failed: "+e.message,"error"),status("Folder sync failed")}finally{$("sync-voice-folders-btn").disabled=!1}});function visibleLibraryVoices(){const showDisabled=$("show-disabled-cb").checked;return _voices.filter(v=>showDisabled||v.enabled!==!1)}function libraryIssueMatch(v,filter=_libraryIssueFilter){const b=voiceBenchmark(v);return filter==="slow"?!!(b&&b.ok&&b.realtime_ok===!1):filter==="no_text"?!String(v.transcript||"").trim():filter==="restart"?!!v.needs_tts_restart:!0}function libraryIssueLabel(filter=_libraryIssueFilter){return{slow:"slow benchmark voices",no_text:"voices without reference text",restart:"voices needing TTS restart"}[filter]||"all voices"}function libraryIssueVoices(filter=_libraryIssueFilter){return visibleLibraryVoices().filter(v=>libraryIssueMatch(v,filter))}function describeIssueVoices(filter=_libraryIssueFilter,limit=12){const voices=libraryIssueVoices(filter).map(v=>v.id);if(!voices.length)return"No matching voices";const extra=voices.length>limit?`, +${voices.length-limit} more`:"";return voices.slice(0,limit).join(", ")+extra}function setLibraryIssueFilter(filter=""){_libraryIssueFilter=_libraryIssueFilter===filter?"":filter,renderVoiceList(),status(_libraryIssueFilter?`${libraryIssueLabel()}: ${describeIssueVoices()}`:"Showing all visible voices")}function libraryTargetDb(){var _a2;const input=$("library-target-db"),raw=Number((_a2=input==null?void 0:input.value)!=null?_a2:-20),value=Number.isFinite(raw)?Math.min(-1,Math.max(-60,raw)):-20;return input&&(input.value=String(value)),value}$("calculate-db-btn").addEventListener("click",async()=>{$("calculate-db-btn").disabled=!0;const _calcTarget=_bulkSelected&&_bulkSelected.size>0?`${_bulkSelected.size} selected`:"visible";status(`Calculating voice loudness (${_calcTarget})\u2026`);try{const d=await clientCalculateVoiceDb();renderVoiceList();const extra=d.errors&&d.errors.length?`, ${d.errors.length} errors`:"";toast(`Calculated dB for ${d.calculated} voices${extra}`,d.errors&&d.errors.length?"error":"success"),status("Calculated voice loudness. Use Normalize volume for visible WAV voices.")}catch(e){toast("Calculate dB failed: "+e.message,"error"),status("dB calculation failed")}finally{$("calculate-db-btn").disabled=!1}}),$("normalize-volume-btn").addEventListener("click",async()=>{var _a2;const target=libraryTargetDb(),visible=visibleLibraryVoices(),voices=visible.filter(v=>voiceFileType(v)==="wav"),skipped=visible.length-voices.length;if(!voices.length){toast("No visible WAV voices to normalize","error");return}if(!confirm(`Normalize ${voices.length} visible WAV voices to ${target} dBFS?${skipped?` ${skipped} non-WAV voices will be skipped.`:""}`))return;$("normalize-volume-btn").disabled=!0,$("calculate-db-btn").disabled=!0;const stats={startedAt:Date.now(),ok:0,slow:skipped,errors:0,middleLabel:"Skipped"},errors=[];let normalized=0;setBenchmarkProgress(0,voices.length,`Normalizing to ${target} dBFS...`,stats),status(`Normalizing ${voices.length} voices to ${target} dBFS...`);try{for(const v of voices){setBenchmarkProgress(normalized+errors.length,voices.length,`Normalizing: ${v.id}`,stats);try{const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:target})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a2=d.duration)!=null?_a2:v.duration,v.file_type=d.file_type||v.file_type,v.path=d.path||v.path,v.needs_tts_restart=!0,markVoiceAudioChanged(v),normalized++,stats.ok++,stats.last=`${v.id}: ${fmtDbfs(v)} dBFS`}catch(e){errors.push({voice_id:v.id,detail:e.message}),stats.errors++,stats.last=`${v.id}: ${e.message}`}setBenchmarkProgress(normalized+errors.length,voices.length,`Normalizing: ${v.id}`,stats),status(`Normalized ${normalized} / ${voices.length}`),await new Promise(resolve=>setTimeout(resolve,0))}setBenchmarkProgress(voices.length,voices.length,"Volume normalization complete",stats),renderVoiceList(),updateLibraryInsights();const extra=`${skipped?`, ${skipped} skipped`:""}${errors.length?`, ${errors.length} errors`:""}`;toast(`Normalized ${normalized} voices${extra}`,errors.length?"error":"success"),status("Volume normalized. Restart TTS before rebenchmarking these voices.")}catch(e){toast("Normalize volume failed: "+e.message,"error"),status("Normalize volume failed")}finally{$("normalize-volume-btn").disabled=!1,$("calculate-db-btn").disabled=!1}});function fmtClock(ms){if(!Number.isFinite(ms)||ms<0)return"-";const total=Math.round(ms/1e3),m=Math.floor(total/60),s=total%60;return`${m}:${String(s).padStart(2,"0")}`}function setBenchmarkProgress(done,total,label="",stats={}){const panel=$("benchmark-progress"),track=panel.querySelector(".benchmark-progress-track"),pct=total?Math.round(done/total*100):0;panel.hidden=!1,$("benchmark-progress-label").textContent=label||(done>=total?"Benchmark complete":"Benchmarking voices..."),$("benchmark-progress-count").textContent=`${done} / ${total}`,$("benchmark-progress-bar").style.width=pct+"%",track.setAttribute("aria-valuenow",String(pct));const live=$("benchmark-live-stats");if(live){const elapsed=stats.startedAt?Date.now()-stats.startedAt:0,avg=done>0?elapsed/done:0,eta=done>0&&total>done?avg*(total-done):0;live.innerHTML=[`Elapsed ${fmtClock(elapsed)}`,`Avg ${done?(avg/1e3).toFixed(1)+"s":"-"}`,`ETA ${done&&total>done?fmtClock(eta):"-"}`,`OK ${stats.ok||0}`,`${stats.middleLabel||"Slow"} ${stats.slow||0}`,`${stats.errorLabel||"Errors"} ${stats.errors||0}`].map(x=>`${escHtml(x)}`).join("")}const last=$("benchmark-live-last");last&&stats.last&&(last.textContent=stats.last)}function hideBenchmarkProgress(){$("benchmark-progress").hidden=!0,$("benchmark-progress-bar").style.width="0%",$("benchmark-live-last")&&($("benchmark-live-last").textContent="")}async function clearTtsRestartFlags(){const r=await fetch("/api/tts/restart-flags/clear",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return _voices.forEach(voice=>{voice.needs_tts_restart=!1}),document.querySelectorAll(".vl-row.edit-open").forEach(row=>row.classList.remove("opt-restart-needed")),updateLibraryInsights(),d}async function runVoiceBenchmark(voiceId="",opts={}){var _a2;const text=(_a2=opts.text)!=null?_a2:benchmarkSampleText();if(!text)return toast("Enter a benchmark sample sentence","error"),null;const payload={active_only:!0,text};voiceId&&(payload.voice_id=voiceId);const r=await fetch("/api/voices/benchmark",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(payload)});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return r.json()}async function runVoiceBenchmarkBatch(){const voices=benchmarkTargetVoices(),text=benchmarkSampleText();if(!text)return toast("Enter a benchmark sample sentence","error"),null;if(!voices.length)return toast("No voices to benchmark","error"),null;const total=voices.length,aggregate={benchmarked:0,errors:[],voices:[],text,active_only:!0},stats={startedAt:Date.now(),ok:0,slow:0,errors:0,last:""};voices.forEach(v=>{const row=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);row&&(row.classList.remove("benchmarking-active","benchmarking-done"),row.classList.add("benchmarking-pending"))}),setBenchmarkProgress(0,total,"Starting benchmark...",stats);for(let i=0;ix.voice_id===voice.id),b=hit&&hit.benchmark;if(b&&b.ok){if(stats.ok++,b.realtime_ok===!1&&stats.slow++,stats.last=`${voice.id}: ${Number(b.elapsed_sec||0).toFixed(1)}s${b.speed!=null?` \xB7 ${Number(b.speed).toFixed(2)}x`:""}${b.realtime_ok===!1?" \xB7 slow":""}`,row){const bc=benchmarkClass(voice),btitle=benchmarkTitle(voice),durCell=row.querySelector(".vl-tbl-dur"),factorCell=row.querySelector(".vl-tbl-factor"),timeCell=row.querySelector(".vl-tbl-time"),wpmCell=row.querySelector(".vl-tbl-wpm");if(durCell&&(durCell.textContent=fmtBenchmarkAudio(voice),durCell.title=`${fmtBenchmarkAudio(voice)} \u2014 length of synthesised benchmark audio`),factorCell&&(factorCell.textContent=fmtFactor(voice),factorCell.className=`vl-tbl-factor ${bc}`,factorCell.title=btitle),timeCell&&(timeCell.textContent=fmtElapsed(voice),timeCell.className=`vl-tbl-time ${bc}`,timeCell.title=btitle),wpmCell){const wpm=voiceWpm(voice);wpmCell.textContent=fmtWpm(voice),wpmCell.title=wpm!=null?`${wpm} wpm \u2014 130\u2013180 wpm is natural for long listening`:""}}}else stats.errors++,stats.last=`${voice.id}: failed${b&&b.error?" \xB7 "+b.error:""}`}}catch(e){aggregate.errors.push({voice_id:voice.id,detail:e.message}),stats.errors++,stats.last=`${voice.id}: failed \xB7 ${e.message}`}row&&(row.classList.remove("benchmarking-active"),row.classList.add("benchmarking-done")),setBenchmarkProgress(i+1,total,`Finished ${voice.id}`,stats)}return voices.forEach(v=>{const row=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);row&&row.classList.remove("benchmarking-pending")}),setBenchmarkProgress(total,total,"Benchmark complete",stats),aggregate}function mergeBenchmarkResults(d){const byId=new Map((d.voices||[]).map(x=>[x.voice_id,x]));_voices.forEach(v=>{const hit=byId.get(v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark)})}window.loadVoiceLibrary=loadVoiceLibrary,window.mergeBenchmarkResults=mergeBenchmarkResults;function activeBenchmarkVoices(){return(_voices||[]).filter(v=>v.enabled!==!1)}function benchmarkTargetVoices(){return typeof _bulkSelected!="undefined"&&_bulkSelected.size>0?(_voices||[]).filter(v=>_bulkSelected.has(v.id)):activeBenchmarkVoices()}function showBenchmarkConfirm(){const voices=benchmarkTargetVoices(),onlySelected=typeof _bulkSelected!="undefined"&&_bulkSelected.size>0;if(!benchmarkSampleText()){toast("Enter a benchmark sample sentence","error");return}if(!voices.length){toast("No voices to benchmark","error");return}const staleCount=voices.filter(v=>v.needs_tts_restart).length;$("benchmark-confirm-title").textContent=`Benchmark ${voices.length} ${onlySelected?"selected":"active"} voice${voices.length===1?"":"s"}?`,$("benchmark-confirm-text").textContent="This sends the sample sentence to each active voice and can keep the GPU busy for a while. Progress updates after every voice."+(staleCount?` ${staleCount} edited voice${staleCount===1?"":"s"} should be restarted first, otherwise cached old voices may be benchmarked.`:""),$("benchmark-confirm").hidden=!1,$("benchmark-confirm-start").focus()}function hideBenchmarkConfirm(){const panel=$("benchmark-confirm");panel&&(panel.hidden=!0)}$("benchmark-voices-btn").addEventListener("click",showBenchmarkConfirm),(_z=$("benchmark-confirm-cancel"))==null||_z.addEventListener("click",hideBenchmarkConfirm),(_A=$("benchmark-confirm-start"))==null||_A.addEventListener("click",async()=>{hideBenchmarkConfirm(),$("benchmark-voices-btn").disabled=!0,$("benchmark-confirm-start").disabled=!0,status("Benchmarking active voices...");try{const d=await runVoiceBenchmarkBatch();if(!d)return;mergeBenchmarkResults(d),await loadVoiceLibrary();const slow=(d.voices||[]).filter(x=>x.benchmark&&x.benchmark.realtime_ok===!1).length,extra=d.errors&&d.errors.length?`, ${d.errors.length} errors`:"";toast(`Benchmarked ${d.benchmarked} voices${slow?`, ${slow} slow`:""}${extra}`,d.errors&&d.errors.length?"error":"success"),status("Benchmark saved with TTFA, total time, RTF, and speed.")}catch(e){toast("Benchmark failed: "+e.message,"error"),status("Benchmark failed")}finally{$("benchmark-voices-btn").disabled=!1,$("benchmark-confirm-start").disabled=!1}}),$("copy-active-voices-btn").addEventListener("click",async()=>{const useSelected=_bulkSelected&&_bulkSelected.size>0,ids=useSelected?[..._bulkSelected]:activeVoiceIds(),label=useSelected?`${ids.length} selected`:`${ids.length} active`;if(!ids.length){toast("No voices to copy","error");return}await copyText(ids.join(", ")),toast("Copied "+label+" voices","success"),status("Copied "+label+" voices to clipboard")});async function _verifyVoicesRoundtrip(ids,opts){opts=opts||{};const results=[],queue=ids.slice(),worker=async()=>{for(;queue.length;){const id=queue.shift(),v=(window._voices||[]).find(x=>x.id===id),text=v&&v.transcript&&v.transcript.trim()||getLibAddSampleText(v&&v.lang||"EN"),backend=typeof _ttsBackendForVoice=="function"?_ttsBackendForVoice(id,libraryTtsBackend()):libraryTtsBackend();try{const rt=await _voiceRoundtripCheck(id,text,backend);results.push({id,text,transcript:rt.transcript,score:rt.score,ok:rt.score>=.5})}catch(e){results.push({id,text,error:e.message||String(e),ok:!1})}opts.onProgress&&opts.onProgress(results.length,ids.length,id)}};return await Promise.all(Array.from({length:Math.min(2,ids.length)},worker)),results}(_B=$("verify-voices-stt-btn"))==null||_B.addEventListener("click",async()=>{const ids=_bulkSelected&&_bulkSelected.size>0?[..._bulkSelected]:activeVoiceIds();if(!ids.length){toast("No voices to verify","error");return}if(!confirm(`Verify ${ids.length} voice(s) by transcribing a synthesized line back with Whisper and comparing it to the original text? This makes one extra synth + STT call per voice.`))return;const btn=$("verify-voices-stt-btn");btn&&(btn.disabled=!0);const ov=document.createElement("div");ov.className="audiobook-overlay",ov.id="verify-voices-overlay",ov.innerHTML=`
Verifying voices
+
0 / ${ids.length}
+
+
`,document.body.appendChild(ov);let cancelled=!1;ov.querySelector("#vv-cancel").addEventListener("click",()=>{cancelled=!0});const fill=ov.querySelector("#vv-fill"),msg=ov.querySelector("#vv-msg"),results=await _verifyVoicesRoundtrip(ids,{onProgress:(done,total,id)=>{if(fill&&(fill.style.width=done/total*100+"%"),msg&&(msg.textContent=`${done} / ${total} \xB7 ${id}`),cancelled)throw new Error("cancelled")}}).catch(()=>[]);ov.remove(),btn&&(btn.disabled=!1);const failed=results.filter(r=>!r.ok),resOv=document.createElement("div");resOv.className="audiobook-overlay",resOv.innerHTML=`
+
Voice verification results
+

${results.length-failed.length} / ${results.length} passed.${failed.length?" Failed voices likely need a redesign.":""}

+
+ ${failed.map(r=>`
+ ${escHtml(r.id)} \u2014 ${r.error?escHtml(r.error):"score "+r.score.toFixed(2)} + ${r.transcript!==void 0?`
Expected: ${escHtml(r.text.slice(0,120))}
Heard: ${escHtml((r.transcript||"(nothing)").slice(0,120))}`:""} +
`).join("")||'
All voices passed.
'} +
+
+ +
+
`,document.body.appendChild(resOv),resOv.querySelector("#vv-close").addEventListener("click",()=>resOv.remove()),resOv.addEventListener("click",e=>{e.target===resOv&&resOv.remove()}),toast(`Verified ${results.length} voice(s) \u2014 ${failed.length} failed`,failed.length?"error":"success")}),(_C=$("precompute-embeddings-btn"))==null||_C.addEventListener("click",async()=>{const backend=libraryTtsBackend(),_useSelected=_bulkSelected&&_bulkSelected.size>0,ids=_useSelected?[..._bulkSelected]:activeVoiceIds(),_scopeLabel=_useSelected?`${ids.length} selected`:`${ids.length} active`;if(!ids.length){toast("No voices to precompute","error");return}if(!confirm(`Precompute speaker embeddings for ${_scopeLabel} voice(s) via \u201C${backend}\u201D? This warms each voice so the engine caches its .pt and first playback is instant.`))return;const btn=$("precompute-embeddings-btn");btn&&(btn.disabled=!0);const ov=document.createElement("div");ov.className="audiobook-overlay",ov.id="precompute-overlay",ov.innerHTML=`
Precomputing embeddings
0 / ${ids.length}
-
`,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}`);try{await fetchTtsPreviewBlob(id,"Hallo.","wav","",backend),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-12&&i>11&&bar.classList.add("hot"),clipped&&i>14&&bar.classList.add("clip"))}),$("lib-add-db-readout").textContent=Number.isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB"}function syncLibAddMicGain(){const gain=parseFloat($("lib-add-mic-gain").value)||0;$("lib-add-mic-gain-value").textContent=gain.toFixed(2)+"x",libAddState.gainNode&&(libAddState.gainNode.gain.value=gain)}function startLibAddMeter(){if(!libAddState.analyser)return;libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf);const data=new Float32Array(libAddState.analyser.fftSize),tick=()=>{libAddState.analyser.getFloatTimeDomainData(data);let sum=0,peak=0;for(const sample of data)sum+=sample*sample,peak=Math.max(peak,Math.abs(sample));const rms=Math.sqrt(sum/data.length),db=rms>0?20*Math.log10(rms):-1/0,level=Number.isFinite(db)?(db+60)/60:0;renderLibAddMeter(level,db,peak>.98),libAddState.meterRaf=requestAnimationFrame(tick)};tick()}async function ensureLibAddMicMonitor(){if(libAddState.recordStream)return;const AudioCtx=window.AudioContext||window.webkitAudioContext;if(libAddState.stream=await requestMicrophoneStream({raw:!0}),AudioCtx){libAddState.audioCtx=new AudioCtx,libAddState.sourceNode=libAddState.audioCtx.createMediaStreamSource(libAddState.stream),libAddState.gainNode=libAddState.audioCtx.createGain(),libAddState.analyser=libAddState.audioCtx.createAnalyser(),libAddState.analyser.fftSize=1024;const dest=libAddState.audioCtx.createMediaStreamDestination();syncLibAddMicGain(),libAddState.sourceNode.connect(libAddState.gainNode),libAddState.gainNode.connect(libAddState.analyser),libAddState.gainNode.connect(dest),libAddState.recordStream=dest.stream,startLibAddMeter()}else libAddState.recordStream=libAddState.stream;libAddState.monitoring=!0,$("lib-add-monitor-btn").disabled=!0,$("lib-add-monitor-stop").disabled=!1}function stopLibAddMic(){libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf),libAddState.meterRaf=null,[libAddState.sourceNode,libAddState.gainNode,libAddState.analyser].forEach(node=>{try{node&&node.disconnect()}catch{}}),libAddState.stream&&libAddState.stream.getTracks().forEach(t=>t.stop()),libAddState.recordStream&&libAddState.recordStream.getTracks().forEach(t=>t.stop()),libAddState.audioCtx&&libAddState.audioCtx.close().catch(()=>{}),libAddState.stream=null,libAddState.recordStream=null,libAddState.sourceNode=null,libAddState.gainNode=null,libAddState.analyser=null,libAddState.audioCtx=null,libAddState.monitoring=!1,$("lib-add-monitor-btn").disabled=!1,$("lib-add-monitor-stop").disabled=!0,renderLibAddMeter(0,-1/0,!1)}let libAddState={id:null,duration:0,audio:null,buffer:null,recorder:null,chunks:[],pendingSource:null,stream:null,recordStream:null,timer:null,secs:0,audioCtx:null,sourceNode:null,gainNode:null,analyser:null,meterRaf:null,monitoring:!1};window.libAddState=libAddState,$("add-new-voice-btn").addEventListener("click",()=>{$("lib-add-panel").classList.toggle("open")}),$("lib-add-sample-lang").addEventListener("change",()=>updateLibAddSampleLanguage($("lib-add-sample-lang").value)),$("lib-add-lang").addEventListener("change",()=>updateLibAddSampleLanguage($("lib-add-lang").value)),$("lib-add-sample-text").addEventListener("input",debounce(saveLibAddSampleText,500)),$("lib-add-use-sample").addEventListener("click",()=>{$("lib-add-transcript").value=$("lib-add-sample-text").value.trim(),setLibAddStatus("Sample sentence copied to transcript")}),$("lib-add-reset-sample").addEventListener("click",resetLibAddSampleText),$("lib-add-mic-help-btn").addEventListener("click",()=>{$("lib-add-mic-help").classList.toggle("open")}),$("lib-add-monitor-btn").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),setLibAddStatus("Mic level monitor active")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message)}}),$("lib-add-monitor-stop").addEventListener("click",()=>{stopLibAddMic(),setLibAddStatus("Mic level monitor stopped")}),$("lib-add-mic-gain").addEventListener("input",syncLibAddMicGain),renderLibAddMeter(),syncLibAddMicGain(),updateLibAddSampleLanguage("EN");function setLibAddStatus(msg){$("lib-add-status").textContent=msg,status(msg)}function suggestLibVoiceId(filename){if($("lib-add-voice-id").value.trim())return;const base=(typeof _umlautSafe=="function"?_umlautSafe(filename||"NewVoice"):String(filename||"NewVoice")).replace(/\.[^.]+$/,"").replace(/[^A-Za-z0-9_-]+/g,"_").replace(/^_+|_+$/g,"").slice(0,60)||"NewVoice";$("lib-add-voice-id").value=`${$("lib-add-lang").value||"EN"}_${$("lib-add-gender").value||"N"}_${base}`}function loadLibAddAudio(id,duration,label="Audio"){libAddState.id=id,libAddState.duration=Number(duration)||0,libAddState.buffer=null,$("lib-add-start").value="0.00",$("lib-add-end").value=libAddState.duration?Math.min(libAddState.duration,20).toFixed(2):"0.00",$("lib-add-audio").src="/api/audio/"+id,$("lib-add-audio").style.display="",$("lib-add-wave").style.display="",attachLibAddWaveSelection(),decodeTempAudio(id).then(buffer=>{libAddState.id===id&&(libAddState.buffer=buffer,drawLibAddWave())}).catch(()=>{}),setLibAddStatus(`${label} loaded${libAddState.duration?" ("+libAddState.duration.toFixed(1)+" s)":""}`)}async function decodeTempAudio(id){const resp=await fetch("/api/audio/"+encodeURIComponent(id));if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const data=await resp.arrayBuffer();return new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(data.slice(0))}function clampLibAddTime(value){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;return Math.max(0,Math.min(duration,Number(value)||0))}function setLibAddCropRange(start,end){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;let a=clampLibAddTime(start),b=clampLibAddTime(end);Math.abs(b-a)<.05&&(b=Math.min(duration,a+Math.min(1,duration||1))),b=3&&dur<=20?"ok":dur?"warn":"")}function drawLibAddWave(){libAddState.buffer&&(drawOptimizerWave($("lib-add-wave"),libAddState.buffer,parseFloat($("lib-add-start").value)||0,parseFloat($("lib-add-end").value)||libAddState.duration||libAddState.buffer.duration),updateLibAddCropHint())}function libAddWaveSelectionPixels(e){var _a2;const rect=$("lib-add-wave").getBoundingClientRect(),duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0,start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||duration),sx=duration&&rect.width?start/duration*rect.width:0,ex=duration&&rect.width?end/duration*rect.width:rect.width;return{x:Math.max(0,Math.min(rect.width,e.clientX-rect.left)),sx,ex,start,end,duration}}function libAddWaveDragMode(e){const{x,sx,ex}=libAddWaveSelectionPixels(e),hit=16;return Math.abs(x-sx)<=hit?"start":Math.abs(x-ex)<=hit?"end":"new"}function attachLibAddWaveSelection(){const canvas=$("lib-add-wave");if(!canvas||canvas.dataset.cropReady)return;canvas.dataset.cropReady="1";let drag=null;canvas.addEventListener("pointerdown",e=>{var _a2;if(!libAddState.buffer)return;e.preventDefault();const mode=libAddWaveDragMode(e),t=libAddWaveTimeFromEvent(e),currentStart=parseFloat($("lib-add-start").value)||0,currentEnd=parseFloat($("lib-add-end").value)||libAddState.duration||0;drag={mode,anchor:t,start:currentStart,end:currentEnd},(_a2=canvas.setPointerCapture)==null||_a2.call(canvas,e.pointerId),mode==="start"?setLibAddCropRange(t,currentEnd):setLibAddCropRange(mode==="end"?currentStart:t,t),setLibAddStatus(mode==="start"?"Dragging crop start handle":mode==="end"?"Dragging crop end handle":"Drag to choose a new crop range")}),canvas.addEventListener("pointermove",e=>{if(!libAddState.buffer)return;if(!drag){const mode=libAddWaveDragMode(e);canvas.style.cursor=mode==="start"||mode==="end"?"ew-resize":"crosshair";return}e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t)});const finish=e=>{if(!drag)return;e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t),drag=null;const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||0;setLibAddStatus(`Crop range ${start.toFixed(2)}s to ${end.toFixed(2)}s (${Math.max(0,end-start).toFixed(1)}s) selected`)};canvas.addEventListener("pointerup",finish),canvas.addEventListener("pointerleave",()=>{drag||(canvas.style.cursor="crosshair")}),canvas.addEventListener("pointercancel",()=>{drag=null,canvas.style.cursor="crosshair"})}async function uploadLibAddFile(file){if(!file)return;const fd=new FormData;fd.append("file",file),setLibAddStatus("Uploading audio\u2026");try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();suggestLibVoiceId(file.name),loadLibAddAudio(d.id,d.duration,file.name||"Audio"),toast("Audio loaded","success")}catch(e){toast("Load failed: "+e.message,"error"),setLibAddStatus("Load failed")}}const libAddDrop=$("lib-add-drop");libAddDrop.addEventListener("click",()=>$("lib-add-file").click()),libAddDrop.addEventListener("dragover",e=>{e.preventDefault(),libAddDrop.classList.add("drag-over")}),libAddDrop.addEventListener("dragleave",()=>libAddDrop.classList.remove("drag-over")),libAddDrop.addEventListener("drop",e=>{e.preventDefault(),libAddDrop.classList.remove("drag-over"),e.dataTransfer.files.length&&uploadLibAddFile(e.dataTransfer.files[0])}),$("lib-add-file").addEventListener("change",async()=>{$("lib-add-file").files.length&&await uploadLibAddFile($("lib-add-file").files[0]),$("lib-add-file").value=""}),$("lib-add-url-btn").addEventListener("click",()=>{const url=$("lib-add-url").value.trim();if(!url){toast("Enter a YouTube or audio URL","error");return}$("lib-add-url-btn").disabled=!0,setLibAddStatus("Starting download\u2026");const es=new EventSource("/api/download-yt?url="+encodeURIComponent(url));es.onmessage=e=>{const d=JSON.parse(e.data);d.error?(toast("Download failed: "+d.error,"error"),setLibAddStatus(d.error),$("lib-add-url-btn").disabled=!1,es.close()):d.done?(es.close(),$("lib-add-url-btn").disabled=!1,suggestLibVoiceId(url.split("/").pop()||"DownloadedVoice"),loadLibAddAudio(d.id,d.duration,"Downloaded audio"),toast("URL audio loaded","success")):setLibAddStatus(d.msg||"Downloading\u2026")},es.onerror=()=>{es.close(),$("lib-add-url-btn").disabled=!1,setLibAddStatus("Download connection closed")}}),$("lib-add-rec-start").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),libAddState.chunks=[],libAddState.secs=0,$("lib-add-rec-time").textContent="0:00",$("lib-add-rec-start").disabled=!0,$("lib-add-rec-stop").disabled=!1,$("lib-add-monitor-stop").disabled=!0,libAddState.timer=setInterval(()=>{libAddState.secs++,$("lib-add-rec-time").textContent=Math.floor(libAddState.secs/60)+":"+String(libAddState.secs%60).padStart(2,"0")},1e3),libAddState.recorder=new MediaRecorder(libAddState.recordStream),libAddState.recorder.ondataavailable=e=>{e.data.size&&libAddState.chunks.push(e.data)},libAddState.recorder.onstop=async()=>{clearInterval(libAddState.timer),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0;const blob=new Blob(libAddState.chunks,{type:libAddState.recorder.mimeType||"audio/webm"}),ext=(libAddState.recorder.mimeType||"").includes("ogg")?".ogg":".webm";stopLibAddMic(),suggestLibVoiceId("recording"),await uploadLibAddFile(new File([blob],"recording"+ext,{type:blob.type}))},libAddState.recorder.start(100),setLibAddStatus("Recording\u2026")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0}}),$("lib-add-rec-stop").addEventListener("click",()=>{libAddState.recorder&&libAddState.recorder.state!=="inactive"&&libAddState.recorder.stop()}),$("lib-add-auto-trim").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}$("lib-add-auto-trim").disabled=!0;try{const r=await fetch("/api/auto-trim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id})});let d;if(r.ok)d=await r.json();else if(r.status===404||r.status===405)d=await clientAutoTrimBounds(libAddState.id);else{const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}$("lib-add-start").value=Number(d.start).toFixed(2),$("lib-add-end").value=Number(d.end).toFixed(2),drawLibAddWave(),setLibAddStatus(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error"),setLibAddStatus("Auto trim failed")}finally{$("lib-add-auto-trim").disabled=!1}});async function transcribeLibAddCurrent(successMessage="Text recognised",audioId=libAddState.id){if(!audioId)throw new Error("Load audio first");setLibAddStatus("Recognising text...");const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:audioId})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const text=(await r.json()).text||"";return $("lib-add-transcript").value=text,setLibAddStatus(successMessage),text}function openSavedLibraryVoice(voiceId){const openRow=()=>{var _a2;const row=Array.from(document.querySelectorAll(".vl-row")).find(r=>r.dataset.id===voiceId);return row?(row.scrollIntoView({behavior:"smooth",block:"center"}),row.classList.contains("edit-open")||(_a2=row.querySelector(".edit-audio-btn"))==null||_a2.click(),!0):!1};openRow()||setTimeout(openRow,150)}async function applyLibAddCrop(){if(!libAddState.id){toast("Load audio first","error");return}const start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||libAddState.duration),duration=end-start;if(end<=start+.1){toast("Crop range is too short","error"),setLibAddStatus("Crop range is too short");return}(duration<3||duration>20)&&toast("Best clone references are 3-20 seconds; cropping anyway.","error"),["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!0)}),setLibAddStatus(`Cropping ${start.toFixed(2)}s to ${end.toFixed(2)}s...`);try{const r=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start,end})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();loadLibAddAudio(d.id,d.duration,"Cropped audio"),toast("Crop applied","success");try{await transcribeLibAddCurrent("Cropped audio loaded and text recognised",d.id)}catch(e){toast("Crop applied, but recognition failed: "+e.message,"error"),setLibAddStatus("Cropped audio loaded; recognition failed")}}catch(e){toast("Crop failed: "+e.message,"error"),setLibAddStatus("Crop failed")}finally{["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!1)})}}$("lib-add-save-crop").addEventListener("click",applyLibAddCrop),$("lib-add-save-crop-bottom").addEventListener("click",applyLibAddCrop),["lib-add-start","lib-add-end"].forEach(id=>$(id).addEventListener("input",drawLibAddWave)),$("lib-add-play").addEventListener("click",()=>{if(!libAddState.id)return;libAddState.audio&&libAddState.audio.pause(),libAddState.audio=new Audio("/api/audio/"+libAddState.id);const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||libAddState.duration;libAddState.audio.currentTime=start,libAddState.audio.ontimeupdate=()=>{libAddState.audio.currentTime>=end&&libAddState.audio.pause()},libAddState.audio.play()}),$("lib-add-recognize").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}try{await transcribeLibAddCurrent("Text recognised")}catch(e){toast("Recognition failed: "+e.message,"error"),setLibAddStatus("Recognition failed")}}),$("lib-add-save").addEventListener("click",async()=>{var _a2,_b2;if(!libAddState.id){toast("Load audio first","error");return}const voiceId=$("lib-add-voice-id").value.trim()||`${$("lib-add-lang").value}_${$("lib-add-gender").value}_NewVoice`;if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}setLibAddStatus("Saving voice..."),$("lib-add-save").disabled=!0;try{const pr=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start:parseFloat($("lib-add-start").value)||0,end:parseFloat($("lib-add-end").value)||libAddState.duration})});if(!pr.ok){const e=await pr.json().catch(()=>({}));throw new Error(e.detail||pr.statusText)}const p=await pr.json();let transcript=$("lib-add-transcript").value.trim();if(!transcript&&(transcript=await transcribeLibAddCurrent("Final clip recognised; saving voice...",p.id),!transcript.trim()))throw new Error("Recognition returned no transcript; add text or try recognising again.");const sr=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:p.id,voice_id:voiceId,transcript})});if(!sr.ok){const e=await sr.json().catch(()=>({}));throw new Error(e.detail||sr.statusText)}if((_a2=libAddState.pendingSource)!=null&&_a2.imageUrl)try{await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,image_url:libAddState.pendingSource.imageUrl})})}catch{}libAddState.pendingSource=null,setLibAddSourcePreview({}),(_b2=$("lib-add-panel"))==null||_b2.classList.remove("open"),toast("Voice saved: "+voiceId,"success"),setLibAddStatus("Voice saved"),await loadVoiceLibrary(),openSavedLibraryVoice(voiceId)}catch(e){toast("Save failed: "+e.message,"error"),setLibAddStatus("Save failed")}finally{$("lib-add-save").disabled=!1}}),$("show-disabled-cb").addEventListener("change",()=>{$("disabled-info").style.display=$("show-disabled-cb").checked?"":"none",renderVoiceList()}),["library-filter-lang","library-filter-sex","library-filter-type","library-filter-rating"].forEach(id=>{var _a2;(_a2=$(id))==null||_a2.addEventListener("change",()=>{readLibraryFilters(),renderVoiceList()})}),(_C=$("library-filter-text"))==null||_C.addEventListener("input",debounce(()=>{readLibraryFilters(),renderVoiceList()},180)),(_D=$("library-filter-tag"))==null||_D.addEventListener("change",function(){window._voiceTagFilter=this.value||null,renderVoiceList()}),(_E=$("library-filter-group"))==null||_E.addEventListener("change",function(){window._voiceGroupFilter=this.value||null,renderVoiceList()}),(_F=$("library-clear-filters"))==null||_F.addEventListener("click",()=>{window._voiceTagFilter=null,window._voiceGroupFilter=null;const tagSel=$("library-filter-tag"),groupSel=$("library-filter-group");tagSel&&(tagSel.value=""),groupSel&&(groupSel.value=""),clearLibraryFilters()}),(_G=$("library-tts-backend-select"))==null||_G.addEventListener("change",()=>{var _a2;status("Library TTS engine: "+(((_a2=backendById(libraryTtsBackend()))==null?void 0:_a2.label)||libraryTtsBackend()))});function renderVoiceList(){var _a2,_b2,_c2,_d2,_e2,_f2;const showDisabled=$("show-disabled-cb").checked,list=$("voice-list");list.innerHTML="",renderVoiceGroupsBar(),populateLibraryFilters(),readLibraryFilters();const _gtBtn=$("voice-group-tag-btn");if(_gtBtn){_gtBtn.classList.toggle("active",!!window._voiceGroupByTag);const ic=_gtBtn.querySelector(".mdi");ic&&(ic.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline")}const _tvBtn=$("voice-table-view-btn");if(_tvBtn){_tvBtn.classList.toggle("active",!!window._voiceTableView),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",!!window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode))}const cat=window._voiceSidebarCat||"all",enabledOk=v=>showDisabled||v.enabled!==!1;let filtered=_voices.filter(v=>cat==="cloned"?enabledOk(v)&&v.has_ref&&v.origin!=="designed":cat==="designed"?enabledOk(v)&&(v.origin==="designed"||!v.has_ref):cat==="favorites"?enabledOk(v)&&(v.rating||0)>=4:cat==="hidden"?v.enabled===!1:enabledOk(v));window._voiceGroupFilter&&(filtered=filtered.filter(v=>(v.group||"").trim()===window._voiceGroupFilter)),window._voiceTagFilter&&(filtered=filtered.filter(v=>String(v.tag||"").split(",").map(t=>t.trim()).includes(window._voiceTagFilter)));const visibleCount=filtered.length;filtered=filtered.filter(libraryFilterMatch);const filterCount=filtered.length;if(_libraryIssueFilter&&(filtered=filtered.filter(v=>libraryIssueMatch(v))),$("voice-count").textContent=filtered.length+" / "+_voices.length+" voices",filtered=filtered.slice().sort((a,b)=>{const av=getSortValue(a,_sortField),bv=getSortValue(b,_sortField);return avbv?_sortDir:0}),updateLibraryInsights(),_libraryIssueFilter){const note=document.createElement("div");note.className="library-filter-note",note.innerHTML=`${escHtml(filtered.length)} / ${escHtml(filterCount)} ${escHtml(libraryIssueLabel())}: ${escHtml(describeIssueVoices())}`,note.querySelector("button").addEventListener("click",()=>setLibraryIssueFilter("")),list.appendChild(note)}if(!filtered.length){if(_voices.length===0){const emptyEl=document.createElement("div");emptyEl.className="voices-empty-state",emptyEl.innerHTML=` +
`,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-12&&i>11&&bar.classList.add("hot"),clipped&&i>14&&bar.classList.add("clip"))}),$("lib-add-db-readout").textContent=Number.isFinite(db)?db.toFixed(1)+" dB":"-\u221E dB"}function syncLibAddMicGain(){const gain=parseFloat($("lib-add-mic-gain").value)||0;$("lib-add-mic-gain-value").textContent=gain.toFixed(2)+"x",libAddState.gainNode&&(libAddState.gainNode.gain.value=gain)}function startLibAddMeter(){if(!libAddState.analyser)return;libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf);const data=new Float32Array(libAddState.analyser.fftSize),tick=()=>{libAddState.analyser.getFloatTimeDomainData(data);let sum=0,peak=0;for(const sample of data)sum+=sample*sample,peak=Math.max(peak,Math.abs(sample));const rms=Math.sqrt(sum/data.length),db=rms>0?20*Math.log10(rms):-1/0,level=Number.isFinite(db)?(db+60)/60:0;renderLibAddMeter(level,db,peak>.98),libAddState.meterRaf=requestAnimationFrame(tick)};tick()}async function ensureLibAddMicMonitor(){if(libAddState.recordStream)return;const AudioCtx=window.AudioContext||window.webkitAudioContext;if(libAddState.stream=await requestMicrophoneStream({raw:!0}),AudioCtx){libAddState.audioCtx=new AudioCtx,libAddState.sourceNode=libAddState.audioCtx.createMediaStreamSource(libAddState.stream),libAddState.gainNode=libAddState.audioCtx.createGain(),libAddState.analyser=libAddState.audioCtx.createAnalyser(),libAddState.analyser.fftSize=1024;const dest=libAddState.audioCtx.createMediaStreamDestination();syncLibAddMicGain(),libAddState.sourceNode.connect(libAddState.gainNode),libAddState.gainNode.connect(libAddState.analyser),libAddState.gainNode.connect(dest),libAddState.recordStream=dest.stream,startLibAddMeter()}else libAddState.recordStream=libAddState.stream;libAddState.monitoring=!0,$("lib-add-monitor-btn").disabled=!0,$("lib-add-monitor-stop").disabled=!1}function stopLibAddMic(){libAddState.meterRaf&&cancelAnimationFrame(libAddState.meterRaf),libAddState.meterRaf=null,[libAddState.sourceNode,libAddState.gainNode,libAddState.analyser].forEach(node=>{try{node&&node.disconnect()}catch{}}),libAddState.stream&&libAddState.stream.getTracks().forEach(t=>t.stop()),libAddState.recordStream&&libAddState.recordStream.getTracks().forEach(t=>t.stop()),libAddState.audioCtx&&libAddState.audioCtx.close().catch(()=>{}),libAddState.stream=null,libAddState.recordStream=null,libAddState.sourceNode=null,libAddState.gainNode=null,libAddState.analyser=null,libAddState.audioCtx=null,libAddState.monitoring=!1,$("lib-add-monitor-btn").disabled=!1,$("lib-add-monitor-stop").disabled=!0,renderLibAddMeter(0,-1/0,!1)}let libAddState={id:null,duration:0,audio:null,buffer:null,recorder:null,chunks:[],pendingSource:null,stream:null,recordStream:null,timer:null,secs:0,audioCtx:null,sourceNode:null,gainNode:null,analyser:null,meterRaf:null,monitoring:!1};window.libAddState=libAddState,$("add-new-voice-btn").addEventListener("click",()=>{$("lib-add-panel").classList.toggle("open")}),$("lib-add-sample-lang").addEventListener("change",()=>updateLibAddSampleLanguage($("lib-add-sample-lang").value)),$("lib-add-lang").addEventListener("change",()=>updateLibAddSampleLanguage($("lib-add-lang").value)),$("lib-add-sample-text").addEventListener("input",debounce(saveLibAddSampleText,500)),$("lib-add-use-sample").addEventListener("click",()=>{$("lib-add-transcript").value=$("lib-add-sample-text").value.trim(),setLibAddStatus("Sample sentence copied to transcript")}),$("lib-add-reset-sample").addEventListener("click",resetLibAddSampleText),$("lib-add-mic-help-btn").addEventListener("click",()=>{$("lib-add-mic-help").classList.toggle("open")}),$("lib-add-monitor-btn").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),setLibAddStatus("Mic level monitor active")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message)}}),$("lib-add-monitor-stop").addEventListener("click",()=>{stopLibAddMic(),setLibAddStatus("Mic level monitor stopped")}),$("lib-add-mic-gain").addEventListener("input",syncLibAddMicGain),renderLibAddMeter(),syncLibAddMicGain(),updateLibAddSampleLanguage("EN");function setLibAddStatus(msg){$("lib-add-status").textContent=msg,status(msg)}function suggestLibVoiceId(filename){if($("lib-add-voice-id").value.trim())return;const base=(typeof _umlautSafe=="function"?_umlautSafe(filename||"NewVoice"):String(filename||"NewVoice")).replace(/\.[^.]+$/,"").replace(/[^A-Za-z0-9_-]+/g,"_").replace(/^_+|_+$/g,"").slice(0,60)||"NewVoice";$("lib-add-voice-id").value=`${$("lib-add-lang").value||"EN"}_${$("lib-add-gender").value||"N"}_${base}`}function loadLibAddAudio(id,duration,label="Audio"){libAddState.id=id,libAddState.duration=Number(duration)||0,libAddState.buffer=null,$("lib-add-start").value="0.00",$("lib-add-end").value=libAddState.duration?Math.min(libAddState.duration,20).toFixed(2):"0.00",$("lib-add-audio").src="/api/audio/"+id,$("lib-add-audio").style.display="",$("lib-add-wave").style.display="",attachLibAddWaveSelection(),decodeTempAudio(id).then(buffer=>{libAddState.id===id&&(libAddState.buffer=buffer,drawLibAddWave())}).catch(()=>{}),setLibAddStatus(`${label} loaded${libAddState.duration?" ("+libAddState.duration.toFixed(1)+" s)":""}`)}async function decodeTempAudio(id){const resp=await fetch("/api/audio/"+encodeURIComponent(id));if(!resp.ok)throw new Error(resp.statusText||"Audio not found");const data=await resp.arrayBuffer();return new(window.AudioContext||window.webkitAudioContext)().decodeAudioData(data.slice(0))}function clampLibAddTime(value){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;return Math.max(0,Math.min(duration,Number(value)||0))}function setLibAddCropRange(start,end){var _a2;const duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0;let a=clampLibAddTime(start),b=clampLibAddTime(end);Math.abs(b-a)<.05&&(b=Math.min(duration,a+Math.min(1,duration||1))),b=3&&dur<=20?"ok":dur?"warn":"")}function drawLibAddWave(){libAddState.buffer&&(drawOptimizerWave($("lib-add-wave"),libAddState.buffer,parseFloat($("lib-add-start").value)||0,parseFloat($("lib-add-end").value)||libAddState.duration||libAddState.buffer.duration),updateLibAddCropHint())}function libAddWaveSelectionPixels(e){var _a2;const rect=$("lib-add-wave").getBoundingClientRect(),duration=libAddState.duration||((_a2=libAddState.buffer)==null?void 0:_a2.duration)||0,start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||duration),sx=duration&&rect.width?start/duration*rect.width:0,ex=duration&&rect.width?end/duration*rect.width:rect.width;return{x:Math.max(0,Math.min(rect.width,e.clientX-rect.left)),sx,ex,start,end,duration}}function libAddWaveDragMode(e){const{x,sx,ex}=libAddWaveSelectionPixels(e),hit=16;return Math.abs(x-sx)<=hit?"start":Math.abs(x-ex)<=hit?"end":"new"}function attachLibAddWaveSelection(){const canvas=$("lib-add-wave");if(!canvas||canvas.dataset.cropReady)return;canvas.dataset.cropReady="1";let drag=null;canvas.addEventListener("pointerdown",e=>{var _a2;if(!libAddState.buffer)return;e.preventDefault();const mode=libAddWaveDragMode(e),t=libAddWaveTimeFromEvent(e),currentStart=parseFloat($("lib-add-start").value)||0,currentEnd=parseFloat($("lib-add-end").value)||libAddState.duration||0;drag={mode,anchor:t,start:currentStart,end:currentEnd},(_a2=canvas.setPointerCapture)==null||_a2.call(canvas,e.pointerId),mode==="start"?setLibAddCropRange(t,currentEnd):setLibAddCropRange(mode==="end"?currentStart:t,t),setLibAddStatus(mode==="start"?"Dragging crop start handle":mode==="end"?"Dragging crop end handle":"Drag to choose a new crop range")}),canvas.addEventListener("pointermove",e=>{if(!libAddState.buffer)return;if(!drag){const mode=libAddWaveDragMode(e);canvas.style.cursor=mode==="start"||mode==="end"?"ew-resize":"crosshair";return}e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t)});const finish=e=>{if(!drag)return;e.preventDefault();const t=libAddWaveTimeFromEvent(e);drag.mode==="start"?setLibAddCropRange(t,drag.end):drag.mode==="end"?setLibAddCropRange(drag.start,t):setLibAddCropRange(drag.anchor,t),drag=null;const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||0;setLibAddStatus(`Crop range ${start.toFixed(2)}s to ${end.toFixed(2)}s (${Math.max(0,end-start).toFixed(1)}s) selected`)};canvas.addEventListener("pointerup",finish),canvas.addEventListener("pointerleave",()=>{drag||(canvas.style.cursor="crosshair")}),canvas.addEventListener("pointercancel",()=>{drag=null,canvas.style.cursor="crosshair"})}async function uploadLibAddFile(file){if(!file)return;const fd=new FormData;fd.append("file",file),setLibAddStatus("Uploading audio\u2026");try{const r=await fetch("/api/upload",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();suggestLibVoiceId(file.name),loadLibAddAudio(d.id,d.duration,file.name||"Audio"),toast("Audio loaded","success")}catch(e){toast("Load failed: "+e.message,"error"),setLibAddStatus("Load failed")}}const libAddDrop=$("lib-add-drop");libAddDrop.addEventListener("click",()=>$("lib-add-file").click()),libAddDrop.addEventListener("dragover",e=>{e.preventDefault(),libAddDrop.classList.add("drag-over")}),libAddDrop.addEventListener("dragleave",()=>libAddDrop.classList.remove("drag-over")),libAddDrop.addEventListener("drop",e=>{e.preventDefault(),libAddDrop.classList.remove("drag-over"),e.dataTransfer.files.length&&uploadLibAddFile(e.dataTransfer.files[0])}),$("lib-add-file").addEventListener("change",async()=>{$("lib-add-file").files.length&&await uploadLibAddFile($("lib-add-file").files[0]),$("lib-add-file").value=""}),$("lib-add-url-btn").addEventListener("click",()=>{const url=$("lib-add-url").value.trim();if(!url){toast("Enter a YouTube or audio URL","error");return}$("lib-add-url-btn").disabled=!0,setLibAddStatus("Starting download\u2026");const es=new EventSource("/api/download-yt?url="+encodeURIComponent(url));es.onmessage=e=>{const d=JSON.parse(e.data);d.error?(toast("Download failed: "+d.error,"error"),setLibAddStatus(d.error),$("lib-add-url-btn").disabled=!1,es.close()):d.done?(es.close(),$("lib-add-url-btn").disabled=!1,suggestLibVoiceId(url.split("/").pop()||"DownloadedVoice"),loadLibAddAudio(d.id,d.duration,"Downloaded audio"),toast("URL audio loaded","success")):setLibAddStatus(d.msg||"Downloading\u2026")},es.onerror=()=>{es.close(),$("lib-add-url-btn").disabled=!1,setLibAddStatus("Download connection closed")}}),$("lib-add-rec-start").addEventListener("click",async()=>{try{await ensureLibAddMicMonitor(),libAddState.chunks=[],libAddState.secs=0,$("lib-add-rec-time").textContent="0:00",$("lib-add-rec-start").disabled=!0,$("lib-add-rec-stop").disabled=!1,$("lib-add-monitor-stop").disabled=!0,libAddState.timer=setInterval(()=>{libAddState.secs++,$("lib-add-rec-time").textContent=Math.floor(libAddState.secs/60)+":"+String(libAddState.secs%60).padStart(2,"0")},1e3),libAddState.recorder=new MediaRecorder(libAddState.recordStream),libAddState.recorder.ondataavailable=e=>{e.data.size&&libAddState.chunks.push(e.data)},libAddState.recorder.onstop=async()=>{clearInterval(libAddState.timer),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0;const blob=new Blob(libAddState.chunks,{type:libAddState.recorder.mimeType||"audio/webm"}),ext=(libAddState.recorder.mimeType||"").includes("ogg")?".ogg":".webm";stopLibAddMic(),suggestLibVoiceId("recording"),await uploadLibAddFile(new File([blob],"recording"+ext,{type:blob.type}))},libAddState.recorder.start(100),setLibAddStatus("Recording\u2026")}catch(e){stopLibAddMic(),$("lib-add-mic-help").classList.add("open");const message=await microphoneErrorMessage(e);toast(message,"error"),setLibAddStatus(message),$("lib-add-rec-start").disabled=!1,$("lib-add-rec-stop").disabled=!0}}),$("lib-add-rec-stop").addEventListener("click",()=>{libAddState.recorder&&libAddState.recorder.state!=="inactive"&&libAddState.recorder.stop()}),$("lib-add-auto-trim").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}$("lib-add-auto-trim").disabled=!0;try{const r=await fetch("/api/auto-trim",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id})});let d;if(r.ok)d=await r.json();else if(r.status===404||r.status===405)d=await clientAutoTrimBounds(libAddState.id);else{const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}$("lib-add-start").value=Number(d.start).toFixed(2),$("lib-add-end").value=Number(d.end).toFixed(2),drawLibAddWave(),setLibAddStatus(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error"),setLibAddStatus("Auto trim failed")}finally{$("lib-add-auto-trim").disabled=!1}});async function transcribeLibAddCurrent(successMessage="Text recognised",audioId=libAddState.id){if(!audioId)throw new Error("Load audio first");setLibAddStatus("Recognising text...");const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:audioId})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const text=(await r.json()).text||"";return $("lib-add-transcript").value=text,setLibAddStatus(successMessage),text}function openSavedLibraryVoice(voiceId){const openRow=()=>{var _a2;const row=Array.from(document.querySelectorAll(".vl-row")).find(r=>r.dataset.id===voiceId);return row?(row.scrollIntoView({behavior:"smooth",block:"center"}),row.classList.contains("edit-open")||(_a2=row.querySelector(".edit-audio-btn"))==null||_a2.click(),!0):!1};openRow()||setTimeout(openRow,150)}async function applyLibAddCrop(){if(!libAddState.id){toast("Load audio first","error");return}const start=clampLibAddTime(parseFloat($("lib-add-start").value)||0),end=clampLibAddTime(parseFloat($("lib-add-end").value)||libAddState.duration),duration=end-start;if(end<=start+.1){toast("Crop range is too short","error"),setLibAddStatus("Crop range is too short");return}(duration<3||duration>20)&&toast("Best clone references are 3-20 seconds; cropping anyway.","error"),["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!0)}),setLibAddStatus(`Cropping ${start.toFixed(2)}s to ${end.toFixed(2)}s...`);try{const r=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start,end})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();loadLibAddAudio(d.id,d.duration,"Cropped audio"),toast("Crop applied","success");try{await transcribeLibAddCurrent("Cropped audio loaded and text recognised",d.id)}catch(e){toast("Crop applied, but recognition failed: "+e.message,"error"),setLibAddStatus("Cropped audio loaded; recognition failed")}}catch(e){toast("Crop failed: "+e.message,"error"),setLibAddStatus("Crop failed")}finally{["lib-add-save-crop","lib-add-save-crop-bottom"].forEach(id=>{$(id)&&($(id).disabled=!1)})}}$("lib-add-save-crop").addEventListener("click",applyLibAddCrop),$("lib-add-save-crop-bottom").addEventListener("click",applyLibAddCrop),["lib-add-start","lib-add-end"].forEach(id=>$(id).addEventListener("input",drawLibAddWave)),$("lib-add-play").addEventListener("click",()=>{if(!libAddState.id)return;libAddState.audio&&libAddState.audio.pause(),libAddState.audio=new Audio("/api/audio/"+libAddState.id);const start=parseFloat($("lib-add-start").value)||0,end=parseFloat($("lib-add-end").value)||libAddState.duration;libAddState.audio.currentTime=start,libAddState.audio.ontimeupdate=()=>{libAddState.audio.currentTime>=end&&libAddState.audio.pause()},libAddState.audio.play()}),$("lib-add-recognize").addEventListener("click",async()=>{if(!libAddState.id){toast("Load audio first","error");return}try{await transcribeLibAddCurrent("Text recognised")}catch(e){toast("Recognition failed: "+e.message,"error"),setLibAddStatus("Recognition failed")}}),$("lib-add-save").addEventListener("click",async()=>{var _a2,_b2;if(!libAddState.id){toast("Load audio first","error");return}const voiceId=$("lib-add-voice-id").value.trim()||`${$("lib-add-lang").value}_${$("lib-add-gender").value}_NewVoice`;if(!validateVoiceId(voiceId)){toast("Voice ID contains invalid characters","error");return}setLibAddStatus("Saving voice..."),$("lib-add-save").disabled=!0;try{const pr=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:libAddState.id,start:parseFloat($("lib-add-start").value)||0,end:parseFloat($("lib-add-end").value)||libAddState.duration})});if(!pr.ok){const e=await pr.json().catch(()=>({}));throw new Error(e.detail||pr.statusText)}const p=await pr.json();let transcript=$("lib-add-transcript").value.trim();if(!transcript&&(transcript=await transcribeLibAddCurrent("Final clip recognised; saving voice...",p.id),!transcript.trim()))throw new Error("Recognition returned no transcript; add text or try recognising again.");const sr=await fetch("/api/save",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:p.id,voice_id:voiceId,transcript})});if(!sr.ok){const e=await sr.json().catch(()=>({}));throw new Error(e.detail||sr.statusText)}if((_a2=libAddState.pendingSource)!=null&&_a2.imageUrl)try{await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,image_url:libAddState.pendingSource.imageUrl})})}catch{}libAddState.pendingSource=null,setLibAddSourcePreview({}),(_b2=$("lib-add-panel"))==null||_b2.classList.remove("open"),toast("Voice saved: "+voiceId,"success"),setLibAddStatus("Voice saved"),await loadVoiceLibrary(),openSavedLibraryVoice(voiceId)}catch(e){toast("Save failed: "+e.message,"error"),setLibAddStatus("Save failed")}finally{$("lib-add-save").disabled=!1}}),$("show-disabled-cb").addEventListener("change",()=>{$("disabled-info").style.display=$("show-disabled-cb").checked?"":"none",renderVoiceList()}),["library-filter-lang","library-filter-sex","library-filter-type","library-filter-rating"].forEach(id=>{var _a2;(_a2=$(id))==null||_a2.addEventListener("change",()=>{readLibraryFilters(),renderVoiceList()})}),(_D=$("library-filter-text"))==null||_D.addEventListener("input",debounce(()=>{readLibraryFilters(),renderVoiceList()},180)),(_E=$("library-filter-tag"))==null||_E.addEventListener("change",function(){window._voiceTagFilter=this.value||null,renderVoiceList()}),(_F=$("library-filter-group"))==null||_F.addEventListener("change",function(){window._voiceGroupFilter=this.value||null,renderVoiceList()}),(_G=$("library-clear-filters"))==null||_G.addEventListener("click",()=>{window._voiceTagFilter=null,window._voiceGroupFilter=null;const tagSel=$("library-filter-tag"),groupSel=$("library-filter-group");tagSel&&(tagSel.value=""),groupSel&&(groupSel.value=""),clearLibraryFilters()}),(_H=$("library-tts-backend-select"))==null||_H.addEventListener("change",()=>{var _a2;status("Library TTS engine: "+(((_a2=backendById(libraryTtsBackend()))==null?void 0:_a2.label)||libraryTtsBackend()))});function renderVoiceList(){var _a2,_b2,_c2,_d2,_e2,_f2;const showDisabled=$("show-disabled-cb").checked,list=$("voice-list");list.innerHTML="",renderVoiceGroupsBar(),populateLibraryFilters(),readLibraryFilters();const _gtBtn=$("voice-group-tag-btn");if(_gtBtn){_gtBtn.classList.toggle("active",!!window._voiceGroupByTag);const ic=_gtBtn.querySelector(".mdi");ic&&(ic.className="mdi mdi-folder"+(window._voiceGroupByTag?"-open":"")+"-outline")}const _tvBtn=$("voice-table-view-btn");if(_tvBtn){_tvBtn.classList.toggle("active",!!window._voiceTableView),(_a2=document.querySelector(".voices-workbench"))==null||_a2.classList.toggle("table-view",!!window._voiceTableView);const editBtn=document.getElementById("voice-table-edit-btn");editBtn&&(editBtn.style.display=window._voiceTableView?"inline-flex":"none",editBtn.classList.toggle("active",!!window._voiceTableEditMode))}const cat=window._voiceSidebarCat||"all",enabledOk=v=>showDisabled||v.enabled!==!1;let filtered=_voices.filter(v=>cat==="cloned"?enabledOk(v)&&v.has_ref&&v.origin!=="designed":cat==="designed"?enabledOk(v)&&(v.origin==="designed"||!v.has_ref):cat==="favorites"?enabledOk(v)&&(v.rating||0)>=4:cat==="hidden"?v.enabled===!1:enabledOk(v));window._voiceGroupFilter&&(filtered=filtered.filter(v=>(v.group||"").trim()===window._voiceGroupFilter)),window._voiceTagFilter&&(filtered=filtered.filter(v=>String(v.tag||"").split(",").map(t=>t.trim()).includes(window._voiceTagFilter)));const visibleCount=filtered.length;filtered=filtered.filter(libraryFilterMatch);const filterCount=filtered.length;if(_libraryIssueFilter&&(filtered=filtered.filter(v=>libraryIssueMatch(v))),$("voice-count").textContent=filtered.length+" / "+_voices.length+" voices",filtered=filtered.slice().sort((a,b)=>{const av=getSortValue(a,_sortField),bv=getSortValue(b,_sortField);return avbv?_sortDir:0}),updateLibraryInsights(),_libraryIssueFilter){const note=document.createElement("div");note.className="library-filter-note",note.innerHTML=`${escHtml(filtered.length)} / ${escHtml(filterCount)} ${escHtml(libraryIssueLabel())}: ${escHtml(describeIssueVoices())}`,note.querySelector("button").addEventListener("click",()=>setLibraryIssueFilter("")),list.appendChild(note)}if(!filtered.length){if(_voices.length===0){const emptyEl=document.createElement("div");emptyEl.className="voices-empty-state",emptyEl.innerHTML=`
@@ -709,7 +725,7 @@ This warms each voice so the engine caches its .pt and first playback is instant `;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=``);const inspectorAvatar=document.querySelector(".inspector-avatar");inspectorAvatar&&wrap.classList.contains("vr-selected")&&(inspectorAvatar.classList.add("insp-avatar-photo"),inspectorAvatar.style.background="",inspectorAvatar.innerHTML=``),v.has_picture=!0};photoCell.addEventListener("update-photo",updatePhotoImg),photoCell.addEventListener("click",()=>photoInput.click()),photoInput.addEventListener("change",async()=>{if(!photoInput.files.length)return;const fd=new FormData;fd.append("voice_id",v.id),fd.append("file",photoInput.files[0]);try{const r=await fetch("/api/voice/picture",{method:"POST",body:fd});if(!r.ok)throw new Error((await r.json()).detail);updatePhotoImg(),toast("Photo uploaded","success")}catch(e){toast("Photo upload failed: "+e.message,"error")}}),photoCell.addEventListener("dragenter",e=>{e.preventDefault(),photoCell.classList.add("drag-over")}),photoCell.addEventListener("dragover",e=>{e.preventDefault(),photoCell.classList.add("drag-over")}),photoCell.addEventListener("dragleave",()=>photoCell.classList.remove("drag-over")),photoCell.addEventListener("drop",async e=>{if(e.preventDefault(),photoCell.classList.remove("drag-over"),e.dataTransfer.files&&e.dataTransfer.files.length>0){photoInput.files=e.dataTransfer.files,photoInput.dispatchEvent(new Event("change"));return}let url=e.dataTransfer.getData("text/uri-list");if(!url){const html=e.dataTransfer.getData("text/html");if(html){const match=html.match(/src=["'](.*?)["']/);match&&(url=match[1])}}if(url||(url=e.dataTransfer.getData("text/plain")),url&&/^https?:\/\//i.test(url)){status("Downloading picture from URL...");try{const r=await fetch("/api/voice/picture-url",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,image_url:url})});if(!r.ok)throw new Error((await r.json()).detail);updatePhotoImg(),toast("Photo saved from URL","success"),status("Photo saved successfully")}catch(err){toast("Photo URL download failed: "+err.message,"error"),status("Photo URL download failed")}}});const normalizeBtn=wrap.querySelector(".normalize-voice-btn"),dbValue=wrap.querySelector(".vr-db-value"),dbCell=wrap.querySelector(".vr-db");normalizeBtn.addEventListener("click",async()=>{var _a3;const target=libraryTargetDb();if(confirm(`Normalize "${v.id}" to ${target} dBFS?`)){normalizeBtn.disabled=!0,status("Normalizing "+v.id+"\u2026");try{const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:target})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a3=d.duration)!=null?_a3:v.duration,v.file_type=d.file_type||v.file_type,v.path=d.path||v.path,v.needs_tts_restart=!0,markVoiceAudioChanged(v),dbValue.textContent=fmtDbfs(v),dbCell.title=v.loudness?`avg ${fmtDbfs(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",vrLengthEl&&(vrLengthEl.textContent=fmtDuration(v.duration)),toast("Normalized: "+v.id,"success"),status(`Normalized ${v.id} to ${target} dBFS. Restart TTS before rebenchmarking.`)}catch(e){toast("Normalize failed: "+e.message,"error"),status("Normalize failed")}finally{normalizeBtn.disabled=!1}}});const flagEmojiEl=wrap.querySelector(".flag-emoji"),flagCodeEl=wrap.querySelector(".flag-code"),flagPicker=wrap.querySelector(".flag-picker");wrap.querySelector(".vr-flag").addEventListener("click",e=>{e.stopPropagation(),document.querySelectorAll(".flag-picker.open").forEach(fp=>{fp!==flagPicker&&fp.classList.remove("open")}),flagPicker.classList.toggle("open")}),flagPicker.querySelectorAll(".flag-opt").forEach(opt=>{opt.addEventListener("click",async e=>{e.stopPropagation();const cc=opt.dataset.cc;flagPicker.classList.remove("open"),flagEmojiEl.textContent=cc2flag(cc),flagCodeEl.textContent=ccDisplay(cc),flagPicker.querySelectorAll(".flag-opt").forEach(o=>o.classList.toggle("active",o.dataset.cc===cc)),v.flag=cc,await saveMeta(v.id,{flag:cc})})});const gBadge=wrap.querySelector(".gender-badge");gBadge.addEventListener("click",async()=>{const cycle=["F","M","N"];v.gender=cycle[(cycle.indexOf(v.gender||"F")+1)%3],gBadge.innerHTML=`${genderMap[v.gender]||"?"}${genderLabel[v.gender]||"\u2014"}`,gBadge.className="gender-badge "+genderClass[v.gender],await saveMeta(v.id,{gender:v.gender})});const nameText=wrap.querySelector(".vr-name-text"),renameConf=wrap.querySelector(".rename-confirm"),nameInput=wrap.querySelector(".vr-name-input"),renameOk=wrap.querySelector(".rename-ok"),renameCancel=wrap.querySelector(".rename-cancel"),startRename=()=>{nameText.style.display="none",renameConf.classList.add("show"),nameInput.focus(),nameInput.select()};nameText.addEventListener("dblclick",startRename);const cancelRename=()=>{nameText.style.display="",renameConf.classList.remove("show"),nameInput.value=v.id};renameCancel.addEventListener("click",cancelRename);const doRename=async()=>{const newId=nameInput.value.trim();if(!newId||newId===v.id){cancelRename();return}if(!/^[A-Za-z0-9_\-\.]+$/.test(newId)){toast("Invalid characters in name","error");return}try{const r=await fetch("/api/voice/rename",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({old_id:v.id,new_id:newId})});if(!r.ok){const e=await r.json();throw new Error(e.detail)}const d=await r.json();v.id=newId,d.path&&(v.path=d.path),d.file_type&&(v.file_type=d.file_type),nameText.textContent=newId,nameText.title=newId,nameText.style.display="",renameConf.classList.remove("show"),wrap.dataset.id=newId,nameInput.value=newId,toast("Renamed to "+newId,"success")}catch(e){toast("Rename failed: "+e.message,"error")}};renameOk.addEventListener("click",doRename),nameInput.addEventListener("keydown",e=>{e.key==="Enter"&&doRename(),e.key==="Escape"&&cancelRename()}),wrap.querySelector(".vl-compact").addEventListener("click",function(e){e.target.closest("button")||selectVoice(wrap)});const editAudioBtn=wrap.querySelector(".edit-audio-btn"),optPanel=wrap.querySelector(".vr-optimizer"),vrTypeEl=wrap.querySelector(".vr-type"),optCanvas=wrap.querySelector(".opt-wave"),optStart=wrap.querySelector(".opt-start"),optEnd=wrap.querySelector(".opt-end"),optTranscript=wrap.querySelector(".opt-transcript"),optTargetDb=wrap.querySelector(".opt-target-db"),optStyleInstruct=wrap.querySelector(".opt-style-instruct"),optStyleBackend=wrap.querySelector(".opt-style-backend"),optStyleVoiceId=wrap.querySelector(".opt-style-voice-id"),optCompareBackend=wrap.querySelector(".opt-compare-backend"),optPlayReferenceBtn=wrap.querySelector(".opt-play-reference"),optSynthReferenceBtn=wrap.querySelector(".opt-synth-reference"),optCompareRefAudio=wrap.querySelector(".opt-compare-ref-audio"),optCompareSynthAudio=wrap.querySelector(".opt-compare-synth-audio"),optPreviewStyleBtn=wrap.querySelector(".opt-preview-style"),optSaveStyleBtn=wrap.querySelector(".opt-save-style"),optStyleAudio=wrap.querySelector(".opt-style-audio"),optStatus=wrap.querySelector(".opt-status"),optSaveTextBtn=wrap.querySelector(".opt-save-text"),optRestartTtsBtn=wrap.querySelector(".opt-restart-tts"),optRebenchmarkBtn=wrap.querySelector(".opt-rebenchmark"),optRestartNote=wrap.querySelector(".opt-restart-note");let optState={loaded:!1,id:null,duration:0,buffer:null,audio:null,compareSynthUrl:null};const setOptStatus=msg=>{optStatus.textContent=msg,status(msg)},setVoiceRestartState=(required,msg="")=>{v.needs_tts_restart=required,optPanel.classList.toggle("opt-restart-needed",required),optRestartNote.hidden=!required,optRestartNote.textContent=required?"Restart TTS before benchmarking; the backend may still have the old voice cached.":"",optRebenchmarkBtn.title=required?"Restart TTS first, otherwise the benchmark may use a cached voice":"Benchmark this voice",benchmarkOneBtn.title=required?"Restart TTS first, otherwise the benchmark may use a cached voice":"Benchmark this voice",msg&&setOptStatus(msg)},markTtsRestartRequired=msg=>setVoiceRestartState(!0,msg),refreshOptimizerFromVoice=async()=>{var _a3;markVoiceAudioChanged(v),optState.loaded=!1,optState.buffer=null,optState.id=null,await loadOptimizer(),vrLengthEl&&(vrLengthEl.textContent=fmtDuration(v.duration)),vrLengthEl&&(vrLengthEl.title=String((_a3=v.duration)!=null?_a3:"")),dbValue.textContent=fmtDbfs(v),dbCell.title=v.loudness?`avg ${fmtDbfs(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:""},saveOptimizerText=async()=>{const transcript=optTranscript.value.trim();setOptStatus("Saving reference text..."),v.transcript=transcript,refInput.value=transcript,refInput.title=transcript,refTranscribeBtn.style.display=transcript?"none":"",await saveMeta(v.id,{transcript}),markTtsRestartRequired("Reference text saved. Restart TTS before rebenchmarking."),toast("Reference text saved: "+v.id,"success")},redrawOpt=()=>{!optState.buffer||optCanvas.clientWidth<4||drawOptimizerWave(optCanvas,optState.buffer,parseFloat(optStart.value)||0,parseFloat(optEnd.value)||optState.duration)};new ResizeObserver(()=>redrawOpt()).observe(optCanvas);const syncCompareReferenceAudio=()=>{if(!optState.id||!optCompareRefAudio)return;const src="/api/audio/"+optState.id;optCompareRefAudio.src.endsWith(src)||(optCompareRefAudio.src=src,optCompareRefAudio.load());const stopAtEnd=()=>{const end=parseFloat(optEnd.value)||optState.duration;optCompareRefAudio.currentTime>=end&&optCompareRefAudio.pause()};optCompareRefAudio.ontimeupdate=stopAtEnd},optWaveTimeFromEvent=e=>{const rect=optCanvas.getBoundingClientRect(),x=Math.max(0,Math.min(rect.width,e.clientX-rect.left));return optState.duration?x/Math.max(1,rect.width)*optState.duration:0},setOptCropRange=(start,end)=>{start=Math.max(0,Math.min(optState.duration||0,Number(start)||0)),end=Math.max(0,Math.min(optState.duration||0,Number(end)||0)),end{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{let drag=null;optCanvas.addEventListener("pointerdown",e=>{var _a3;if(!optState.buffer||!optState.duration)return;e.preventDefault(),(_a3=optCanvas.setPointerCapture)==null||_a3.call(optCanvas,e.pointerId);const mode=optWaveDragMode(e),currentStart=parseFloat(optStart.value)||0,currentEnd=parseFloat(optEnd.value)||optState.duration;drag={mode,anchor:optWaveTimeFromEvent(e),start:currentStart,end:currentEnd,length:Math.max(.05,currentEnd-currentStart)},optCanvas.style.cursor=mode==="move"?"grabbing":"ew-resize",mode==="new"&&setOptCropRange(drag.anchor,drag.anchor)}),optCanvas.addEventListener("pointermove",e=>{if(!optState.buffer||!optState.duration)return;if(!drag){const mode=optWaveDragMode(e);optCanvas.style.cursor=mode==="move"?"grab":mode==="start"||mode==="end"?"ew-resize":"crosshair";return}e.preventDefault();const t=optWaveTimeFromEvent(e);if(drag.mode==="start")setOptCropRange(Math.min(t,drag.end-.05),drag.end);else if(drag.mode==="end")setOptCropRange(drag.start,Math.max(t,drag.start+.05));else if(drag.mode==="move"){let start=t-(drag.anchor-drag.start);start=Math.max(0,Math.min((optState.duration||0)-drag.length,start)),setOptCropRange(start,start+drag.length)}else setOptCropRange(drag.anchor,t)});const finish=e=>{var _a3;drag&&((_a3=optCanvas.releasePointerCapture)==null||_a3.call(optCanvas,e.pointerId),drag=null,optCanvas.style.cursor="crosshair")};optCanvas.addEventListener("pointerup",finish),optCanvas.addEventListener("pointercancel",finish),optCanvas.addEventListener("pointerleave",()=>{drag||(optCanvas.style.cursor="crosshair")})})();const loadOptimizer=async()=>{if(optState.loaded)return;setOptStatus("Loading voice optimizer\u2026");const d=await loadLibraryVoiceAudio(v);optState.id=d.id,optState.duration=d.duration,v.duration=d.duration,optState.buffer=await decodeVoiceAudio(v),optState.loaded=!0,optStart.value="0.00",optEnd.value=d.duration.toFixed(2),optEnd.max=d.duration.toFixed(2),optTranscript.value=d.transcript||v.transcript||"",redrawOpt(),syncCompareReferenceAudio(),setVoiceRestartState(!!v.needs_tts_restart),setOptStatus(v.needs_tts_restart?"Optimizer ready. Restart TTS before benchmarking this edit.":"Optimizer ready")};if(wrap._loadOptimizer=loadOptimizer,wrap._redrawOpt=redrawOpt,editAudioBtn.addEventListener("click",async()=>{editAudioBtn.disabled=!0;try{const opening=!wrap.classList.contains("edit-open");document.querySelectorAll(".vl-row.edit-open").forEach(r=>{r!==wrap&&r.classList.remove("edit-open")}),wrap.classList.toggle("edit-open",opening),opening&&(await loadOptimizer(),wrap.scrollIntoView({behavior:"smooth",block:"nearest"}))}catch(e){toast("Edit load failed: "+e.message,"error"),status("Edit load failed")}finally{editAudioBtn.disabled=!1}}),[optStart,optEnd].forEach(inp=>inp.addEventListener("input",()=>{redrawOpt(),syncCompareReferenceAudio()})),optStyleInstruct==null||optStyleInstruct.addEventListener("input",()=>{optStyleVoiceId.value.trim()||(optStyleVoiceId.value=suggestedStyleVoiceId(v.id,optStyleInstruct.value))}),optStyleBackend==null||optStyleBackend.addEventListener("change",()=>updateStyleBackendHelp(wrap)),optCompareBackend.addEventListener("change",()=>{var _a3;return setOptStatus(`Comparison backend: ${((_a3=optCompareBackend.options[optCompareBackend.selectedIndex])==null?void 0:_a3.textContent)||optCompareBackend.value}`)}),optCompareBackend.value===""&&(optCompareBackend.innerHTML=styleBackendOptions("voice_clone"),optCompareBackend.disabled=!availableTtsBackends().length),optStyleBackend&&updateStyleBackendHelp(wrap),wrap.querySelector(".opt-db-minus").addEventListener("click",()=>{optTargetDb.value=(Number(optTargetDb.value||-20)-1).toFixed(1)}),wrap.querySelector(".opt-db-plus").addEventListener("click",()=>{optTargetDb.value=(Number(optTargetDb.value||-20)+1).toFixed(1)}),wrap.querySelector(".opt-db-auto").addEventListener("click",()=>{optTargetDb.value="-20.0"}),wrap.querySelector(".opt-play").addEventListener("click",async()=>{try{await loadOptimizer(),optState.audio&&optState.audio.pause(),optState.audio=new Audio("/api/audio/"+optState.id),optState.audio.currentTime=parseFloat(optStart.value)||0;const end=parseFloat(optEnd.value)||optState.duration;optState.audio.ontimeupdate=()=>{optState.audio.currentTime>=end&&optState.audio.pause()},optState.audio.play()}catch(e){toast("Preview failed: "+e.message,"error")}}),optPlayReferenceBtn.addEventListener("click",async()=>{try{await loadOptimizer(),syncCompareReferenceAudio(),optCompareRefAudio.currentTime=parseFloat(optStart.value)||0,await optCompareRefAudio.play().catch(()=>{}),setOptStatus("Playing reference WAV selection for comparison.")}catch(e){toast("Reference playback failed: "+e.message,"error")}}),optSynthReferenceBtn.addEventListener("click",async()=>{const text=optTranscript.value.trim();if(!text){toast("Enter reference text first","error"),optTranscript.focus();return}if(v.needs_tts_restart){if(!confirm("This voice is still marked as needing a TTS restart. If you already restarted TTS manually, clear the restart flags and synthesize now?")){setOptStatus("Restart TTS before synthesizing this comparison, or clear the flag after a manual restart.");return}try{const d=await clearTtsRestartFlags();setVoiceRestartState(!1,`Restart flags cleared (${d.cleared_restart_flags||0}). Synthesizing comparison...`),toast("Restart flags cleared","success")}catch(e){toast("Could not clear restart flags: "+e.message,"error"),setOptStatus("Could not clear restart flags");return}}optSynthReferenceBtn.disabled=!0;try{await loadOptimizer(),setOptStatus("Synthesizing reference text for comparison...");const source=await createTtsAudioSource(v.id,text,optCompareBackend.value,"settings","");optState.compareSynthUrl&&URL.revokeObjectURL(optState.compareSynthUrl),optCompareSynthAudio.src=source.url,optState.compareSynthUrl=source.streaming?null:source.url,await optCompareSynthAudio.play().catch(()=>{}),setOptStatus(source.streaming?"Streaming synthesized comparison.":"Synthesized comparison ready.")}catch(e){toast("Synthesis comparison failed: "+e.message,"error"),setOptStatus("Synthesis comparison failed")}finally{optSynthReferenceBtn.disabled=!1}}),wrap.querySelector(".opt-auto-trim").addEventListener("click",async()=>{try{await loadOptimizer();const d=await clientAutoTrimBounds(optState.id);optStart.value=Number(d.start).toFixed(2),optEnd.value=Number(d.end).toFixed(2),redrawOpt(),setOptStatus(d.reason||"Auto trim ready")}catch(e){toast("Auto trim failed: "+e.message,"error")}}),wrap.querySelector(".opt-recognize").addEventListener("click",async()=>{try{await loadOptimizer();const r=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:optState.id})});if(!r.ok){const e=await r.json();throw new Error(e.detail||r.statusText)}const d=await r.json();optTranscript.value=d.text||"",setOptStatus("Reference text recognised. Review it, then Save text.")}catch(e){toast("Recognition failed: "+e.message,"error")}}),optSaveTextBtn.addEventListener("click",async()=>{optSaveTextBtn.disabled=!0;try{await saveOptimizerText()}catch(e){toast("Save text failed: "+e.message,"error"),setOptStatus("Save text failed")}finally{optSaveTextBtn.disabled=!1}}),wrap.querySelector(".opt-save-crop").addEventListener("click",async()=>{var _a3;try{await loadOptimizer();const cropStart=Math.max(0,parseFloat(optStart.value)||0),cropEnd=Math.min(optState.duration,parseFloat(optEnd.value)||optState.duration);if(cropStart<=.01&&cropEnd>=optState.duration-.05){setOptStatus("No crop range selected. Adjust Start or End first, then Save crop."),toast("No crop range selected","error");return}if(cropEnd<=cropStart+.1){setOptStatus("Crop range is too short."),toast("Crop range is too short","error");return}setOptStatus(`Saving crop ${cropStart.toFixed(2)}s -> ${cropEnd.toFixed(2)}s...`);const pr=await fetch("/api/process",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:optState.id,start:cropStart,end:cropEnd})});if(!pr.ok){const e=await pr.json();throw new Error(e.detail||pr.statusText)}const p=await pr.json(),rr=await fetch("/api/voice-replace",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:p.id,voice_id:v.id,path:v.path,transcript:optTranscript.value})});if(!rr.ok){const e=await rr.json().catch(()=>({}));throw new Error(e.detail||rr.statusText)}const saved=await rr.json();v.transcript=optTranscript.value,v.duration=(_a3=saved.duration)!=null?_a3:p.duration,saved.loudness&&(v.loudness=saved.loudness),saved.path&&(v.path=saved.path),saved.file_type&&(v.file_type=saved.file_type),markVoiceAudioChanged(v),refInput.value=v.transcript,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",vrTypeEl&&(vrTypeEl.textContent=voiceFileType(v).toUpperCase(),vrTypeEl.title=voiceFileType(v)),await refreshOptimizerFromVoice(),toast("Voice crop saved: "+v.id,"success"),markTtsRestartRequired(saved.backup?"Crop saved and loaded. Restart TTS before rebenchmarking; undo is available.":"Crop saved and loaded. Restart TTS before rebenchmarking.")}catch(e){toast("Save crop failed: "+e.message,"error"),setOptStatus("Save crop failed")}}),optStyleInstruct){const styleVariationInput=()=>{const style=optStyleInstruct.value.trim(),text=optTranscript.value.trim()||benchmarkSampleText(),newId=optStyleVoiceId.value.trim()||suggestedStyleVoiceId(v.id,style);return style?text?/^[A-Za-z0-9_\-.]+$/.test(newId)?{style,text,newId,backend:optStyleBackend.value}:(toast("Invalid characters in new voice ID","error"),optStyleVoiceId.focus(),null):(toast("Enter reference text first","error"),optTranscript.focus(),null):(toast("Enter a style instruction first","error"),optStyleInstruct.focus(),null)};optPreviewStyleBtn.addEventListener("click",async()=>{const input=styleVariationInput();if(input){optPreviewStyleBtn.disabled=!0;try{setOptStatus("Synthesizing style preview...");const blob=await fetchTtsPreviewBlob(v.id,input.text,"wav",input.style,input.backend);optStyleAudio.src&&URL.revokeObjectURL(optStyleAudio.src),optStyleAudio.src=URL.createObjectURL(blob),optStyleAudio.style.display="",await optStyleAudio.play().catch(()=>{}),setOptStatus("Style preview ready. If it sounds right, save it as a new voice.")}catch(e){toast("Style preview failed: "+e.message,"error"),setOptStatus("Style preview failed")}finally{optPreviewStyleBtn.disabled=!1}}}),optSaveStyleBtn.addEventListener("click",async()=>{const input=styleVariationInput();if(input){optSaveStyleBtn.disabled=!0;try{setOptStatus(`Synthesizing style variation ${input.newId}...`);const r=await fetch("/api/tts-style-variation",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({source_voice:v.id,voice_id:input.newId,text:input.text,instruct:input.style,backend:input.backend})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();toast("Style variation saved: "+d.voice_id,"success"),setOptStatus("Style variation saved. Restart TTS so the backend scans the new voice."),await loadVoiceLibrary(),renderIntegrationSnippets()}catch(e){toast("Style variation failed: "+e.message,"error"),setOptStatus("Style variation failed")}finally{optSaveStyleBtn.disabled=!1}}})}wrap.querySelector(".opt-undo").addEventListener("click",async()=>{var _a3;if(confirm(`Restore the original backup for "${v.id}"?`))try{setOptStatus("Restoring original\u2026");const r=await fetch("/api/voice/undo",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.duration=(_a3=d.duration)!=null?_a3:v.duration,v.loudness=d.loudness||v.loudness,v.path=d.path||v.path,v.file_type=d.file_type||v.file_type,markVoiceAudioChanged(v),vrTypeEl&&(vrTypeEl.textContent=voiceFileType(v).toUpperCase(),vrTypeEl.title=voiceFileType(v)),await refreshOptimizerFromVoice(),toast("Original restored: "+v.id,"success"),markTtsRestartRequired("Original restored. Restart TTS before rebenchmarking.")}catch(e){toast("Undo failed: "+e.message,"error"),setOptStatus("Undo failed")}}),wrap.querySelector(".opt-save-volume").addEventListener("click",async()=>{var _a3;try{setOptStatus("Saving volume\u2026");const r=await fetch("/api/voice/normalize",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:v.id,path:v.path,target_dbfs:Number(optTargetDb.value||-20)})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();v.loudness=d.loudness||v.loudness,v.duration=(_a3=d.duration)!=null?_a3:v.duration,d.path&&(v.path=d.path),d.file_type&&(v.file_type=d.file_type),markVoiceAudioChanged(v),dbValue.textContent=fmtDbfs(v),dbCell.title=v.loudness?`avg ${fmtDbfs(v)} dBFS${v.loudness.peak_dbfs!=null?", peak "+Number(v.loudness.peak_dbfs).toFixed(1)+" dBFS":""}`:"",await refreshOptimizerFromVoice(),toast("Volume saved: "+v.id,"success"),markTtsRestartRequired("Volume saved. Restart TTS before rebenchmarking this voice.")}catch(e){toast("Volume save failed: "+e.message,"error"),setOptStatus("Volume save failed")}}),optRestartTtsBtn.addEventListener("click",async()=>{optRestartTtsBtn.disabled=!0;try{setOptStatus("Restarting WAV backends (Voice Clone + Streaming)\u2026");const r=await fetch("/api/tts/restart",{method:"POST"});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_voices.forEach(voice=>{voice.needs_tts_restart=!1}),updateLibraryInsights();const names=(d.restarted||[]).join(", ")||"containers",errTxt=(d.errors||[]).length?` (errors: ${d.errors.join("; ")})`:"";setVoiceRestartState(!1,`Restarted: ${names}${errTxt}. Rebenchmark now uses the edited voice.`),toast(`TTS restarted: ${names}`,"success")}catch(e){toast("Restart TTS failed: "+e.message,"error"),setOptStatus("Restart TTS failed: "+e.message)}finally{optRestartTtsBtn.disabled=!1}});const refInput=wrap.querySelector(".vr-ref input"),refTranscribeBtn=wrap.querySelector(".ref-transcribe-btn");refInput.addEventListener("input",debounce(async()=>{v.transcript=refInput.value,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",await saveMeta(v.id,{transcript:v.transcript}),wrap.classList.contains("edit-open")?markTtsRestartRequired("Reference text saved. Restart TTS before rebenchmarking."):v.needs_tts_restart=!0},800)),refTranscribeBtn.addEventListener("click",async()=>{refTranscribeBtn.disabled=!0;try{const d=await loadLibraryVoiceAudio(v),tr=await fetch("/api/transcribe",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id:d.id})});if(!tr.ok){const e=await tr.json();throw new Error(e.detail)}const text=await tr.json();v.transcript=text.text||"",refInput.value=v.transcript,refInput.title=v.transcript,refTranscribeBtn.style.display=v.transcript?"none":"",await saveMeta(v.id,{transcript:v.transcript}),v.needs_tts_restart=!0,toast("Reference text recognised; restart TTS before benchmarking","success")}catch(e){toast("Recognition failed: "+e.message,"error")}finally{refTranscribeBtn.disabled=!1}});const noteInput=wrap.querySelector(".vr-note input");noteInput.addEventListener("input",debounce(async()=>{v.note=noteInput.value,await saveMeta(v.id,{note:v.note})},800));const sourceInput=wrap.querySelector(".vr-source input");sourceInput.addEventListener("input",debounce(async()=>{v.origin=sourceInput.value.trim(),await saveMeta(v.id,{origin:v.origin});const tblCell=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"] .vl-tbl-source`);tblCell&&(tblCell.textContent=_displaySource(v)||"-")},800));const starSpans=wrap.querySelectorAll(".star");starSpans.forEach(s=>{s.addEventListener("click",async()=>{const val=parseInt(s.dataset.val),newRating=val===v.rating?0:val;v.rating=newRating,starSpans.forEach((ss,i)=>ss.classList.toggle("on",i{const val=parseInt(s.dataset.val);starSpans.forEach((ss,i)=>ss.classList.toggle("on",i{starSpans.forEach((ss,i)=>ss.classList.toggle("on",i<(v.rating||0)))})});const benchmarkOneBtn=wrap.querySelector(".benchmark-one-btn"),benchmarkThisVoice=async triggerBtn=>{if(v.needs_tts_restart&&!confirm("This voice changed since the last TTS restart. Benchmarking now may use the cached old voice. Continue anyway?")){setOptStatus("Restart TTS first, then rebenchmark this voice.");return}triggerBtn.disabled=!0,status("Benchmarking "+v.id+"...");try{setBenchmarkProgress(0,1,`Benchmarking ${v.id}`);const d=await runVoiceBenchmark(v.id);mergeBenchmarkResults(d);const hit=(d.voices||[]).find(x=>x.voice_id===v.id);hit&&hit.benchmark&&(v.benchmark=hit.benchmark);const benchCell=wrap.querySelector(".vr-bench")||document.querySelector("#voices-inspector .vr-bench");if(benchCell){benchCell.className="vr-bench "+benchmarkClass(v),benchCell.title=benchmarkTitle(v);const benchValue=benchCell.querySelector(".vr-bench-value");benchValue&&(benchValue.textContent=fmtBenchmark(v))}const tblRow=document.querySelector(`.vl-row[data-id="${CSS.escape(v.id)}"]`);if(tblRow){const bc=benchmarkClass(v),btitle=benchmarkTitle(v),tblDur=tblRow.querySelector(".vl-tbl-dur"),tblFactor=tblRow.querySelector(".vl-tbl-factor"),tblTime=tblRow.querySelector(".vl-tbl-time"),tblWpm=tblRow.querySelector(".vl-tbl-wpm");if(tblDur&&(tblDur.textContent=fmtBenchmarkAudio(v),tblDur.title=`${fmtBenchmarkAudio(v)} \u2014 length of synthesised benchmark audio`),tblFactor&&(tblFactor.textContent=fmtFactor(v),tblFactor.className=`vl-tbl-factor ${bc}`,tblFactor.title=btitle),tblTime&&(tblTime.textContent=fmtElapsed(v),tblTime.className=`vl-tbl-time ${bc}`,tblTime.title=btitle),tblWpm){const wpm=voiceWpm(v);tblWpm.textContent=fmtWpm(v),tblWpm.title=wpm!=null?`${wpm} wpm \u2014 130\u2013180 wpm is natural for long listening`:""}}setBenchmarkProgress(1,1,`Finished ${v.id}`),toast("Benchmarked "+v.id,"success"),setVoiceRestartState(!1,"Benchmark saved for "+v.id)}catch(e){toast("Benchmark failed: "+e.message,"error"),setOptStatus("Benchmark failed")}finally{triggerBtn.disabled=!1}};benchmarkOneBtn.addEventListener("click",()=>benchmarkThisVoice(benchmarkOneBtn)),optRebenchmarkBtn.addEventListener("click",()=>benchmarkThisVoice(optRebenchmarkBtn));const originalPlayBtn=wrap.querySelector(".vr-play-original button"),synthPlayBtn=wrap.querySelector(".vr-play-synth button"),playIcon='',pauseIcon='',generatingIcon='';function setLibraryPlayButtonState(btn,state){btn.classList.toggle("is-generating",state==="generating"),btn.innerHTML=state==="playing"?pauseIcon:state==="generating"?generatingIcon:playIcon,btn.title=state==="generating"?"Generating synthesized sample...":state==="playing"?"Pause playback":btn.dataset.playKind==="synth"?"Generate and play synthesized sample":"Play original recording"}async function playLibraryVoice(kind,playBtn){var _a3,_b2,_c2;const bar=$("lib-audio-bar"),audio=$("lib-audio"),playKey=v.id+":"+kind;if(playBtn.dataset.playKind=kind,_activePlayVoiceId===playKey&&!audio.paused){audio.pause(),setLibraryPlayButtonState(playBtn,"idle");return}if(_activePlayVoiceId===playKey&&audio.paused&&audio.src){_activePlayButton=playBtn;try{await audio.play()}catch(e){toast("Play failed: "+e.message,"error")}return}_activePlayButton&&_activePlayButton!==playBtn&&setLibraryPlayButtonState(_activePlayButton,"idle"),_activePlayButton=playBtn,_activePlayVoiceId=playKey,_activePlayUrl&&(URL.revokeObjectURL(_activePlayUrl),_activePlayUrl=null),kind==="synth"&&setLibraryPlayButtonState(playBtn,"generating"),playBtn.disabled=!0;try{if(kind==="synth"){v.needs_tts_restart&&toast("This voice changed since backend refresh; synthesized playback may use a cached voice.","error");const synthMode=((_a3=document.querySelector("#vl-synth-mode-seg .vl-synth-seg-btn.active"))==null?void 0:_a3.dataset.mode)||"preview",text=synthMode==="transcript"&&((_b2=v.transcript)==null?void 0:_b2.trim())||benchmarkSampleText(),textLabel=synthMode==="transcript"?"reference transcript":"preview text",backend=isClone?libraryTtsBackend():"voice_design",source=await createTtsAudioSource(v.id,text,backend,"settings","");audio.src=source.url,source.streaming||(_activePlayUrl=source.url),$("lib-audio-label").textContent=v.id+" \xB7 synthesized "+textLabel+" \xB7 "+(((_c2=backendById(backend))==null?void 0:_c2.label)||backend)}else audio.src=voiceFileUrl(v),$("lib-audio-label").textContent=v.id+" \xB7 original recording";bar.style.display="",audio.onended=()=>{setLibraryPlayButtonState(playBtn,"idle"),_activePlayVoiceId=null},audio.onpause=()=>{_activePlayButton===playBtn&&setLibraryPlayButtonState(playBtn,"idle")},audio.onplay=()=>{setLibraryPlayButtonState(playBtn,"playing")},await audio.play()}catch(e){setLibraryPlayButtonState(playBtn,"idle"),toast("Play failed: "+e.message,"error")}finally{playBtn.disabled=!1}}originalPlayBtn.dataset.playKind="original",synthPlayBtn.dataset.playKind="synth",setLibraryPlayButtonState(originalPlayBtn,"idle"),setLibraryPlayButtonState(synthPlayBtn,"idle"),originalPlayBtn.addEventListener("click",()=>playLibraryVoice("original",originalPlayBtn)),synthPlayBtn.addEventListener("click",()=>playLibraryVoice("synth",synthPlayBtn));const toggleCb=wrap.querySelector(".toggle input");toggleCb.addEventListener("change",async()=>{const nextEnabled=toggleCb.checked,previousEnabled=v.enabled!==!1;toggleCb.disabled=!0;try{const saved=await saveMeta(v.id,{enabled:nextEnabled});v.enabled=nextEnabled,saved&&saved.path&&(v.path=saved.path),wrap.classList.toggle("vr-disabled",!v.enabled),toast(nextEnabled?"Moved to active_voices":"Moved to hidden_voices","success"),!v.enabled&&!$("show-disabled-cb").checked&&(wrap.style.transition="opacity .4s",wrap.style.opacity="0",setTimeout(()=>wrap.remove(),400))}catch(e){toggleCb.checked=previousEnabled,v.enabled=previousEnabled,wrap.classList.toggle("vr-disabled",!v.enabled),toast("Move failed: "+e.message,"error")}finally{toggleCb.disabled=!1}});const deleteBtn=wrap.querySelector(".delete-btn"),deleteConfirm=wrap.querySelector(".delete-confirm"),deleteCancelBtn=wrap.querySelector(".delete-confirm-cancel"),deleteGoBtn=wrap.querySelector(".delete-confirm-go"),closeDeleteConfirm=()=>wrap.classList.remove("delete-pending");return deleteBtn.addEventListener("click",e=>{e.stopPropagation(),document.querySelectorAll(".vl-row.delete-pending").forEach(row=>{row!==wrap&&row.classList.remove("delete-pending")}),wrap.classList.add("delete-pending"),deleteGoBtn.focus()}),deleteCancelBtn.addEventListener("click",e=>{e.stopPropagation(),closeDeleteConfirm()}),deleteConfirm.addEventListener("click",e=>e.stopPropagation()),deleteGoBtn.addEventListener("click",async e=>{e.stopPropagation(),deleteGoBtn.disabled=!0,deleteCancelBtn.disabled=!0;try{const r=await fetch(`/api/voice/${encodeURIComponent(v.id)}`,{method:"DELETE"});if(!r.ok){const e2=await r.json();throw new Error(e2.detail)}_voices=_voices.filter(x=>x.id!==v.id),wrap.style.transition="opacity .3s",wrap.style.opacity="0",setTimeout(()=>{wrap.remove(),$("voice-count").textContent=_voices.filter(x=>$("show-disabled-cb").checked||x.enabled!==!1).length+" / "+_voices.length+" voices"},300),toast(`Deleted: ${v.id}`,"success")}catch(e2){toast("Delete failed: "+e2.message,"error"),deleteGoBtn.disabled=!1,deleteCancelBtn.disabled=!1,closeDeleteConfirm()}}),wrap}function renderVoiceGroupsBar(){var _a2;const bar=$("voice-groups-bar");if(!bar)return;const groups={};(_voices||[]).forEach(v=>{const g=(v.group||"").trim();g&&(groups[g]=(groups[g]||0)+1)});const names=Object.keys(groups).sort();if(!names.length){bar.hidden=!0,bar.innerHTML="";return}bar.hidden=!1;const active=window._voiceGroupFilter||"";bar.innerHTML=' Groups'+names.map(g=>` ${escHtml(g)} ${groups[g]} - `).join("")+(active?'':""),bar.querySelectorAll(".vg-chip").forEach(chip=>{chip.addEventListener("click",e=>{if(e.target.closest(".vg-del"))return;const g=chip.dataset.group;window._voiceGroupFilter=window._voiceGroupFilter===g?"":g;const groupSel=$("library-filter-group");groupSel&&(groupSel.value=window._voiceGroupFilter||""),renderVoiceList()})}),bar.querySelectorAll(".vg-del").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),deleteVoiceGroup(btn.dataset.group)})}),(_a2=bar.querySelector(".vg-clear"))==null||_a2.addEventListener("click",()=>{window._voiceGroupFilter="";const groupSel=$("library-filter-group");groupSel&&(groupSel.value=""),renderVoiceList()})}async function deleteVoiceGroup(group){const count=(_voices||[]).filter(v=>(v.group||"").trim()===group).length;if(confirm(`Delete all ${count} voice${count!==1?"s":""} in group "${group}"? This cannot be undone.`))try{const r=await fetch("/api/voices/delete-group",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({group})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();window._voiceGroupFilter===group&&(window._voiceGroupFilter=""),toast(`Deleted ${d.count} voice${d.count!==1?"s":""} from "${group}"`,"success"),await loadVoiceLibrary()}catch(e){toast("Delete failed: "+e.message,"error")}}async function saveMeta(voiceId,patch){const r=await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,...patch})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return r.json()}document.addEventListener("click",()=>{document.querySelectorAll(".flag-picker.open").forEach(fp=>fp.classList.remove("open")),document.querySelectorAll(".vl-row.delete-pending").forEach(row=>row.classList.remove("delete-pending"))},{passive:!0});const _bulkSelected=new Set;function _bulkUpdate(){const bar=$("vl-bulk-bar"),count=$("vl-bulk-count"),n=_bulkSelected.size;bar&&(bar.hidden=n===0),count&&(count.textContent=`${n} selected`),document.querySelectorAll(".vl-bulk-cb").forEach(cb=>{cb.checked=_bulkSelected.has(cb.dataset.id)});const headerCb=document.getElementById("vl-select-all-header");if(headerCb){const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id),allSelected=rows.length>0&&rows.every(r=>_bulkSelected.has(r.dataset.id));headerCb.checked=allSelected}}function _bulkToggle(id,checked){checked?_bulkSelected.add(id):_bulkSelected.delete(id),_bulkUpdate()}const _origRenderVoiceList=renderVoiceList;renderVoiceList=function(){_origRenderVoiceList.apply(this,arguments),_bulkInjectCheckboxes()};let _bulkLastClickedId=null;function _bulkInjectCheckboxes(){document.querySelectorAll("#voice-list .vl-row").forEach(row=>{if(row.querySelector(".vl-bulk-cb"))return;const id=row.dataset.id,cb=document.createElement("input");cb.type="checkbox",cb.className="vl-bulk-cb",cb.dataset.id=id,cb.checked=_bulkSelected.has(id),cb.title="Select for bulk edit (shift-click to select a range)",cb.addEventListener("click",e=>{if(e.stopPropagation(),e.shiftKey&&_bulkLastClickedId){const ids=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id).map(r=>r.dataset.id),from=ids.indexOf(_bulkLastClickedId),to=ids.indexOf(id);if(from!==-1&&to!==-1){const[lo,hi]=from{document.querySelectorAll("#voice-list .vl-row").forEach(row=>{row.dataset.id&&_bulkSelected.add(row.dataset.id)}),_bulkUpdate()}),(_I=$("vl-select-all-header"))==null||_I.addEventListener("change",e=>{const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id);e.target.checked?rows.forEach(r=>_bulkSelected.add(r.dataset.id)):rows.forEach(r=>_bulkSelected.delete(r.dataset.id)),_bulkUpdate()}),(_J=$("vl-bulk-deselect"))==null||_J.addEventListener("click",()=>{_bulkSelected.clear(),_bulkUpdate()}),(_K=$("vl-bulk-hide"))==null||_K.addEventListener("click",async()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],r=await _bulkSetEnabled(ids,!1);toast(`Hidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),(_L=$("vl-bulk-unhide"))==null||_L.addEventListener("click",async()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],r=await _bulkSetEnabled(ids,!0);toast(`Unhidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),(_M=$("vl-bulk-tag"))==null||_M.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-tag-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-tag-inp",inp.placeholder="tag1, tag2\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML=' Apply',applyBtn.addEventListener("click",async()=>{const newTags=inp.value.split(",").map(t=>t.trim()).filter(Boolean);let done=0;for(const id of ids){const v=(window._voices||[]).find(vv=>vv.id===id),merged=String((v==null?void 0:v.tag)||"").split(",").map(t=>t.trim()).filter(Boolean).slice();for(const t of newTags)merged.some(e=>e.toLowerCase()===t.toLowerCase())||merged.push(t);await saveMeta(id,{tag:merged.join(", ")}).catch(()=>{}),done++}toast(`Tag added on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_N=$("vl-bulk-source"))==null||_N.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-source-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-source-inp",inp.placeholder="e.g. fish-audio, cloned\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML=' Apply',applyBtn.addEventListener("click",async()=>{const val=inp.value.trim();let done=0;for(const id of ids)await saveMeta(id,{origin:val}).catch(()=>{}),done++;toast(`Source set on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_O=$("vl-bulk-rating"))==null||_O.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let sel=bar.querySelector(".vl-bulk-rating-sel");sel||(sel=document.createElement("select"),sel.className="vl-bulk-rating-sel",sel.innerHTML=''+[1,2,3,4,5].map(n=>``).join(""),sel.addEventListener("change",async()=>{const rating=Number(sel.value);if(!rating)return;let done=0;for(const id of ids)await saveMeta(id,{rating}).catch(()=>{}),done++;toast(`Rated ${done} voice${done!==1?"s":""}`,"success"),sel.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(sel),sel.focus())}),(_P=$("vl-bulk-delete"))==null||_P.addEventListener("click",()=>{_bulkSelected.size&&_showBulkDeleteConfirm([..._bulkSelected])});function _showBulkDeleteConfirm(ids){var _a2;(_a2=document.querySelector(".vl-bdc-overlay"))==null||_a2.remove();const plural=ids.length!==1?"s":"",names=ids.slice(0,12).map(id=>`${escHtml(id)}`).join(""),more=ids.length>12?`+${ids.length-12} more`:"",ov=document.createElement("div");ov.className="vl-bdc-overlay",ov.innerHTML=` + `).join("")+(active?'':""),bar.querySelectorAll(".vg-chip").forEach(chip=>{chip.addEventListener("click",e=>{if(e.target.closest(".vg-del"))return;const g=chip.dataset.group;window._voiceGroupFilter=window._voiceGroupFilter===g?"":g;const groupSel=$("library-filter-group");groupSel&&(groupSel.value=window._voiceGroupFilter||""),renderVoiceList()})}),bar.querySelectorAll(".vg-del").forEach(btn=>{btn.addEventListener("click",e=>{e.stopPropagation(),deleteVoiceGroup(btn.dataset.group)})}),(_a2=bar.querySelector(".vg-clear"))==null||_a2.addEventListener("click",()=>{window._voiceGroupFilter="";const groupSel=$("library-filter-group");groupSel&&(groupSel.value=""),renderVoiceList()})}async function deleteVoiceGroup(group){const count=(_voices||[]).filter(v=>(v.group||"").trim()===group).length;if(confirm(`Delete all ${count} voice${count!==1?"s":""} in group "${group}"? This cannot be undone.`))try{const r=await fetch("/api/voices/delete-group",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({group})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();window._voiceGroupFilter===group&&(window._voiceGroupFilter=""),toast(`Deleted ${d.count} voice${d.count!==1?"s":""} from "${group}"`,"success"),await loadVoiceLibrary()}catch(e){toast("Delete failed: "+e.message,"error")}}async function saveMeta(voiceId,patch){const r=await fetch("/api/voice/meta",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({voice_id:voiceId,...patch})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return r.json()}document.addEventListener("click",()=>{document.querySelectorAll(".flag-picker.open").forEach(fp=>fp.classList.remove("open")),document.querySelectorAll(".vl-row.delete-pending").forEach(row=>row.classList.remove("delete-pending"))},{passive:!0});const _bulkSelected=new Set;function _bulkUpdate(){const bar=$("vl-bulk-bar"),count=$("vl-bulk-count"),n=_bulkSelected.size;bar&&(bar.hidden=n===0),count&&(count.textContent=`${n} selected`),document.querySelectorAll(".vl-bulk-cb").forEach(cb=>{cb.checked=_bulkSelected.has(cb.dataset.id)});const headerCb=document.getElementById("vl-select-all-header");if(headerCb){const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id),allSelected=rows.length>0&&rows.every(r=>_bulkSelected.has(r.dataset.id));headerCb.checked=allSelected}}function _bulkToggle(id,checked){checked?_bulkSelected.add(id):_bulkSelected.delete(id),_bulkUpdate()}const _origRenderVoiceList=renderVoiceList;renderVoiceList=function(){_origRenderVoiceList.apply(this,arguments),_bulkInjectCheckboxes()};let _bulkLastClickedId=null;function _bulkInjectCheckboxes(){document.querySelectorAll("#voice-list .vl-row").forEach(row=>{if(row.querySelector(".vl-bulk-cb"))return;const id=row.dataset.id,cb=document.createElement("input");cb.type="checkbox",cb.className="vl-bulk-cb",cb.dataset.id=id,cb.checked=_bulkSelected.has(id),cb.title="Select for bulk edit (shift-click to select a range)",cb.addEventListener("click",e=>{if(e.stopPropagation(),e.shiftKey&&_bulkLastClickedId){const ids=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id).map(r=>r.dataset.id),from=ids.indexOf(_bulkLastClickedId),to=ids.indexOf(id);if(from!==-1&&to!==-1){const[lo,hi]=from{document.querySelectorAll("#voice-list .vl-row").forEach(row=>{row.dataset.id&&_bulkSelected.add(row.dataset.id)}),_bulkUpdate()}),(_J=$("vl-select-all-header"))==null||_J.addEventListener("change",e=>{const rows=[...document.querySelectorAll("#voice-list .vl-row")].filter(r=>r.dataset.id);e.target.checked?rows.forEach(r=>_bulkSelected.add(r.dataset.id)):rows.forEach(r=>_bulkSelected.delete(r.dataset.id)),_bulkUpdate()}),(_K=$("vl-bulk-deselect"))==null||_K.addEventListener("click",()=>{_bulkSelected.clear(),_bulkUpdate()}),(_L=$("vl-bulk-hide"))==null||_L.addEventListener("click",async()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],r=await _bulkSetEnabled(ids,!1);toast(`Hidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),(_M=$("vl-bulk-unhide"))==null||_M.addEventListener("click",async()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],r=await _bulkSetEnabled(ids,!0);toast(`Unhidden ${r} voice${r!==1?"s":""}`,"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),(_N=$("vl-bulk-tag"))==null||_N.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-tag-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-tag-inp",inp.placeholder="tag1, tag2\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML=' Apply',applyBtn.addEventListener("click",async()=>{const newTags=inp.value.split(",").map(t=>t.trim()).filter(Boolean);let done=0;for(const id of ids){const v=(window._voices||[]).find(vv=>vv.id===id),merged=String((v==null?void 0:v.tag)||"").split(",").map(t=>t.trim()).filter(Boolean).slice();for(const t of newTags)merged.some(e=>e.toLowerCase()===t.toLowerCase())||merged.push(t);await saveMeta(id,{tag:merged.join(", ")}).catch(()=>{}),done++}toast(`Tag added on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_O=$("vl-bulk-source"))==null||_O.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let inp=bar.querySelector(".vl-bulk-source-inp");if(!inp){inp=document.createElement("input"),inp.type="text",inp.className="vl-bulk-source-inp",inp.placeholder="e.g. fish-audio, cloned\u2026",inp.autocomplete="off";const applyBtn=document.createElement("button");applyBtn.className="vl-bulk-btn",applyBtn.innerHTML=' Apply',applyBtn.addEventListener("click",async()=>{const val=inp.value.trim();let done=0;for(const id of ids)await saveMeta(id,{origin:val}).catch(()=>{}),done++;toast(`Source set on ${done} voice${done!==1?"s":""}`,"success"),inp.remove(),applyBtn.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(inp),bar.appendChild(applyBtn)}inp.focus()}),(_P=$("vl-bulk-rating"))==null||_P.addEventListener("click",()=>{if(!_bulkSelected.size)return;const ids=[..._bulkSelected],bar=$("vl-bulk-bar");let sel=bar.querySelector(".vl-bulk-rating-sel");sel||(sel=document.createElement("select"),sel.className="vl-bulk-rating-sel",sel.innerHTML=''+[1,2,3,4,5].map(n=>``).join(""),sel.addEventListener("change",async()=>{const rating=Number(sel.value);if(!rating)return;let done=0;for(const id of ids)await saveMeta(id,{rating}).catch(()=>{}),done++;toast(`Rated ${done} voice${done!==1?"s":""}`,"success"),sel.remove(),_bulkSelected.clear(),await loadVoiceLibrary()}),bar.appendChild(sel),sel.focus())}),(_Q=$("vl-bulk-delete"))==null||_Q.addEventListener("click",()=>{_bulkSelected.size&&_showBulkDeleteConfirm([..._bulkSelected])});function _showBulkDeleteConfirm(ids){var _a2;(_a2=document.querySelector(".vl-bdc-overlay"))==null||_a2.remove();const plural=ids.length!==1?"s":"",names=ids.slice(0,12).map(id=>`${escHtml(id)}`).join(""),more=ids.length>12?`+${ids.length-12} more`:"",ov=document.createElement("div");ov.className="vl-bdc-overlay",ov.innerHTML=`
Delete ${ids.length} voice${plural}?

This permanently removes the selected voice${plural} from your library. This cannot be undone.

@@ -718,7 +734,7 @@ This warms each voice so the engine caches its .pt and first playback is instant
-
`;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=` Deleting ${n}/${ids.length}\u2026`}),close(),toast(`Deleted ${done} voice${done!==1?"s":""}${errors?` (${errors} errors)`:""}`,errors?"error":"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),document.body.appendChild(ov),ov.querySelector(".vl-bdc-cancel").focus()}async function _bulkSetEnabled(ids,enabled){let done=0;for(const id of ids)await saveMeta(id,{enabled}).catch(()=>{}),done++;return done}function backendVoiceId(value){return typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||JSON.stringify(value)}function shouldFilterBackendVoices(backend){return["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend||"")}async function activeLibraryVoiceIds(){return _voices.length||await loadVoiceLibrary(),new Set((_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id))}function cleanReferenceText(text){return String(text||"").trim()}function selectedPreviewLibraryVoice(){var _a2;const id=((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"";return id?(_voices||[]).find(v=>v.id===id):null}function previewVoiceWarnings(v){var _a2;const warnings=[],backend=backendById(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"");backend&&backend.id&&!["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend.id)&&warnings.push(backend.id==="nvidia_magpie"?"NVIDIA Magpie uses fixed speaker voices, not saved WAV clone identity.":"This backend may follow style/model voice more than the saved WAV identity."),backend&&backend.id==="nvidia_zeroshot"&&v.duration&&(Number(v.duration)<3||Number(v.duration)>10)&&warnings.push("NVIDIA Zeroshot works best with a clear 3-10 second prompt."),backend&&backend.id==="nvidia_flow"&&!v.transcript&&warnings.push("NVIDIA Flow requires the exact saved reference transcript for this voice."),v.transcript||warnings.push("No reference transcript is saved; cloned identity is harder to judge."),v.duration&&(Number(v.duration)<3||Number(v.duration)>20)&&warnings.push("Reference clip length is outside the 3-20 second sweet spot."),v.needs_tts_restart&&warnings.push("This voice changed since the last backend refresh; restart or clear restart flags before judging it.");const healthWarnings=v.health&&Array.isArray(v.health.warnings)?v.health.warnings:[];return warnings.push(...healthWarnings.slice(0,3)),warnings}function updatePreviewVoiceMatchPanel(){const panel=$("preview-match-panel");if(!panel)return;const v=selectedPreviewLibraryVoice();if(!v){panel.hidden=!0;return}panel.hidden=!1;const lang=v.language||v.lang||(v.id||"").split("_")[0]||"-",gender=v.gender||(v.id||"").split("_")[1]||"-",db=fmtDbfs(v),dur=v.duration?fmtDuration(v.duration):"-";$("preview-match-title").textContent=v.id,$("preview-match-detail").textContent=`${lang} \xB7 ${gender} \xB7 ${dur} \xB7 ${db} dBFS`;const warnings=previewVoiceWarnings(v);$("preview-match-warning").textContent=warnings.length?warnings.join(" "):"For a fair voice match check, play the WAV and synthesize the exact saved reference text.";const transcript=cleanReferenceText(v.transcript||"");$("preview-match-transcript").textContent=transcript||"No reference text saved for this voice.",$("preview-ref-use-text").disabled=!transcript,$("preview-ref-synth").disabled=!transcript;const audio=$("preview-ref-audio"),expected=voiceFileUrl(v);audio.dataset.src!==expected&&(audio.pause(),audio.src=expected,audio.dataset.src=expected);const actionsEl=panel.querySelector(".preview-match-actions");let personaBtn=panel.querySelector(".preview-persona-btn");v.persona?personaBtn||(personaBtn=document.createElement("button"),personaBtn.className="btn-secondary preview-persona-btn",personaBtn.type="button",personaBtn.textContent="Rewrite with persona",actionsEl==null||actionsEl.appendChild(personaBtn),personaBtn.addEventListener("click",async()=>{const text=$("preview-text-area").value.trim();if(!text){toast("Enter text to rewrite","error");return}const lv=selectedPreviewLibraryVoice();if(!(lv!=null&&lv.persona)){toast("This voice has no persona","error");return}personaBtn.disabled=!0,personaBtn.textContent="Rewriting\u2026";try{const llmUrl=localStorage.getItem("refine-llm-url")||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",r=await fetch("/api/rewrite-with-persona",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,persona:lv.persona,llm_url:llmUrl,model:(_appSettings==null?void 0:_appSettings.llm_model)||"",mode:"rewrite"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("preview-text-area").value=d.text,toast("Text rewritten in persona style","success")}catch(e){toast("Persona rewrite failed: "+e.message,"error")}finally{personaBtn.disabled=!1,personaBtn.textContent="Rewrite with persona"}})):personaBtn==null||personaBtn.remove()}async function synthesizeSelectedReferenceText(){const v=selectedPreviewLibraryVoice();if(!v){toast("Select a library voice first","error");return}let text=cleanReferenceText(v.transcript||"");if(!text){toast("This voice has no reference text","error");return}if(v.needs_tts_restart){if(!confirm("This voice is marked as needing a TTS restart. If you already restarted the backend, clear the flag and synthesize anyway?"))return;await clearTtsRestartFlags(),v.needs_tts_restart=!1,updatePreviewVoiceMatchPanel()}const backend=$("tts-backend-select").value;if(!backend){toast("No available TTS backend","error");return}const btn=$("preview-ref-synth");btn.disabled=!0;try{$("preview-text-area").value=text;const source=await createTtsAudioSource(v.id,text,backend,$("preview-playback-mode").value,$("preview-style-instruction").value.trim());previewBlob=source.blob;const audio=$("preview-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,toast(source.streaming?"Reference text streaming":"Reference text synthesized","success")}catch(e){toast("Reference synthesis failed: "+e.message,"error")}finally{btn.disabled=!1}}$("fetch-tts-voices-btn").addEventListener("click",async()=>{var _a2;$("fetch-tts-voices-btn").disabled=!0;try{const backend=(_a2=$("tts-backend-select"))==null?void 0:_a2.value;if(!backend)throw new Error("No available TTS backend");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=$("tts-voice-select"),prev=sel.value,ids=voices.map(backendVoiceId);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='',ids.forEach(id=>{const o=document.createElement("option");o.value=o.textContent=id,sel.appendChild(o)}),prev&&ids.includes(prev)&&(sel.value=prev)),updatePreviewVoiceMatchPanel();const suffix=shouldFilterBackendVoices(backend)?" active voices":" voices";toast("Fetched "+voices.length+suffix,"success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("fetch-tts-voices-btn").disabled=!1}}),$("tts-backend-select").addEventListener("change",()=>{const sel=$("tts-voice-select");sel.innerHTML='',updateBackendHelp(),updatePreviewVoiceMatchPanel(),previewBlob=null,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0}),$("tts-voice-select").addEventListener("change",updatePreviewVoiceMatchPanel),$("preview-ref-play").addEventListener("click",async()=>{updatePreviewVoiceMatchPanel();const audio=$("preview-ref-audio");try{await audio.play()}catch(e){toast("Reference playback failed: "+e.message,"error")}}),$("preview-ref-use-text").addEventListener("click",()=>{const v=selectedPreviewLibraryVoice(),text=cleanReferenceText((v==null?void 0:v.transcript)||"");if(!text){toast("This voice has no reference text","error");return}$("preview-text-area").value=text,toast("Reference text copied to target text","success")}),$("preview-ref-synth").addEventListener("click",synthesizeSelectedReferenceText);let _ttsStreamHealth=null;function effectiveTtsPlaybackMode(override="settings"){return override&&override!=="settings"?override:_appSettings.tts_stream_mode||"auto"}async function isTtsStreamAvailable(force=!1){if(_ttsStreamHealth&&!force)return _ttsStreamHealth.ok;try{return _ttsStreamHealth=await fetch("/api/tts-stream-health").then(r=>r.json()),!!_ttsStreamHealth.ok}catch{return _ttsStreamHealth={ok:!1},!1}}async function createTtsStreamUrl(voice,text,instruct=""){if(!await isTtsStreamAvailable())throw new Error("streaming backend unavailable");const r=await fetch("/api/tts-stream-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,voice,instruct})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return(await r.json()).url}async function fetchTtsPreviewBlob(voice,text,responseFormat="wav",instruct="",backend="voice_clone",applyPersona=!1,extra=null){const body={text,voice,response_format:responseFormat,instruct,backend};applyPersona&&(body.apply_persona=!0),extra&&typeof extra=="object"&&Object.assign(body,extra);const r=await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return await r.blob()}async function createTtsAudioSource(voice,text,backend="voice_clone",modeOverride="settings",instruct="",applyPersona=!1,extra=null){const mode=effectiveTtsPlaybackMode(modeOverride);if(backend!=="streaming"||mode==="buffered"){const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}try{return{url:await createTtsStreamUrl(voice,text,instruct),blob:null,streaming:!0,label:"streaming"}}catch(e){if(mode==="streaming")throw e;const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}}let previewBlob=null;const PREVIEW_SAMPLE_TEXT="Hello! This is a voice preview from TTS Voice Creator - Clone and Design.";$("preview-text-area").addEventListener("focus",()=>{$("preview-text-area").value===PREVIEW_SAMPLE_TEXT&&($("preview-text-area").value="")},{once:!0});function _onPreviewGenerated(source,voice,text,backend,instruct){typeof effectsSourceBlob!="undefined"&&(window._effectsSourceBlob=null),window._effectsSynthArgs={voice,text,instruct:instruct||"",backend};const ea=$("effects-apply-btn");ea&&(ea.disabled=!1);const ap=$("add-to-playlist-btn");ap&&source.blob&&(ap.disabled=!1),typeof historyPush=="function"&&source.blob&&historyPush(voice,text,backend,source.blob,source.url)}const _TRYOUT_SPEED_KEY="ttsvc_tryout_native_speed";(function(){const saved=localStorage.getItem(_TRYOUT_SPEED_KEY);if(saved){const el=$("preview-native-speed");el&&(el.value=saved)}})(),(_Q=$("preview-native-speed"))==null||_Q.addEventListener("change",function(){localStorage.setItem(_TRYOUT_SPEED_KEY,this.value)}),$("preview-btn").addEventListener("click",async()=>{var _a2,_b2,_c2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,text=$("preview-text-area").value.trim(),instruct=$("preview-style-instruction").value.trim(),applyPersona=((_a2=$("preview-persona-toggle"))==null?void 0:_a2.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((_b2=$("preview-native-speed"))==null?void 0:_b2.value),_extra=!isNaN(_nspd)&&_nspd!==1?{speed:_nspd}:null;try{const audio=$("preview-audio"),source=((_c2=$("preview-chunked-toggle"))==null?void 0:_c2.checked)&&text.length>200&&typeof generateChunkedTts=="function"?await generateChunkedTts(voice,text,backend,instruct,_extra):await createTtsAudioSource(voice,text,backend,$("preview-playback-mode").value,instruct,applyPersona,_extra);previewBlob=source.blob,window._previewVoice=voice,window._previewBackend=backend,window._previewText=text,audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,_onPreviewGenerated(source,voice,text,backend,instruct),toast(source.streaming?"Streaming preview playing":source.label==="chunked"?`Chunked (${text.length} chars) playing`:"Preview playing","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{$("preview-btn").disabled=!1}}),$("save-preview-mp3-btn").addEventListener("click",async()=>{const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,text=$("preview-text-area").value.trim(),instruct=$("preview-style-instruction").value.trim();if(!backend){toast("No available TTS backend","error");return}if(!voice||!text)return;const btn=$("save-preview-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(voice||"preview")+"_preview.mp3",a.click(),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),$("save-preview-btn").addEventListener("click",()=>{if(!previewBlob)return;const a=document.createElement("a");a.href=URL.createObjectURL(previewBlob),a.download=($("tts-voice-select").value||"preview")+"_preview.wav",a.click()});const PERF_HISTORY_KEY="vcf-perf-history",PERF_HISTORY_MAX=50,PERF_HISTORY_SORT={key:"ts",dir:"desc"};function perfHistoryLoad(){try{return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY)||"[]")}catch{return[]}}function perfHistorySave(entries){try{localStorage.setItem(PERF_HISTORY_KEY,JSON.stringify(entries.slice(-PERF_HISTORY_MAX)))}catch{}}function perfHistoryAdd(entry){const h=perfHistoryLoad();h.push(entry),perfHistorySave(h)}function perfSparklineSvg(rtfValues){if(!rtfValues.length)return"";const W=120,H=32,PAD=2,barW=Math.max(4,Math.floor((W-PAD*2)/rtfValues.length)-1),maxV=Math.max(...rtfValues,1),bars=rtfValues.map((v,i)=>{const bh=Math.max(3,Math.round(v/maxV*(H-PAD*2))),x=PAD+i*(barW+1),y=H-PAD-bh,col=v<1?"var(--green)":"var(--yellow)";return``}).join("");return``}function perfHistoryVoiceLookup(voiceId){return(Array.isArray(window._voices)&&window._voices.length?window._voices:typeof _voices!="undefined"&&Array.isArray(_voices)?_voices:[]).find(v=>v&&(v.id===voiceId||v.name===voiceId||v.voice_id===voiceId))||null}function perfHistoryVoiceMeta(entry){var _a2,_b2,_c2;const voice=(entry==null?void 0:entry.voice)||"",saved=(entry==null?void 0:entry.voiceMeta)||{},lib=perfHistoryVoiceLookup(voice)||{},language=saved.lang||saved.language||(entry==null?void 0:entry.lang)||(entry==null?void 0:entry.language)||lib.lang||lib.language||"",gender=String(saved.gender||(entry==null?void 0:entry.gender)||lib.gender||"").trim().toUpperCase().charAt(0);return{id:voice,label:saved.label||saved.display_name||(entry==null?void 0:entry.voiceLabel)||lib.display_name||lib.name||voice,lang:language,gender:["F","M","N"].includes(gender)?gender:"",flag:saved.flag||(entry==null?void 0:entry.flag)||lib.flag||"",avatar:saved.avatar||(entry==null?void 0:entry.avatar)||lib.avatar||"",hasPicture:!!((_c2=(_b2=(_a2=saved.has_picture)!=null?_a2:saved.hasPicture)!=null?_b2:entry==null?void 0:entry.hasPicture)!=null?_c2:lib.has_picture)}}function perfHistoryExtraFields(voice,backend){const lib=perfHistoryVoiceLookup(voice)||{};return{voiceMeta:{label:lib.display_name||lib.name||voice,lang:lib.lang||lib.language||"",gender:lib.gender||"",flag:lib.flag||"",avatar:lib.avatar||"",has_picture:!!lib.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function perfHistoryGenderLabel(gender){return{F:"Female",M:"Male",N:"Diverse"}[gender]||""}function perfHistoryDeviceLabel(entry){return(entry==null?void 0:entry.device)||(typeof backendComputeDevice=="function"?backendComputeDevice((entry==null?void 0:entry.backend)||""):"")||"Unknown"}function perfHistoryDeviceHtml(entry){const label=perfHistoryDeviceLabel(entry),cls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass((entry==null?void 0:entry.backend)||""):label.toLowerCase().includes("gpu")?"gpu":label.toLowerCase().includes("cpu")?"cpu":"";return`${escHtml(label)}`}function perfHistoryAvatarHtml(entry){var _a2;const meta=perfHistoryVoiceMeta(entry),title=meta.label||meta.id||"Voice";if(meta.hasPicture)return``;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,24):null;if(icon)return`${icon.replace(/vp-avatar/g,"perf-history-avatar-icon")}`;const color=typeof avatarColor=="function"?avatarColor(meta.lang||meta.id||title):"#6b7280",init=((_a2=(title||"?").trim()[0])==null?void 0:_a2.toUpperCase())||"?";return`${escHtml(init)}`}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`
`;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=` Deleting ${n}/${ids.length}\u2026`}),close(),toast(`Deleted ${done} voice${done!==1?"s":""}${errors?` (${errors} errors)`:""}`,errors?"error":"success"),_bulkSelected.clear(),await loadVoiceLibrary()}),document.body.appendChild(ov),ov.querySelector(".vl-bdc-cancel").focus()}async function _bulkSetEnabled(ids,enabled){let done=0;for(const id of ids)await saveMeta(id,{enabled}).catch(()=>{}),done++;return done}function backendVoiceId(value){return typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||JSON.stringify(value)}function shouldFilterBackendVoices(backend){return["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend||"")}function shouldReplaceWithLibraryVoices(backend){return backend==="voice_design"}async function activeLibraryVoiceIds(){return _voices.length||await loadVoiceLibrary(),new Set((_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id))}function cleanReferenceText(text){return String(text||"").trim()}function selectedPreviewLibraryVoice(){var _a2;const id=((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"";return id?(_voices||[]).find(v=>v.id===id):null}function previewVoiceWarnings(v){var _a2;const warnings=[],backend=backendById(((_a2=$("tts-backend-select"))==null?void 0:_a2.value)||"");backend&&backend.id&&!["voice_clone","streaming","nvidia_zeroshot","nvidia_flow"].includes(backend.id)&&warnings.push(backend.id==="nvidia_magpie"?"NVIDIA Magpie uses fixed speaker voices, not saved WAV clone identity.":"This backend may follow style/model voice more than the saved WAV identity."),backend&&backend.id==="nvidia_zeroshot"&&v.duration&&(Number(v.duration)<3||Number(v.duration)>10)&&warnings.push("NVIDIA Zeroshot works best with a clear 3-10 second prompt."),backend&&backend.id==="nvidia_flow"&&!v.transcript&&warnings.push("NVIDIA Flow requires the exact saved reference transcript for this voice."),v.transcript||warnings.push("No reference transcript is saved; cloned identity is harder to judge."),v.duration&&(Number(v.duration)<3||Number(v.duration)>20)&&warnings.push("Reference clip length is outside the 3-20 second sweet spot."),v.needs_tts_restart&&warnings.push("This voice changed since the last backend refresh; restart or clear restart flags before judging it.");const healthWarnings=v.health&&Array.isArray(v.health.warnings)?v.health.warnings:[];return warnings.push(...healthWarnings.slice(0,3)),warnings}function updatePreviewVoiceMatchPanel(){const panel=$("preview-match-panel");if(!panel)return;const v=selectedPreviewLibraryVoice();if(!v){panel.hidden=!0;return}panel.hidden=!1;const lang=v.language||v.lang||(v.id||"").split("_")[0]||"-",gender=v.gender||(v.id||"").split("_")[1]||"-",db=fmtDbfs(v),dur=v.duration?fmtDuration(v.duration):"-";$("preview-match-title").textContent=v.id,$("preview-match-detail").textContent=`${lang} \xB7 ${gender} \xB7 ${dur} \xB7 ${db} dBFS`;const warnings=previewVoiceWarnings(v);$("preview-match-warning").textContent=warnings.length?warnings.join(" "):"For a fair voice match check, play the WAV and synthesize the exact saved reference text.";const transcript=cleanReferenceText(v.transcript||"");$("preview-match-transcript").textContent=transcript||"No reference text saved for this voice.",$("preview-ref-use-text").disabled=!transcript,$("preview-ref-synth").disabled=!transcript;const audio=$("preview-ref-audio"),expected=voiceFileUrl(v);audio.dataset.src!==expected&&(audio.pause(),audio.src=expected,audio.dataset.src=expected);const actionsEl=panel.querySelector(".preview-match-actions");let personaBtn=panel.querySelector(".preview-persona-btn");v.persona?personaBtn||(personaBtn=document.createElement("button"),personaBtn.className="btn-secondary preview-persona-btn",personaBtn.type="button",personaBtn.textContent="Rewrite with persona",actionsEl==null||actionsEl.appendChild(personaBtn),personaBtn.addEventListener("click",async()=>{const text=$("preview-text-area").value.trim();if(!text){toast("Enter text to rewrite","error");return}const lv=selectedPreviewLibraryVoice();if(!(lv!=null&&lv.persona)){toast("This voice has no persona","error");return}personaBtn.disabled=!0,personaBtn.textContent="Rewriting\u2026";try{const llmUrl=localStorage.getItem("refine-llm-url")||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",r=await fetch("/api/rewrite-with-persona",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,persona:lv.persona,llm_url:llmUrl,model:(_appSettings==null?void 0:_appSettings.llm_model)||"",mode:"rewrite"})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();$("preview-text-area").value=d.text,toast("Text rewritten in persona style","success")}catch(e){toast("Persona rewrite failed: "+e.message,"error")}finally{personaBtn.disabled=!1,personaBtn.textContent="Rewrite with persona"}})):personaBtn==null||personaBtn.remove();const personaToggle=$("preview-persona-toggle");if(personaToggle){const label=personaToggle.closest("label");v.persona?(personaToggle.disabled=!1,label&&(label.title="Rewrite text through this voice's character persona before generating")):(personaToggle.checked=!1,personaToggle.disabled=!0,label&&(label.title="This voice has no character persona saved \u2014 set one on the Voice Inspector page first."))}}async function synthesizeSelectedReferenceText(){const v=selectedPreviewLibraryVoice();if(!v){toast("Select a library voice first","error");return}let text=cleanReferenceText(v.transcript||"");if(!text){toast("This voice has no reference text","error");return}if(v.needs_tts_restart){if(!confirm("This voice is marked as needing a TTS restart. If you already restarted the backend, clear the flag and synthesize anyway?"))return;await clearTtsRestartFlags(),v.needs_tts_restart=!1,updatePreviewVoiceMatchPanel()}const backend=$("tts-backend-select").value;if(!backend){toast("No available TTS backend","error");return}const btn=$("preview-ref-synth");btn.disabled=!0;try{$("preview-text-area").value=text;const source=await createTtsAudioSource(v.id,text,backend,$("preview-playback-mode").value,$("preview-style-instruction").value.trim());previewBlob=source.blob;const audio=$("preview-audio");audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,toast(source.streaming?"Reference text streaming":"Reference text synthesized","success")}catch(e){toast("Reference synthesis failed: "+e.message,"error")}finally{btn.disabled=!1}}$("fetch-tts-voices-btn").addEventListener("click",async()=>{var _a2;$("fetch-tts-voices-btn").disabled=!0;try{const backend=(_a2=$("tts-backend-select"))==null?void 0:_a2.value;if(!backend)throw new Error("No available TTS backend");let ids;if(shouldReplaceWithLibraryVoices(backend))_voices.length||await loadVoiceLibrary(),ids=(_voices||[]).filter(v=>v.enabled!==!1).map(v=>v.id);else{const rawVoices=await fetch("/api/tts-voices?backend="+encodeURIComponent(backend)).then(r=>r.json());let voices=Array.isArray(rawVoices)?rawVoices:[];if(shouldFilterBackendVoices(backend)){const activeIds=await activeLibraryVoiceIds();voices=voices.filter(v=>activeIds.has(backendVoiceId(v)))}ids=voices.map(backendVoiceId)}const sel=$("tts-voice-select"),prev=sel.value;window.VoicePicker?(VoicePicker.upgrade("tts-voice-select"),VoicePicker.populate("tts-voice-select",ids),prev&&ids.includes(prev)&&VoicePicker.setValue("tts-voice-select",prev)):(sel.innerHTML='',ids.forEach(id=>{const o=document.createElement("option");o.value=o.textContent=id,sel.appendChild(o)}),prev&&ids.includes(prev)&&(sel.value=prev)),updatePreviewVoiceMatchPanel();const suffix=shouldFilterBackendVoices(backend)||shouldReplaceWithLibraryVoices(backend)?" active voices":" voices";toast("Fetched "+ids.length+suffix,"success")}catch(e){toast("Fetch failed: "+e.message,"error")}finally{$("fetch-tts-voices-btn").disabled=!1}}),$("tts-backend-select").addEventListener("change",()=>{const sel=$("tts-voice-select");sel.innerHTML='',updateBackendHelp(),updatePreviewVoiceMatchPanel(),previewBlob=null,$("save-preview-mp3-btn").disabled=!0,$("save-preview-btn").disabled=!0}),$("tts-voice-select").addEventListener("change",updatePreviewVoiceMatchPanel),$("preview-ref-play").addEventListener("click",async()=>{updatePreviewVoiceMatchPanel();const audio=$("preview-ref-audio");try{await audio.play()}catch(e){toast("Reference playback failed: "+e.message,"error")}}),$("preview-ref-use-text").addEventListener("click",()=>{const v=selectedPreviewLibraryVoice(),text=cleanReferenceText((v==null?void 0:v.transcript)||"");if(!text){toast("This voice has no reference text","error");return}$("preview-text-area").value=text,toast("Reference text copied to target text","success")}),$("preview-ref-synth").addEventListener("click",synthesizeSelectedReferenceText);let _ttsStreamHealth=null;function effectiveTtsPlaybackMode(override="settings"){return override&&override!=="settings"?override:_appSettings.tts_stream_mode||"auto"}async function isTtsStreamAvailable(force=!1){if(_ttsStreamHealth&&!force)return _ttsStreamHealth.ok;try{return _ttsStreamHealth=await fetch("/api/tts-stream-health").then(r=>r.json()),!!_ttsStreamHealth.ok}catch{return _ttsStreamHealth={ok:!1},!1}}async function createTtsStreamUrl(voice,text,instruct=""){if(!await isTtsStreamAvailable())throw new Error("streaming backend unavailable");const r=await fetch("/api/tts-stream-session",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,voice,instruct})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}return(await r.json()).url}async function _ttsPreviewFetchWithRetry(body,tries){tries=tries||3;for(let i=1;i<=tries;i++)try{return await fetch("/api/tts-preview",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(body)})}catch(e){if(i===tries)throw e;await new Promise(r=>setTimeout(r,2500*i))}}function _ttsBackendForVoice(voiceId,fallbackBackend){if(!voiceId||voiceId==="me")return fallbackBackend;const v=(window._voices||[]).find(x=>x.id===voiceId);return v&&(v.origin==="designed"||!v.has_ref)?"voice_design":fallbackBackend}async function fetchTtsPreviewBlob(voice,text,responseFormat="wav",instruct="",backend="voice_clone",applyPersona=!1,extra=null){const body={text,voice,response_format:responseFormat,instruct,backend};applyPersona&&(body.apply_persona=!0),extra&&typeof extra=="object"&&Object.assign(body,extra);const r=await _ttsPreviewFetchWithRetry(body);if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const blob=await r.blob();if(responseFormat==="wav"){const v=(window._voices||[]).find(x=>x.id===voice),gainDb=v&&v.loudness&&typeof v.loudness.gain_db=="number"?v.loudness.gain_db:0;if(gainDb)try{const nr=await fetch("/api/audio/apply-gain?gain_db="+encodeURIComponent(gainDb),{method:"POST",body:blob});if(nr.ok)return await nr.blob()}catch{}}return blob}function _textWords(s){return String(s||"").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g,"").replace(/[^\p{L}\p{N}\s]/gu," ").split(/\s+/).filter(Boolean)}function _textSimilarity(a,b){const wa=_textWords(a),wb=_textWords(b);if(!wa.length&&!wb.length)return 1;if(!wa.length||!wb.length)return 0;const counts=new Map;wa.forEach(w=>counts.set(w,(counts.get(w)||0)+1));let overlap=0;return wb.forEach(w=>{const c=counts.get(w);c&&(overlap++,counts.set(w,c-1))}),2*overlap/(wa.length+wb.length)}async function _voiceRoundtripCheck(voiceId,text,backend,instruct=""){const blob=await fetchTtsPreviewBlob(voiceId,text,"wav",instruct,backend),fd=new FormData;fd.append("file",blob,"roundtrip.wav"),fd.append("backend","configured");const r=await fetch("/api/transcribe-bytes",{method:"POST",body:fd});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();return{transcript:d.text||"",score:_textSimilarity(text,d.text||""),blob}}async function createTtsAudioSource(voice,text,backend="voice_clone",modeOverride="settings",instruct="",applyPersona=!1,extra=null){const mode=effectiveTtsPlaybackMode(modeOverride);if(backend!=="streaming"||mode==="buffered"){const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}try{return{url:await createTtsStreamUrl(voice,text,instruct),blob:null,streaming:!0,label:"streaming"}}catch(e){if(mode==="streaming")throw e;const blob=await fetchTtsPreviewBlob(voice,text,"wav",instruct,backend,applyPersona,extra);return{url:URL.createObjectURL(blob),blob,streaming:!1,label:"buffered"}}}let previewBlob=null;const PREVIEW_SAMPLE_TEXT="Hello! This is a voice preview from TTS Voice Creator - Clone and Design.";$("preview-text-area").addEventListener("focus",()=>{$("preview-text-area").value===PREVIEW_SAMPLE_TEXT&&($("preview-text-area").value="")},{once:!0});function _onPreviewGenerated(source,voice,text,backend,instruct){typeof effectsSourceBlob!="undefined"&&(window._effectsSourceBlob=null),window._effectsSynthArgs={voice,text,instruct:instruct||"",backend};const ea=$("effects-apply-btn");ea&&(ea.disabled=!1);const ap=$("add-to-playlist-btn");ap&&source.blob&&(ap.disabled=!1),typeof historyPush=="function"&&source.blob&&historyPush(voice,text,backend,source.blob,source.url)}const _TRYOUT_SPEED_KEY="ttsvc_tryout_native_speed";(function(){const saved=localStorage.getItem(_TRYOUT_SPEED_KEY);if(saved){const el=$("preview-native-speed");el&&(el.value=saved)}})(),(_R=$("preview-native-speed"))==null||_R.addEventListener("change",function(){localStorage.setItem(_TRYOUT_SPEED_KEY,this.value)}),$("preview-btn").addEventListener("click",async()=>{var _a2,_b2,_c2;const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,text=$("preview-text-area").value.trim(),instruct=$("preview-style-instruction").value.trim(),applyPersona=((_a2=$("preview-persona-toggle"))==null?void 0:_a2.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((_b2=$("preview-native-speed"))==null?void 0:_b2.value),_extra=!isNaN(_nspd)&&_nspd!==1?{speed:_nspd}:null;try{const audio=$("preview-audio"),source=((_c2=$("preview-chunked-toggle"))==null?void 0:_c2.checked)&&text.length>200&&typeof generateChunkedTts=="function"?await generateChunkedTts(voice,text,backend,instruct,_extra,applyPersona):await createTtsAudioSource(voice,text,backend,$("preview-playback-mode").value,instruct,applyPersona,_extra);previewBlob=source.blob,window._previewVoice=voice,window._previewBackend=backend,window._previewText=text,audio.src=source.url,audio.style.display="",await audio.play(),$("save-preview-mp3-btn").disabled=!1,$("save-preview-btn").disabled=source.streaming,_onPreviewGenerated(source,voice,text,backend,instruct),toast(source.streaming?"Streaming preview playing":source.label==="chunked"?`Chunked (${text.length} chars) playing`:"Preview playing","success")}catch(e){toast("TTS failed: "+e.message,"error")}finally{$("preview-btn").disabled=!1}}),$("save-preview-mp3-btn").addEventListener("click",async()=>{const voice=$("tts-voice-select").value,backend=$("tts-backend-select").value,text=$("preview-text-area").value.trim(),instruct=$("preview-style-instruction").value.trim();if(!backend){toast("No available TTS backend","error");return}if(!voice||!text)return;const btn=$("save-preview-mp3-btn");btn.disabled=!0;try{const blob=await fetchTtsPreviewBlob(voice,text,"mp3",instruct,backend),a=document.createElement("a");a.href=URL.createObjectURL(blob),a.download=(voice||"preview")+"_preview.mp3",a.click(),toast("MP3 saved","success")}catch(e){toast("MP3 save failed: "+e.message,"error")}finally{btn.disabled=!1}}),$("save-preview-btn").addEventListener("click",()=>{if(!previewBlob)return;const a=document.createElement("a");a.href=URL.createObjectURL(previewBlob),a.download=($("tts-voice-select").value||"preview")+"_preview.wav",a.click()});const PERF_HISTORY_KEY="vcf-perf-history",PERF_HISTORY_MAX=50,PERF_HISTORY_SORT={key:"ts",dir:"desc"};function perfHistoryLoad(){try{return JSON.parse(localStorage.getItem(PERF_HISTORY_KEY)||"[]")}catch{return[]}}function perfHistorySave(entries){try{localStorage.setItem(PERF_HISTORY_KEY,JSON.stringify(entries.slice(-PERF_HISTORY_MAX)))}catch{}}function perfHistoryAdd(entry){const h=perfHistoryLoad();h.push(entry),perfHistorySave(h)}function perfSparklineSvg(rtfValues){if(!rtfValues.length)return"";const W=120,H=32,PAD=2,barW=Math.max(4,Math.floor((W-PAD*2)/rtfValues.length)-1),maxV=Math.max(...rtfValues,1),bars=rtfValues.map((v,i)=>{const bh=Math.max(3,Math.round(v/maxV*(H-PAD*2))),x=PAD+i*(barW+1),y=H-PAD-bh,col=v<1?"var(--green)":"var(--yellow)";return``}).join("");return``}function perfHistoryVoiceLookup(voiceId){return(Array.isArray(window._voices)&&window._voices.length?window._voices:typeof _voices!="undefined"&&Array.isArray(_voices)?_voices:[]).find(v=>v&&(v.id===voiceId||v.name===voiceId||v.voice_id===voiceId))||null}function perfHistoryVoiceMeta(entry){var _a2,_b2,_c2;const voice=(entry==null?void 0:entry.voice)||"",saved=(entry==null?void 0:entry.voiceMeta)||{},lib=perfHistoryVoiceLookup(voice)||{},language=saved.lang||saved.language||(entry==null?void 0:entry.lang)||(entry==null?void 0:entry.language)||lib.lang||lib.language||"",gender=String(saved.gender||(entry==null?void 0:entry.gender)||lib.gender||"").trim().toUpperCase().charAt(0);return{id:voice,label:saved.label||saved.display_name||(entry==null?void 0:entry.voiceLabel)||lib.display_name||lib.name||voice,lang:language,gender:["F","M","N"].includes(gender)?gender:"",flag:saved.flag||(entry==null?void 0:entry.flag)||lib.flag||"",avatar:saved.avatar||(entry==null?void 0:entry.avatar)||lib.avatar||"",hasPicture:!!((_c2=(_b2=(_a2=saved.has_picture)!=null?_a2:saved.hasPicture)!=null?_b2:entry==null?void 0:entry.hasPicture)!=null?_c2:lib.has_picture)}}function perfHistoryExtraFields(voice,backend){const lib=perfHistoryVoiceLookup(voice)||{};return{voiceMeta:{label:lib.display_name||lib.name||voice,lang:lib.lang||lib.language||"",gender:lib.gender||"",flag:lib.flag||"",avatar:lib.avatar||"",has_picture:!!lib.has_picture},device:typeof backendComputeDevice=="function"?backendComputeDevice(backend):""}}function perfHistoryGenderLabel(gender){return{F:"Female",M:"Male",N:"Diverse"}[gender]||""}function perfHistoryDeviceLabel(entry){return(entry==null?void 0:entry.device)||(typeof backendComputeDevice=="function"?backendComputeDevice((entry==null?void 0:entry.backend)||""):"")||"Unknown"}function perfHistoryDeviceHtml(entry){const label=perfHistoryDeviceLabel(entry),cls=typeof backendComputeDeviceClass=="function"?backendComputeDeviceClass((entry==null?void 0:entry.backend)||""):label.toLowerCase().includes("gpu")?"gpu":label.toLowerCase().includes("cpu")?"cpu":"";return`${escHtml(label)}`}function perfHistoryAvatarHtml(entry){var _a2;const meta=perfHistoryVoiceMeta(entry),title=meta.label||meta.id||"Voice";if(meta.hasPicture)return``;const icon=window.voiceAvatarIcon?window.voiceAvatarIcon(meta.avatar,24):null;if(icon)return`${icon.replace(/vp-avatar/g,"perf-history-avatar-icon")}`;const color=typeof avatarColor=="function"?avatarColor(meta.lang||meta.id||title):"#6b7280",init=((_a2=(title||"?").trim()[0])==null?void 0:_a2.toUpperCase())||"?";return`${escHtml(init)}`}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``}function renderPerfHistory(){var _a2,_b2;const histList=$("perf-history-list");if(!histList)return;const filterEl=$("perf-history-filter-current"),filterOn=filterEl==null?void 0:filterEl.checked,curBack=(_a2=$("perf-backend-select"))==null?void 0:_a2.value,curVoice=(_b2=$("perf-voice-select"))==null?void 0:_b2.value;let entries=perfHistoryLoad().slice();if(filterOn&&curBack&&(entries=entries.filter(e=>e.backend===curBack&&e.voice===curVoice)),!entries.length){histList.innerHTML='
'+(filterOn?"No history for this backend/voice yet.":"No benchmark history yet. Run a benchmark above to start tracking.")+"
";return}entries.sort((a,b)=>perfHistoryCompare(a,b)||Number(b.ts)-Number(a.ts));const head=`
${perfHistoryHeadButton("ts","Date / Time")} @@ -762,7 +778,7 @@ This warms each voice so the engine caches its .pt and first playback is instant `,sessionDone&&rtfArr.length&&(updateTrendDisplay(perfBackendSel.value,perfVoiceSel.value,avgRtf),perfHistoryAdd({ts:Date.now(),backend:perfBackendSel.value,voice:perfVoiceSel.value,...perfHistoryExtraFields(perfVoiceSel.value,perfBackendSel.value),textLen:perfText.value.trim().length,avgLatencyMs:avg,minLatencyMs:minL,maxLatencyMs:maxL,avgRtf,runCount:ok.length,allOk:ok.length===perfRows.length}),renderPerfHistory())}else perfSummary.innerHTML='All runs failed'}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;i1?"s":""} completed.`,renderPerfTable(!0),perfRunBtn.disabled=!1}),(_a2=$("perf-history-filter-current"))==null||_a2.addEventListener("change",renderPerfHistory),(_b2=$("perf-history-clear-btn"))==null||_b2.addEventListener("click",()=>{perfHistorySave([]),renderPerfHistory(),toast("Benchmark history cleared","success")}),renderPerfHistory()})();async function mergeWavBlobs(blobs){if(!blobs||blobs.length===0)return null;if(blobs.length===1)return blobs[0];function parseWav(bytes){const v=new DataView(bytes.buffer);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}}const parsed=[];for(const b of blobs){const bytes=new Uint8Array(await b.arrayBuffer()),p=parseWav(bytes);if(!p.fmt)throw new Error("Invalid WAV in chunk");parsed.push({bytes,...p})}const ref=parsed[0].fmt,totalPcm=parsed.reduce((s,p)=>s+p.dataSize,0),out=new Uint8Array(44+totalPcm),dv=new DataView(out.buffer);dv.setUint32(0,1380533830,!1),dv.setUint32(4,36+totalPcm,!0),dv.setUint32(8,1463899717,!1),dv.setUint32(12,1718449184,!1),dv.setUint32(16,16,!0),dv.setUint16(20,1,!0),dv.setUint16(22,ref.channels,!0),dv.setUint32(24,ref.sampleRate,!0),dv.setUint32(28,ref.sampleRate*ref.channels*(ref.bitDepth>>3),!0),dv.setUint16(32,ref.channels*(ref.bitDepth>>3),!0),dv.setUint16(34,ref.bitDepth,!0),dv.setUint32(36,1684108385,!1),dv.setUint32(40,totalPcm,!0);let pos=44;for(const p of parsed)out.set(p.bytes.slice(p.dataOff,p.dataOff+p.dataSize),pos),pos+=p.dataSize;return new Blob([out],{type:"audio/wav"})}function splitTextIntoChunks(text,maxLen=800){const abbrev=/\b(Mr|Mrs|Ms|Dr|Prof|Sr|Jr|vs|etc|e\.g|i\.e)\.\s/g,safe=text.replace(abbrev,m=>m.replace(".","\0")),restore=s=>s.replace(/\x00/g,"."),segments=[];for(const line of safe.split(` `)){if(!line.trim()){segments.length&&segments.push("");continue}const sentences=line.match(/[^.!?]+[.!?]+\s*/g)||[],rest=line.replace(/[^.!?]+[.!?]+\s*/g,"").trim();segments.push(...sentences),rest&&segments.push(rest)}if(!segments.length)return[text];const chunks=[];let cur="",paraPending=!1;for(const seg of segments){if(seg===""){paraPending=!0;continue}const joined=cur?cur+(paraPending?` -`:" ")+seg.trim():seg.trim();joined.length>maxLen&&cur?(chunks.push(restore(cur.trim())),cur=seg.trim()):cur=joined,paraPending=!1}return cur.trim()&&chunks.push(restore(cur.trim())),chunks.length?chunks:[text]}async function generateChunkedTts(voice,text,backend,instruct,extra=null){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;i20&&_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='
No generations yet.
';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?``:`${(item.voice||"?")[0].toUpperCase()}`;return`
+`:" ")+seg.trim():seg.trim();joined.length>maxLen&&cur?(chunks.push(restore(cur.trim())),cur=seg.trim()):cur=joined,paraPending=!1}return cur.trim()&&chunks.push(restore(cur.trim())),chunks.length?chunks:[text]}async function generateChunkedTts(voice,text,backend,instruct,extra=null,applyPersona=!1){const chunks=splitTextIntoChunks(text),prog=$("preview-chunk-progress");prog&&(prog.hidden=!1,prog.textContent=`Chunk 1 / ${chunks.length}\u2026`);const blobs=[];for(let i=0;i20&&_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='
No generations yet.
';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?``:`${(item.voice||"?")[0].toUpperCase()}`;return`
${avatar} ${escHtml(item.voice)} @@ -776,7 +792,7 @@ This warms each voice so the engine caches its .pt and first playback is instant
-
`}).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)})})}}(_R=$("history-clear-btn"))==null||_R.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='
No clips in playlist. Use + Playlist after generating.
',$("playlist-export-btn")&&($("playlist-export-btn").disabled=!0);return}list.innerHTML=_playlist.map((item,i)=>` +
`}).join(""),list.querySelectorAll(".hist-del-btn").forEach(btn=>{btn.addEventListener("click",e=>{var _a2;e.stopPropagation();const hid=(_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid,idx=_genHistory.findIndex(h=>h.id===hid);idx!==-1&&(_genHistory.splice(idx,1),renderHistory())})}),list.querySelectorAll(".hist-play-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});if(!(item!=null&&item.url))return;const audio=$("preview-audio");audio.src=item.url,audio.style.display="",audio.play().catch(()=>{})})}),list.querySelectorAll(".hist-reuse-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});if(!item)return;$("preview-text-area").value=item.text;const bSel=$("tts-backend-select");bSel&&[...bSel.options].forEach(o=>{o.value===item.backend&&(bSel.value=item.backend)});const vSel=$("tts-voice-select");vSel&&[...vSel.options].forEach(o=>{o.value===item.voice&&(vSel.value=item.voice)}),toast("Settings restored from history","success")})}),list.querySelectorAll(".hist-playlist-btn").forEach(btn=>{btn.addEventListener("click",()=>{const item=_genHistory.find(h=>{var _a2;return h.id===((_a2=btn.closest("[data-hid]"))==null?void 0:_a2.dataset.hid)});item!=null&&item.blob&&playlistAdd(item.voice,item.text,item.blob,item.url)})})}}(_S=$("history-clear-btn"))==null||_S.addEventListener("click",()=>{_genHistory.length=0,renderHistory(),toast("History cleared","success")});const _playlist=[];function playlistAdd(voice,text,blob,url){const id="pl-"+Date.now()+"-"+Math.random().toString(36).slice(2,5);_playlist.push({id,voice,text:text.slice(0,120),blob,url}),renderPlaylist(),$("playlist-export-btn")&&($("playlist-export-btn").disabled=!1),toast("Added to playlist","success")}function renderPlaylist(){const list=$("playlist-list");if(list){if(!_playlist.length){list.innerHTML='
No clips in playlist. Use + Playlist after generating.
',$("playlist-export-btn")&&($("playlist-export-btn").disabled=!0);return}list.innerHTML=_playlist.map((item,i)=>`
${i+1}
@@ -788,7 +804,7 @@ This warms each voice so the engine caches its .pt and first playback is instant
-
`).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())})})}}(_S=$("add-to-playlist-btn"))==null||_S.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)||"")}),(_T=$("playlist-export-btn"))==null||_T.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}}),(_U=$("playlist-clear-btn"))==null||_U.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:-18,ratio:3}},broadcast:{compressor:{on:!0,threshold_db:-12,ratio:6}},telephone:{compressor:{on:!0,threshold_db:-10,ratio:8}},warm:{reverb:{on:!0,room_size:.2,wet:.15},compressor:{on:!0,threshold_db:-20,ratio:2}},radio:{compressor:{on:!0,threshold_db:-14,ratio:5}}};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"),(_V=$("effects-preset"))==null||_V.addEventListener("change",()=>{const preset=_FX_PRESETS[$("effects-preset").value];preset&&(["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-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)),["fx-reverb-room","fx-reverb-wet","fx-comp-thresh","fx-comp-ratio","fx-chorus-rate","fx-chorus-mix","fx-pitch-semi"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))}))}),(_W=$("effects-reset-btn"))==null||_W.addEventListener("click",()=>{$("effects-preset").value="",["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-on"].forEach(id=>{const el=$(id);el&&(el.checked=!1)}),$("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"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))})});let _effectsSourceBlob=null;(_X=$("effects-apply-btn"))==null||_X.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2;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}}),!chain.length){toast("Enable at least one effect","error");return}const btn=$("effects-apply-btn"),st=$("effects-status");btn.disabled=!0,st&&(st.textContent="Processing\u2026");try{let blob=previewBlob||_effectsSourceBlob;if(!blob){const args=window._effectsSynthArgs;if(!args||typeof fetchTtsPreviewBlob!="function"){toast("Generate audio first","error");return}st&&(st.textContent="Synthesizing audio\u2026"),blob=await fetchTtsPreviewBlob(args.voice,args.text,"wav",args.instruct,args.backend)}const fd=new FormData;fd.append("audio",blob,"audio.wav"),fd.append("effects",JSON.stringify(chain));const resp=await fetch("/api/audio/effects",{method:"POST",body:fd});if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.detail||resp.statusText)}const out=await resp.blob();_effectsSourceBlob||(_effectsSourceBlob=previewBlob),previewBlob=out;const audio=$("preview-audio");audio.src=URL.createObjectURL(out),audio.style.display="",audio.play().catch(()=>{}),st&&(st.textContent="Applied."),toast("Effects applied","success")}catch(e){st&&(st.textContent=""),toast("Effects failed: "+e.message,"error")}finally{btn.disabled=!1}});let _refineOriginal=null;(function(){const inp=$("refine-llm-url");if(!inp)return;const saved=_appSettings&&_appSettings.refine_llm_url||localStorage.getItem("refine-llm-url");saved?inp.value=saved:inp.value=_appSettings&&_appSettings.llm_url||_appSettings&&_appSettings.engine_local_urls&&_appSettings.engine_local_urls.ollama||localStorage.getItem("llm-local-url-ollama")||"http://localhost:11434/v1",inp.addEventListener("input",()=>{localStorage.setItem("refine-llm-url",inp.value),_patchSettings({refine_llm_url:inp.value}),_appSettings&&(_appSettings.refine_llm_url=inp.value)})})();function updateRefineButtonState(){var _a2,_b2;const btn=$("refine-btn");btn&&(btn.disabled=!((_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)!=null&&_b2.trim()))}(_Y=$("stt-tts-text"))==null||_Y.addEventListener("input",updateRefineButtonState),(_Z=$("refine-btn"))==null||_Z.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l2,_m2,_n2,_o2,_p2;const text=(_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)==null?void 0:_b2.trim();if(!text)return;const btn=$("refine-btn"),st=$("refine-status");btn.disabled=!0,st&&(st.textContent="Refining\u2026");try{const r=await fetch("/api/refine-text",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,llm_url:((_d2=(_c2=$("refine-llm-url"))==null?void 0:_c2.value)==null?void 0:_d2.trim())||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",model:((_f2=(_e2=$("refine-model"))==null?void 0:_e2.value)==null?void 0:_f2.trim())||(_appSettings==null?void 0:_appSettings.refine_model)||(_appSettings==null?void 0:_appSettings.llm_model)||"",toggles:{fillers:(_h2=(_g2=$("refine-fillers"))==null?void 0:_g2.checked)!=null?_h2:!0,repetitions:(_j2=(_i2=$("refine-repetitions"))==null?void 0:_i2.checked)!=null?_j2:!0,corrections:(_l2=(_k2=$("refine-corrections"))==null?void 0:_k2.checked)!=null?_l2:!0,punctuation:(_n2=(_m2=$("refine-punctuation"))==null?void 0:_m2.checked)!=null?_n2:!0}})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_refineOriginal=text,$("stt-tts-text").value=d.text,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!1),st&&(st.textContent="Done."),toast("Transcription refined","success")}catch(e){st&&(st.textContent=""),toast("Refinement failed: "+e.message,"error")}finally{btn.disabled=!((_p2=(_o2=$("stt-tts-text"))==null?void 0:_o2.value)!=null&&_p2.trim())}}),(__=$("refine-restore-btn"))==null||__.addEventListener("click",()=>{_refineOriginal&&($("stt-tts-text").value=_refineOriginal,_refineOriginal=null,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!0),$("refine-status")&&($("refine-status").textContent=""),toast("Original transcription restored","success"))}),(_$=$("preview-style-instruction"))==null||_$.addEventListener("input",()=>{typeof updateBackendHelp=="function"&&updateBackendHelp()}),function(){var _a2;const batchBackendSel=$("batch-backend-select"),batchRunsSel=$("batch-runs"),batchLoadBtn=$("batch-load-voices-btn"),batchSearchInput=$("batch-voice-search"),batchSelectAllBtn=$("batch-select-all-btn"),batchSelectNoneBtn=$("batch-select-none-btn"),batchVoiceList=$("batch-voice-list"),batchSelCount=$("batch-selected-count"),batchRunBtn=$("batch-run-btn"),batchStopBtn=$("batch-stop-btn"),perfRunSelectedBtn=$("perf-run-selected-btn"),batchProgress=$("batch-progress"),batchProgLabel=$("batch-progress-label"),batchProgCount=$("batch-progress-count"),batchProgBar=$("batch-progress-bar"),batchResultsCard=$("batch-results-card"),batchResultsLabel=$("batch-results-label"),batchTbody=$("batch-tbody");if(!batchRunBtn)return;let batchStopped=!1,batchResults=[],batchVoices=[],batchSelected=new Set,batchSortField="factor",batchSortDir=-1;function populateBatchBackends(){if(!batchBackendSel)return;const cur=batchBackendSel.value;batchBackendSel.innerHTML=availableTtsBackends().map(b=>``).join("")||''}populateBatchBackends();function libraryVoice(id){return(window._voices||[]).find(v=>v&&v.id===id)||null}function normalizeBatchVoice(value){const id=typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||(value==null?void 0:value.voice_id)||"",meta=typeof value=="object"?value.meta||libraryVoice(id)||value:libraryVoice(id)||null;return id?{id,label:(meta==null?void 0:meta.display_name)||(value==null?void 0:value.label)||(meta==null?void 0:meta.name)||(value==null?void 0:value.name)||id,meta}:null}function batchVoiceStats(v){var _a3,_b2;const meta=(v==null?void 0:v.meta)||libraryVoice(v==null?void 0:v.id)||{},b=meta.benchmark||{},parts=[],dur=Number(meta.duration),speed=Number((_a3=b.speed)!=null?_a3:b.avg_speed),rtf=Number((_b2=b.rtf)!=null?_b2:b.avg_rtf);return Number.isFinite(dur)&&dur>0&&parts.push(`${dur.toFixed(1)}s`),Number.isFinite(speed)&&speed>0?parts.push(`${speed.toFixed(2)}x`):Number.isFinite(rtf)&&rtf>0&&parts.push(`RTF ${rtf.toFixed(2)}`),parts.join(" \xB7 ")}function batchColor(id){const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i>>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``;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`${escHtml(init)}`}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=` ${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='
No voices found. Fetch voices above or reload from backend.
',updateSelCount();return}if(!visible.length){batchVoiceList.innerHTML='
No matching voices.
',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`
`).join(""),list.querySelectorAll(".pl-rm").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx>=0&&(_playlist.splice(idx,1),renderPlaylist())})}),list.querySelectorAll(".pl-up").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx>0&&([_playlist[idx-1],_playlist[idx]]=[_playlist[idx],_playlist[idx-1]],renderPlaylist())})}),list.querySelectorAll(".pl-dn").forEach(btn=>{btn.addEventListener("click",()=>{var _a2;const pid=(_a2=btn.closest("[data-pid]"))==null?void 0:_a2.dataset.pid,idx=_playlist.findIndex(p=>p.id===pid);idx<_playlist.length-1&&([_playlist[idx],_playlist[idx+1]]=[_playlist[idx+1],_playlist[idx]],renderPlaylist())})})}}(_T=$("add-to-playlist-btn"))==null||_T.addEventListener("click",()=>{var _a2,_b2,_c2;if(!previewBlob){toast("Generate audio first","error");return}playlistAdd(window._previewVoice||((_a2=$("tts-voice-select"))==null?void 0:_a2.value)||"",window._previewText||((_b2=$("preview-text-area"))==null?void 0:_b2.value)||"",previewBlob,((_c2=$("preview-audio"))==null?void 0:_c2.src)||"")}),(_U=$("playlist-export-btn"))==null||_U.addEventListener("click",async()=>{if(!_playlist.length)return;const btn=$("playlist-export-btn"),orig=btn.textContent;btn.disabled=!0,btn.textContent="Merging\u2026";try{const merged=await mergeWavBlobs(_playlist.map(p=>p.blob).filter(Boolean));if(!merged)throw new Error("No audio to export");const a=document.createElement("a");a.href=URL.createObjectURL(merged),a.download="playlist_"+Date.now()+".wav",a.click(),toast("Playlist exported as WAV","success")}catch(e){toast("Export failed: "+e.message,"error")}finally{btn.disabled=_playlist.length===0,btn.textContent=orig}}),(_V=$("playlist-clear-btn"))==null||_V.addEventListener("click",()=>{_playlist.length=0,renderPlaylist(),toast("Playlist cleared","success")});const _FX_PRESETS={studio:{reverb:{on:!0,room_size:.6,wet:.35},compressor:{on:!0,threshold_db:-24,ratio:3}},broadcast:{compressor:{on:!0,threshold_db:-20,ratio:6}},telephone:{compressor:{on:!0,threshold_db:-18,ratio:8},bandpass:{on:!0,low:300,high:3400}},warm:{reverb:{on:!0,room_size:.2,wet:.15},compressor:{on:!0,threshold_db:-26,ratio:2}},radio:{compressor:{on:!0,threshold_db:-22,ratio:5},bandpass:{on:!0,low:150,high:5500}}};function fxSliderBind(sliderId,labelId,fmt){const s=$(sliderId),l=$(labelId);if(!s||!l)return;const upd=()=>{l.textContent=fmt(s.value)};upd(),s.addEventListener("input",upd)}fxSliderBind("fx-reverb-room","fx-reverb-room-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-reverb-wet","fx-reverb-wet-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-comp-thresh","fx-comp-thresh-val",v=>v+" dB"),fxSliderBind("fx-comp-ratio","fx-comp-ratio-val",v=>v+":1"),fxSliderBind("fx-chorus-rate","fx-chorus-rate-val",v=>parseFloat(v).toFixed(1)+" Hz"),fxSliderBind("fx-chorus-mix","fx-chorus-mix-val",v=>parseFloat(v).toFixed(2)),fxSliderBind("fx-pitch-semi","fx-pitch-semi-val",v=>(parseFloat(v)>=0?"+":"")+v+" st"),fxSliderBind("fx-bandpass-low","fx-bandpass-low-val",v=>Math.round(v)+" Hz"),fxSliderBind("fx-bandpass-high","fx-bandpass-high-val",v=>Math.round(v)+" Hz"),(_W=$("effects-preset"))==null||_W.addEventListener("change",()=>{const preset=_FX_PRESETS[$("effects-preset").value];preset&&(["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-on","fx-bandpass-on"].forEach(id=>{const el=$(id);el&&(el.checked=!1)}),preset.reverb&&($("fx-reverb-on").checked=!!preset.reverb.on,preset.reverb.room_size!=null&&($("fx-reverb-room").value=preset.reverb.room_size),preset.reverb.wet!=null&&($("fx-reverb-wet").value=preset.reverb.wet)),preset.compressor&&($("fx-compressor-on").checked=!!preset.compressor.on,preset.compressor.threshold_db!=null&&($("fx-comp-thresh").value=preset.compressor.threshold_db),preset.compressor.ratio!=null&&($("fx-comp-ratio").value=preset.compressor.ratio)),preset.bandpass&&($("fx-bandpass-on").checked=!!preset.bandpass.on,preset.bandpass.low!=null&&($("fx-bandpass-low").value=preset.bandpass.low),preset.bandpass.high!=null&&($("fx-bandpass-high").value=preset.bandpass.high)),["fx-reverb-room","fx-reverb-wet","fx-comp-thresh","fx-comp-ratio","fx-chorus-rate","fx-chorus-mix","fx-pitch-semi","fx-bandpass-low","fx-bandpass-high"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))}))}),(_X=$("effects-reset-btn"))==null||_X.addEventListener("click",()=>{$("effects-preset").value="",["fx-reverb-on","fx-compressor-on","fx-chorus-on","fx-pitch-on","fx-bandpass-on"].forEach(id=>{const el=$(id);el&&(el.checked=!1)}),$("fx-bandpass-low").value="300",$("fx-bandpass-high").value="3400",$("fx-reverb-room").value="0.35",$("fx-reverb-wet").value="0.25",$("fx-comp-thresh").value="-20",$("fx-comp-ratio").value="4",$("fx-chorus-rate").value="1",$("fx-chorus-mix").value="0.5",$("fx-pitch-semi").value="0",["fx-reverb-room","fx-reverb-wet","fx-comp-thresh","fx-comp-ratio","fx-chorus-rate","fx-chorus-mix","fx-pitch-semi","fx-bandpass-low","fx-bandpass-high"].forEach(id=>{var _a2;return(_a2=$(id))==null?void 0:_a2.dispatchEvent(new Event("input"))})});let _effectsSourceBlob=null;(_Y=$("effects-apply-btn"))==null||_Y.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2;const chain=[];if((_a2=$("fx-reverb-on"))!=null&&_a2.checked&&chain.push({type:"reverb",params:{room_size:+$("fx-reverb-room").value,wet:+$("fx-reverb-wet").value,dry:1-+$("fx-reverb-wet").value}}),(_b2=$("fx-compressor-on"))!=null&&_b2.checked&&chain.push({type:"compressor",params:{threshold_db:+$("fx-comp-thresh").value,ratio:+$("fx-comp-ratio").value}}),(_c2=$("fx-chorus-on"))!=null&&_c2.checked&&chain.push({type:"chorus",params:{rate_hz:+$("fx-chorus-rate").value,mix:+$("fx-chorus-mix").value}}),(_d2=$("fx-pitch-on"))!=null&&_d2.checked&&chain.push({type:"pitch_shift",params:{semitones:+$("fx-pitch-semi").value}}),(_e2=$("fx-bandpass-on"))!=null&&_e2.checked&&(chain.push({type:"highpass",params:{cutoff_hz:+$("fx-bandpass-low").value}}),chain.push({type:"lowpass",params:{cutoff_hz:+$("fx-bandpass-high").value}})),!chain.length){toast("Enable at least one effect","error");return}const btn=$("effects-apply-btn"),st=$("effects-status");btn.disabled=!0,st&&(st.textContent="Processing\u2026");try{let blob=previewBlob||_effectsSourceBlob;if(!blob){const args=window._effectsSynthArgs;if(!args||typeof fetchTtsPreviewBlob!="function"){toast("Generate audio first","error");return}st&&(st.textContent="Synthesizing audio\u2026"),blob=await fetchTtsPreviewBlob(args.voice,args.text,"wav",args.instruct,args.backend)}const fd=new FormData;fd.append("audio",blob,"audio.wav"),fd.append("effects",JSON.stringify(chain));const resp=await fetch("/api/audio/effects",{method:"POST",body:fd});if(!resp.ok){const e=await resp.json().catch(()=>({}));throw new Error(e.detail||resp.statusText)}const out=await resp.blob();_effectsSourceBlob||(_effectsSourceBlob=previewBlob),previewBlob=out;const audio=$("preview-audio");audio.src=URL.createObjectURL(out),audio.style.display="",audio.play().catch(()=>{}),st&&(st.textContent="Applied."),toast("Effects applied","success")}catch(e){st&&(st.textContent=""),toast("Effects failed: "+e.message,"error")}finally{btn.disabled=!1}});let _refineOriginal=null;(function(){const inp=$("refine-llm-url");if(!inp)return;const saved=_appSettings&&_appSettings.refine_llm_url||localStorage.getItem("refine-llm-url");saved?inp.value=saved:inp.value=_appSettings&&_appSettings.llm_url||_appSettings&&_appSettings.engine_local_urls&&_appSettings.engine_local_urls.ollama||localStorage.getItem("llm-local-url-ollama")||"http://localhost:11434/v1",inp.addEventListener("input",()=>{localStorage.setItem("refine-llm-url",inp.value),_patchSettings({refine_llm_url:inp.value}),_appSettings&&(_appSettings.refine_llm_url=inp.value)})})();function updateRefineButtonState(){var _a2,_b2;const btn=$("refine-btn");btn&&(btn.disabled=!((_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)!=null&&_b2.trim()))}(_Z=$("stt-tts-text"))==null||_Z.addEventListener("input",updateRefineButtonState),(__=$("refine-btn"))==null||__.addEventListener("click",async()=>{var _a2,_b2,_c2,_d2,_e2,_f2,_g2,_h2,_i2,_j2,_k2,_l2,_m2,_n2,_o2,_p2;const text=(_b2=(_a2=$("stt-tts-text"))==null?void 0:_a2.value)==null?void 0:_b2.trim();if(!text)return;const btn=$("refine-btn"),st=$("refine-status");btn.disabled=!0,st&&(st.textContent="Refining\u2026");try{const r=await fetch("/api/refine-text",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({text,llm_url:((_d2=(_c2=$("refine-llm-url"))==null?void 0:_c2.value)==null?void 0:_d2.trim())||(_appSettings==null?void 0:_appSettings.llm_url)||"http://localhost:11434/v1",model:((_f2=(_e2=$("refine-model"))==null?void 0:_e2.value)==null?void 0:_f2.trim())||(_appSettings==null?void 0:_appSettings.refine_model)||(_appSettings==null?void 0:_appSettings.llm_model)||"",toggles:{fillers:(_h2=(_g2=$("refine-fillers"))==null?void 0:_g2.checked)!=null?_h2:!0,repetitions:(_j2=(_i2=$("refine-repetitions"))==null?void 0:_i2.checked)!=null?_j2:!0,corrections:(_l2=(_k2=$("refine-corrections"))==null?void 0:_k2.checked)!=null?_l2:!0,punctuation:(_n2=(_m2=$("refine-punctuation"))==null?void 0:_m2.checked)!=null?_n2:!0}})});if(!r.ok){const e=await r.json().catch(()=>({}));throw new Error(e.detail||r.statusText)}const d=await r.json();_refineOriginal=text,$("stt-tts-text").value=d.text,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!1),st&&(st.textContent="Done."),toast("Transcription refined","success")}catch(e){st&&(st.textContent=""),toast("Refinement failed: "+e.message,"error")}finally{btn.disabled=!((_p2=(_o2=$("stt-tts-text"))==null?void 0:_o2.value)!=null&&_p2.trim())}}),(_$=$("refine-restore-btn"))==null||_$.addEventListener("click",()=>{_refineOriginal&&($("stt-tts-text").value=_refineOriginal,_refineOriginal=null,$("refine-restore-btn")&&($("refine-restore-btn").disabled=!0),$("refine-status")&&($("refine-status").textContent=""),toast("Original transcription restored","success"))}),(_aa=$("preview-style-instruction"))==null||_aa.addEventListener("input",()=>{typeof updateBackendHelp=="function"&&updateBackendHelp()}),function(){var _a2;const batchBackendSel=$("batch-backend-select"),batchRunsSel=$("batch-runs"),batchLoadBtn=$("batch-load-voices-btn"),batchSearchInput=$("batch-voice-search"),batchSelectAllBtn=$("batch-select-all-btn"),batchSelectNoneBtn=$("batch-select-none-btn"),batchVoiceList=$("batch-voice-list"),batchSelCount=$("batch-selected-count"),batchRunBtn=$("batch-run-btn"),batchStopBtn=$("batch-stop-btn"),perfRunSelectedBtn=$("perf-run-selected-btn"),batchProgress=$("batch-progress"),batchProgLabel=$("batch-progress-label"),batchProgCount=$("batch-progress-count"),batchProgBar=$("batch-progress-bar"),batchResultsCard=$("batch-results-card"),batchResultsLabel=$("batch-results-label"),batchTbody=$("batch-tbody");if(!batchRunBtn)return;let batchStopped=!1,batchResults=[],batchVoices=[],batchSelected=new Set,batchSortField="factor",batchSortDir=-1;function populateBatchBackends(){if(!batchBackendSel)return;const cur=batchBackendSel.value;batchBackendSel.innerHTML=availableTtsBackends().map(b=>``).join("")||''}populateBatchBackends();function libraryVoice(id){return(window._voices||[]).find(v=>v&&v.id===id)||null}function normalizeBatchVoice(value){const id=typeof value=="string"?value:(value==null?void 0:value.id)||(value==null?void 0:value.voice)||(value==null?void 0:value.name)||(value==null?void 0:value.voice_id)||"",meta=typeof value=="object"?value.meta||libraryVoice(id)||value:libraryVoice(id)||null;return id?{id,label:(meta==null?void 0:meta.display_name)||(value==null?void 0:value.label)||(meta==null?void 0:meta.name)||(value==null?void 0:value.name)||id,meta}:null}function batchVoiceStats(v){var _a3,_b2;const meta=(v==null?void 0:v.meta)||libraryVoice(v==null?void 0:v.id)||{},b=meta.benchmark||{},parts=[],dur=Number(meta.duration),speed=Number((_a3=b.speed)!=null?_a3:b.avg_speed),rtf=Number((_b2=b.rtf)!=null?_b2:b.avg_rtf);return Number.isFinite(dur)&&dur>0&&parts.push(`${dur.toFixed(1)}s`),Number.isFinite(speed)&&speed>0?parts.push(`${speed.toFixed(2)}x`):Number.isFinite(rtf)&&rtf>0&&parts.push(`RTF ${rtf.toFixed(2)}`),parts.join(" \xB7 ")}function batchColor(id){const palette=["#3b82f6","#10b981","#8b5cf6","#f59e0b","#ef4444","#ec4899","#06b6d4","#84cc16"];let h=0;for(let i=0;i>>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``;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`${escHtml(init)}`}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=` ${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='
No voices found. Fetch voices above or reload from backend.
',updateSelCount();return}if(!visible.length){batchVoiceList.innerHTML='
No matching voices.
',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`