tts-voice-creator-clone-and.../static/sections/s-voices.html
mARTin-B78 0551b6651f feat(voices): redesigned My Voices list, working copy, engine-aware TTS availability
My Voices
- Top bar with search, filter toggle (active-count badge), synth-source drawer, group and table buttons;
  filters/sort in a collapsible labelled panel; one-line sort header with select-all/none checkbox
- Groups chip list is a collapsed accordion; bottom bar is Refresh/Sync/Import plus one Actions (N) menu
  (Edit / Audio / Share / Danger), disabled until voices are checked; touch targets stay 44px
- Copy selected works again (copyText was no longer exported), shows "N voice names copied to clipboard"
  and falls back to a copy dialog when the async clipboard is unavailable

TTS / engines
- Health probe works for /v1 card URLs and, behind a LiteLLM gateway, checks the selected model's own
  upstream so stopped containers are not reported as available
- Unhiding a voice publishes a missing entry to the engine's voices.json (no more silent default-voice fallback)
- Status-bar TTS picker also drives the My Voices and STT-test selectors; Use as TTS refreshes the status bar

Includes the engine connect/disconnect, custom TTS, STT footer and magazine auth work that was already
in the working tree, with their tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-11 04:44:27 +02:00

350 lines
26 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div class="section-head">
<span class="section-icon"><span class="mdi mdi-account-voice"></span></span>
<div class="section-title">
<h2>My Voices</h2>
<p>Pick a voice on the left, edit on the right.</p>
</div>
<div class="section-head-actions">
<input type="search" class="section-search" id="section-voices-search" placeholder="Search voices&#x2026;" autocomplete="off"
oninput="var f=document.getElementById('library-filter-text');if(f){f.value=this.value;f.dispatchEvent(new Event('input'));}">
<button class="btn-primary section-new-voice-btn" onclick="document.getElementById('add-new-voice-btn')?.click()">+ New voice</button>
</div>
</div>
<!-- Workbench: list pane + inspector pane -->
<div class="voices-workbench" id="tab-library">
<!-- LEFT: compact voice list — two cards -->
<div class="voices-list-pane">
<!-- Card 2: filters + sort + voice list + toolbar -->
<div class="vl-card vl-card-list">
<!-- Top bar: search + filter toggle + view buttons. Filters/sort live in a collapsible panel. -->
<div class="vl-topbar">
<input type="search" id="library-filter-text" placeholder="Search voices&#x2026;" aria-label="Search voices" autocomplete="off" class="vl-search vl-search-main">
<button id="vl-synth-toggle" class="vl-sort-dir-btn" type="button" aria-expanded="false" aria-controls="vl-synth-drawer" title="Preview text &amp; synth source" aria-label="Preview text and synth source"><span class="mdi mdi-account-voice" aria-hidden="true"></span></button>
<button id="vl-filter-toggle" class="vl-sort-dir-btn vl-filter-toggle" type="button" aria-expanded="false" aria-controls="vl-filter-panel" title="Show filters and sorting"><span class="mdi mdi-filter-variant" aria-hidden="true"></span><span class="vl-filter-badge" id="vl-filter-badge" hidden>0</span><span class="sr-only"> Filters</span></button>
<button id="voice-group-tag-btn" class="vl-sort-dir-btn" type="button" title="Group into virtual folders by tag" aria-label="Group by tag"><span class="mdi mdi-folder-outline" aria-hidden="true"></span></button>
<button id="voice-table-view-btn" class="vl-sort-dir-btn" type="button" title="Toggle full-width table view" aria-label="Table view"><span class="mdi mdi-table" aria-hidden="true"></span></button>
<button id="voice-table-edit-btn" class="vl-sort-dir-btn" type="button" title="Toggle table edit mode" aria-label="Table edit mode" style="display:none"><span class="mdi mdi-pencil-box-outline" aria-hidden="true"></span></button>
</div>
<div class="vl-synth-drawer" id="vl-synth-drawer" hidden>
<div class="vl-synth-panel">
<div class="vl-synth-mode-row">
<span class="vl-synth-icon"><span class="mdi mdi-play"></span><span class="mdi mdi-play"></span></span>
<span class="vl-synth-label">Synth uses</span>
<div class="vl-synth-seg" id="vl-synth-mode-seg">
<button class="vl-synth-seg-btn active" data-mode="preview"
title="Synthesize the preview sentence — good for comparing voices side by side">Preview text</button>
<button class="vl-synth-seg-btn" data-mode="transcript"
title="Synthesize this voice&#x2019;s saved reference transcript — good for quality check vs. original recording">Reference transcript</button>
</div>
</div>
<div class="vl-preview-text-wrap" id="vl-preview-text-wrap">
<div class="vl-preview-row">
<button class="vl-preview-lang-btn" id="vl-preview-lang-btn"
title="English — click to change sample language"
onclick="openPreviewLangPicker(this)" data-lang="EN">
<span class="fi fi-gb"></span>
</button>
<textarea class="vl-preview-input" id="vl-preview-sample" rows="2"
placeholder="Sample sentence for TTS preview&#x2026;"
oninput="var t=document.getElementById('benchmark-sample-text');if(t){t.value=this.value;t.dispatchEvent(new Event('input'));}"
></textarea>
</div>
</div>
</div>
</div>
<div class="vl-filter-panel" id="vl-filter-panel" role="group" aria-label="Filter and sort voices" hidden>
<div class="vl-filter-grid">
<label>Language <select id="library-filter-lang"><option value="">All</option></select></label>
<label>Gender <select id="library-filter-sex"><option value="">All</option></select></label>
<label>Type <select id="library-filter-type"><option value="">All types</option></select></label>
<label>Tag <select id="library-filter-tag"><option value="">All tags</option></select></label>
<label>Rating <select id="library-filter-rating" >
<option value="">All ratings</option>
<option value="5">★★★★★</option>
<option value="4">★★★★+</option>
<option value="3">★★★+</option>
<option value="2">★★+</option>
<option value="1">★+</option>
<option value="0">Unrated</option>
</select></label>
<label>Group <select id="library-filter-group"><option value="">All groups</option></select></label>
<label>Sort by <select id="voice-sort-field" title="Sort field">
<option value="id">Name</option>
<option value="flag">Language</option>
<option value="gender">Gender</option>
<option value="benchmark">Speed</option>
<option value="rating">Rating</option>
<option value="duration">Duration</option>
<option value="dbfs">Volume dB</option>
<option value="source">Source</option>
<option value="seed">Seed</option>
<option value="note">Note</option>
<option value="tag">Tags</option>
<option value="enabled">Active</option>
</select></label>
<label>Order <button id="voice-sort-dir" class="vl-sort-dir-btn vl-sort-dir-wide" type="button" title="Toggle sort direction"><span class="mdi mdi-arrow-up" aria-hidden="true"></span></button></label>
</div>
<div class="vl-filter-actions">
<label class="vl-disabled-label"><input type="checkbox" id="show-disabled-cb"> Show disabled</label>
<button id="library-clear-filters" class="btn-secondary vl-tb-btn" type="button" title="Clear all filters"><span class="mdi mdi-filter-off-outline" aria-hidden="true"></span> Clear filters</button>
</div>
<div id="voice-groups-bar" class="voice-groups-bar" hidden></div>
</div>
<div class="benchmark-confirm" id="benchmark-confirm" hidden role="group" aria-live="polite">
<strong id="benchmark-confirm-title">Benchmark active voices?</strong>
<span id="benchmark-confirm-text"></span>
<button class="btn-secondary" id="benchmark-confirm-cancel" type="button">Cancel</button>
<button class="benchmark-confirm-start" id="benchmark-confirm-start" type="button">Start benchmark</button>
</div>
<!-- Bulk-edit toolbar — appears when voices are selected -->
<div class="vl-bulk-bar" id="vl-bulk-bar" hidden>
<span class="vl-bulk-count" id="vl-bulk-count" role="status">0 selected</span>
</div>
<div class="vl-table-header" id="vl-table-header">
<div style="display:flex; justify-content:center; align-items:center;"><input type="checkbox" id="vl-select-all-header" class="vl-select-all-cb" title="Select all visible voices" aria-label="Select all visible voices"></div>
<div>Img</div>
<div>Play</div>
<div class="vl-th-sortable" data-sort="id">Name <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="flag">Lang <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="gender">Gender <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="factor">Factor <span class="mdi mdi-sort"></span><span class="vl-th-info mdi mdi-information-outline" title="Speed multiplier: audio duration ÷ render time. Above 1.0× means the GPU is faster than real-time — higher is better."></span></div>
<div class="vl-th-sortable" data-sort="wpm">WPM <span class="mdi mdi-sort"></span><span class="vl-th-info mdi mdi-information-outline" title="Words per minute the voice speaks. 130–180 wpm is natural for audiobooks; lower = slower, more deliberate pace."></span></div>
<div class="vl-th-sortable" data-sort="seed">Seed <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="dbfs">dBFS <span class="mdi mdi-sort"></span></div>
<div>Type</div>
<div class="vl-th-sortable" data-sort="source">Source <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="rating">Rating <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="tag">Tags <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="note">Note <span class="mdi mdi-sort"></span></div>
<div class="vl-th-sortable" data-sort="enabled">Active <span class="mdi mdi-sort"></span></div>
</div>
<div class="vl-compact-header" id="vl-compact-header" role="group" aria-label="Sort voices">
<input type="checkbox" class="vl-select-all-cb" title="Select all / none of the visible voices" aria-label="Select all visible voices">
<button type="button" class="vl-th-sortable" data-sort="id">Name <span class="mdi mdi-sort" aria-hidden="true"></span></button>
<button type="button" class="vl-th-sortable" data-sort="flag">Lang <span class="mdi mdi-sort" aria-hidden="true"></span></button>
<button type="button" class="vl-th-sortable" data-sort="gender">Gender <span class="mdi mdi-sort" aria-hidden="true"></span></button>
<button type="button" class="vl-th-sortable" data-sort="benchmark">Speed <span class="mdi mdi-sort" aria-hidden="true"></span></button>
<button type="button" class="vl-th-sortable" data-sort="rating">Rating <span class="mdi mdi-sort" aria-hidden="true"></span></button>
</div>
<div id="voice-list" tabindex="0" role="region" aria-label="Voice library"></div>
<div class="vl-toolbar">
<button class="btn-secondary vl-tb-btn" id="refresh-voices-btn" title="Refresh voice list"><span class="mdi mdi-refresh" aria-hidden="true"></span><span class="vl-tb-label"> Refresh</span></button>
<button class="btn-secondary vl-tb-btn" id="sync-voice-folders-btn" title="Sync active/hidden folders"><span class="mdi mdi-sync" aria-hidden="true"></span><span class="vl-tb-label"> Sync</span></button>
<button class="btn-secondary vl-tb-btn" id="import-voices-btn" title="Import a .voice.zip bundle; an existing voice is never overwritten"><span class="mdi mdi-import" aria-hidden="true"></span><span class="vl-tb-label"> Import bundle</span></button>
<input type="file" id="import-voices-input" accept=".zip,application/zip" multiple hidden>
<div class="vl-actions">
<button type="button" class="btn-primary vl-actions-btn" id="vl-actions-btn" aria-expanded="false" aria-controls="vl-actions-menu" disabled title="Check one or more voices to enable actions"><span class="mdi mdi-lightning-bolt-outline" aria-hidden="true"></span> Actions <span class="vl-actions-n" id="vl-actions-n"></span> <span class="mdi mdi-chevron-up" aria-hidden="true"></span></button>
<div class="vl-actions-menu" id="vl-actions-menu" role="group" aria-label="Actions on the checked voices" hidden>
<div class="vl-menu-title">Edit</div>
<button class="vl-menu-item" id="vl-bulk-tag" title="Add a tag to the selected voices, keeping their existing tags"><span class="mdi mdi-tag-outline" aria-hidden="true"></span> Set tag</button>
<button class="vl-menu-item" id="vl-bulk-source" title="Set source / origin on selected voices"><span class="mdi mdi-database-outline" aria-hidden="true"></span> Set source</button>
<button class="vl-menu-item" id="vl-bulk-rating" title="Set star rating on selected"><span class="mdi mdi-star-outline" aria-hidden="true"></span> Rate</button>
<button class="vl-menu-item" id="vl-bulk-hide" title="Disable (hide) selected voices"><span class="mdi mdi-eye-off-outline" aria-hidden="true"></span> Hide</button>
<button class="vl-menu-item" id="vl-bulk-unhide" title="Enable (unhide) selected voices"><span class="mdi mdi-eye-outline" aria-hidden="true"></span> Unhide</button>
<div class="vl-menu-title">Audio</div>
<button class="vl-menu-item" id="precompute-embeddings-btn" title="Warm all active voices so the TTS engine pre-computes &amp; caches each speaker embedding (.pt) — makes first playback instant"><span class="mdi mdi-flash-outline"></span> Precompute</button>
<button class="vl-menu-item" id="calculate-db-btn" title="Calculate dBFS"><span class="mdi mdi-sine-wave" aria-hidden="true"></span> Calc dB</button>
<button class="vl-menu-item" id="benchmark-voices-btn" title="Benchmark TTS speed"><span class="mdi mdi-speedometer" aria-hidden="true"></span> Benchmark</button>
<button class="vl-menu-item" id="seed-batch-all-btn" title="Generate &amp; cache Seed Finder samples for every active voice (skips already-cached; resumable)"><span class="mdi mdi-dice-multiple-outline"></span> Batch seeds</button>
<button class="vl-menu-item" id="verify-voices-stt-btn" title="Synthesize a test sentence with each voice, transcribe it back with Whisper, and compare to the original text — catches repeated/garbled/nonsense output that a duration-only benchmark misses"><span class="mdi mdi-check-decagram-outline"></span> Verify (STT)</button>
<div class="vl-menu-title">Share</div>
<button class="vl-menu-item" id="copy-active-voices-btn" title="Copy the names of the checked voices, comma separated (e.g. for SillyTavern)"><span class="mdi mdi-content-copy" aria-hidden="true"></span> Copy selected</button>
<button class="vl-menu-item" id="export-voices-btn" title="Export the checked voices as .voice.zip bundles (backup / share)"><span class="mdi mdi-export" aria-hidden="true"></span> Export selected</button>
<div class="vl-menu-title">Danger</div>
<button class="vl-menu-item vl-bulk-danger" id="vl-bulk-delete" title="Delete selected voices permanently"><span class="mdi mdi-delete-outline" aria-hidden="true"></span> Delete</button>
</div>
</div>
</div>
<div class="vl-footer">
<span id="voice-count" class="note"></span>
<button class="vl-add-btn" id="add-new-voice-btn" style="display:none" aria-hidden="true" tabindex="-1" title="Add a new voice to the library">+ Add voice</button>
</div>
</div><!-- /vl-card-list -->
</div><!-- /voices-list-pane -->
<!-- RIGHT: inspector / editor pane -->
<div class="voices-inspector-pane" id="voices-inspector">
<div class="inspector-placeholder">
<span><span class="mdi mdi-account-voice"></span></span>
<p>Pick a voice on the left<br>to edit it here</p>
</div>
</div>
</div><!-- /voices-workbench -->
<!-- Hidden scaffold — IDs required by voice-library.js but not shown in new layout -->
<div id="vl-scaffold" style="display:none" aria-hidden="true">
<div id="library-filter-panel"></div>
<div class="library-insights" id="library-insights"></div>
<div class="disabled-info" id="disabled-info">
<strong>About disabled voices:</strong> The Active toggle moves the voice package between
<code>active_voices</code> and <code>hidden_voices</code>. Qwen3-TTS should scan only <code>active_voices</code>.
</div>
<!-- Audio playback bar -->
<div id="lib-audio-bar">
<div class="lib-audio-label" id="lib-audio-label">-</div>
<audio id="lib-audio" controls></audio>
</div>
<!-- Benchmark config panel -->
<div class="library-benchmark-panel">
<textarea id="benchmark-sample-text" placeholder="Sample sentence for benchmark"></textarea>
<select id="library-tts-backend-select"><option value="">Checking backends...</option></select>
<input type="number" id="library-target-db" value="-20" min="-60" max="-1" step="0.5">
<button id="normalize-volume-btn">Normalize volume</button>
<select id="benchmark-preset-select" title="Load a benchmark sentence preset">
<option value="">Sentence presets…</option>
<option value="de">DE — Die Welt ist voller Geschichten…</option>
<option value="en">EN — The old lighthouse stood firm…</option>
<option value="de2">DE — Der schnelle braune Fuchs…</option>
<option value="en2">EN — She sells seashells…</option>
<option value="reset">↺ Reset to default</option>
</select>
<button id="benchmark-use-preview-btn">Use preview text</button>
</div>
<!-- Benchmark progress -->
<div class="benchmark-progress" id="benchmark-progress" hidden aria-live="polite">
<div class="benchmark-progress-head">
<span id="benchmark-progress-label">Benchmarking voices...</span>
<span id="benchmark-progress-count">0 / 0</span>
</div>
<div class="benchmark-progress-track" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0">
<div id="benchmark-progress-bar"></div>
</div>
<div class="benchmark-live-stats" id="benchmark-live-stats">
<span>Elapsed -</span><span>Avg -</span><span>ETA -</span><span>OK 0</span><span>Slow 0</span><span>Errors 0</span>
</div>
<div class="benchmark-live-last" id="benchmark-live-last"></div>
</div>
<!-- Add new voice panel -->
<div class="lib-add-panel" id="lib-add-panel">
<div class="lib-add-stack">
<div class="lib-add-section">
<h2 style="margin-bottom:10px">1 Load or record source</h2>
<div class="lib-add-input-grid">
<div class="lib-add-import-box">
<input type="file" id="lib-add-file" accept="audio/*,video/*" style="display:none">
<div class="lib-add-drop" id="lib-add-drop">
<strong>Drop an audio / video file here</strong>
<span>WAV &middot; MP3 &middot; OGG &middot; FLAC &middot; M4A &middot; MP4 &middot; MKV &middot; WEBM</span>
<span>or click to browse</span>
</div>
<div>
<label class="note" for="lib-add-url">Audio or video URL</label>
<div class="url-row">
<input type="text" id="lib-add-url" placeholder="Paste a direct MP3/WAV, YouTube, or Aiartes URL">
<button class="btn-primary" id="lib-add-url-btn">Download URL</button>
</div>
<div class="btn-row">
<button class="btn-secondary" id="lib-iso-btn" hidden>Remove background music</button>
<button class="btn-secondary" id="lib-iso-toggle" hidden></button>
</div>
<div class="note" id="lib-iso-msg" role="status" aria-live="polite"></div>
</div>
</div>
<div class="lib-add-source-box lib-add-record-box">
<div class="record-head">
<span class="note">Record a fresh sample</span>
<div class="record-head-actions">
<button class="btn-secondary" id="lib-add-mic-help-btn">How to unlock mic</button>
</div>
</div>
<div class="lib-add-recorder-tools">
<div class="lib-add-mic-row">
<div class="mic-buttons">
<button class="btn-secondary" id="lib-add-monitor-btn">Check level</button>
<button class="btn-secondary" id="lib-add-monitor-stop" disabled>Stop monitor</button>
<button class="btn-red" id="lib-add-rec-start"><span class="mdi mdi-record-circle-outline"></span> Record</button>
<button class="btn-secondary" id="lib-add-rec-stop" disabled><span class="mdi mdi-stop-circle-outline"></span> Stop</button>
<span class="mic-timer" id="lib-add-rec-time">0:00</span>
</div>
<div class="mic-monitor-box">
<div class="mic-monitor-head">
<span>Input level</span>
<span class="meter-readout" id="lib-add-db-readout">-<span class="mdi mdi-infinity"></span> dB</span>
</div>
<div class="mic-meter" id="lib-add-mic-meter" aria-hidden="true"></div>
<div class="mic-gain-row">
<label for="lib-add-mic-gain">Mic gain</label>
<input id="lib-add-mic-gain" type="range" min="0" max="2" step="0.05" value="1">
<span class="mic-gain-value" id="lib-add-mic-gain-value">1.00x</span>
</div>
<div class="note" style="margin-top:5px">Best peaks: -18 to -9 dB, never red.</div>
</div>
</div>
<div class="sample-read-box">
<div class="sample-head">
<label for="lib-add-sample-lang">Read sample</label>
<select id="lib-add-sample-lang">
<option value="EN">English</option><option value="DE">Deutsch</option>
<option value="IT">Italiano</option><option value="ES">Espa&ntilde;ol</option>
<option value="FR">Fran&ccedil;ais</option><option value="PT">Portugu&ecirc;s</option>
<option value="NL">Nederlands</option><option value="PL">Polski</option>
</select>
</div>
<textarea class="sample-sentence" id="lib-add-sample-text" spellcheck="true"></textarea>
<div class="sample-actions">
<button class="btn-secondary" id="lib-add-use-sample">Use as transcript</button>
<button class="btn-secondary" id="lib-add-reset-sample">Reset sentence</button>
</div>
</div>
</div>
<div class="mic-help-panel" id="lib-add-mic-help">
<strong>If the microphone is blocked:</strong>
<ul>
<li><strong>Chrome/Brave/Edge:</strong> click the lock icon in the address bar &rarr; Microphone &rarr; Allow, then reload.</li>
<li><strong>Firefox:</strong> click the lock icon &rarr; remove Blocked &rarr; Allow, then reload.</li>
<li><strong>Safari:</strong> Settings &rarr; Websites &rarr; Microphone &rarr; allow this site.</li>
</ul>
</div>
<div class="opt-status" id="lib-add-status">Load a file, paste a URL, or record. Then trim, name, and save.</div>
</div>
</div>
<div class="lib-add-audio-row">
<audio id="lib-add-audio" controls style="display:none"></audio>
<div class="btn-row">
<button class="btn-secondary" id="lib-add-auto-trim">Auto trim</button>
<button class="btn-primary" id="lib-add-save-crop">Crop selection</button>
<button class="btn-secondary" id="lib-add-play">Play crop</button>
</div>
</div>
<canvas class="opt-wave" id="lib-add-wave" style="display:none;margin-top:10px"></canvas>
<div class="opt-controls">
<div class="opt-field"><label>Start</label><input id="lib-add-start" type="number" step="0.01" value="0"></div>
<div class="opt-field"><label>End</label><input id="lib-add-end" type="number" step="0.01" value="0"></div>
<button class="btn-primary" id="lib-add-save-crop-bottom">Crop selection</button>
<span class="crop-duration-hint" id="lib-add-crop-hint">Select 3-20 seconds for best cloning.</span>
</div>
</div>
<div class="lib-add-section">
<h2 style="margin-bottom:10px">2 Name and save voice</h2>
<div class="lib-add-name-grid">
<div class="opt-field"><label>Language</label><select id="lib-add-lang" data-langsel data-default="EN" aria-label="Voice language" title="Search by name or code; regional accents use BCP 47 subtags (EN-GB, EN-AU)"></select></div>
<div class="opt-field"><label>Gender</label><input id="lib-add-gender" value="N"></div>
<div class="opt-field"><label>Voice ID</label><input id="lib-add-voice-id" placeholder="EN_N_NewVoice"></div>
</div>
<textarea id="lib-add-transcript" placeholder="Reference transcript" style="margin-top:10px"></textarea>
<div class="btn-row" style="margin-top:10px">
<button class="btn-secondary" id="lib-add-recognize">Recognise text</button>
<button class="btn-green" id="lib-add-save">Save voice</button>
</div>
</div>
</div>
</div>
</div><!-- /vl-scaffold -->