- Add Source input to voice inspector panel (saves to origin in meta.json) - Auto-detect fish-audio voices via tag — SOURCE column now shows "fish-audio" for tagged voices even without an explicit origin field - Bulk "Set source" action in the multi-select toolbar - Source cell in table reflects the live-displayed value and updates immediately when changed via the inspector Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
311 lines
21 KiB
HTML
311 lines
21 KiB
HTML
<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…" 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 1: TTS synth mode + preview sentence -->
|
|
<div class="vl-card vl-card-synth">
|
|
<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’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…"
|
|
oninput="var t=document.getElementById('benchmark-sample-text');if(t){t.value=this.value;t.dispatchEvent(new Event('input'));}"
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div><!-- /vl-card-synth -->
|
|
|
|
<!-- Card 2: filters + sort + voice list + toolbar -->
|
|
<div class="vl-card vl-card-list">
|
|
<div class="vl-filters">
|
|
<select id="library-filter-lang" title="Language"><option value="">All</option></select>
|
|
<select id="library-filter-type" title="Type"><option value="">All types</option></select>
|
|
<label class="vl-disabled-label"><input type="checkbox" id="show-disabled-cb"> Disabled</label>
|
|
</div>
|
|
<div class="vl-sort-bar">
|
|
<input type="search" id="library-filter-text" placeholder="🔍 Search voices…" autocomplete="off" class="vl-search vl-search-main">
|
|
<span class="vl-sort-label">Sort</span>
|
|
<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>
|
|
<button id="voice-sort-dir" class="vl-sort-dir-btn" title="Toggle sort direction"><span class="mdi mdi-arrow-up"></span></button>
|
|
<button id="voice-group-tag-btn" class="vl-sort-dir-btn" title="Group into virtual folders by tag"><span class="mdi mdi-folder-outline"></span></button>
|
|
<button id="voice-table-view-btn" class="vl-sort-dir-btn" title="Toggle full-width table view"><span class="mdi mdi-table"></span></button>
|
|
<button id="voice-table-edit-btn" class="vl-sort-dir-btn" title="Toggle table edit mode" style="display:none"><span class="mdi mdi-pencil-box-outline"></span></button>
|
|
</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>
|
|
|
|
<div id="voice-groups-bar" class="voice-groups-bar" hidden></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">0 selected</span>
|
|
<button class="vl-bulk-btn" id="vl-bulk-select-all" title="Select all visible"><span class="mdi mdi-checkbox-multiple-marked-outline"></span> All</button>
|
|
<button class="vl-bulk-btn" id="vl-bulk-deselect" title="Clear selection"><span class="mdi mdi-checkbox-multiple-blank-outline"></span> None</button>
|
|
<div class="vl-bulk-sep"></div>
|
|
<button class="vl-bulk-btn" id="vl-bulk-tag" title="Add / replace tag on selected voices"><span class="mdi mdi-tag-outline"></span> Set tag</button>
|
|
<button class="vl-bulk-btn" id="vl-bulk-source" title="Set source / origin on selected voices"><span class="mdi mdi-database-outline"></span> Set source</button>
|
|
<button class="vl-bulk-btn" id="vl-bulk-hide" title="Disable (hide) selected voices"><span class="mdi mdi-eye-off-outline"></span> Hide</button>
|
|
<button class="vl-bulk-btn" id="vl-bulk-unhide" title="Enable (unhide) selected voices"><span class="mdi mdi-eye-outline"></span> Unhide</button>
|
|
<button class="vl-bulk-btn" id="vl-bulk-rating" title="Set star rating on selected"><span class="mdi mdi-star-outline"></span> Rate</button>
|
|
<div class="vl-bulk-sep"></div>
|
|
<button class="vl-bulk-btn vl-bulk-danger" id="vl-bulk-delete" title="Delete selected voices permanently"><span class="mdi mdi-delete-outline"></span> Delete</button>
|
|
</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" title="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>Type</div>
|
|
<div class="vl-th-sortable" data-sort="benchmark">Speed <span class="mdi mdi-sort"></span></div>
|
|
<div class="vl-th-sortable" data-sort="dbfs">dBFS <span class="mdi mdi-sort"></span></div>
|
|
<div class="vl-th-sortable" data-sort="duration">Length <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="source">Source <span class="mdi mdi-sort"></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="note">Note <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="enabled">Active <span class="mdi mdi-sort"></span></div>
|
|
</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"></span> Refresh</button>
|
|
<button class="btn-secondary vl-tb-btn" id="sync-voice-folders-btn" title="Sync active/hidden folders">Sync</button>
|
|
<button class="btn-secondary vl-tb-btn" id="calculate-db-btn" title="Calculate dBFS">Calc dB</button>
|
|
<button class="btn-secondary vl-tb-btn" id="benchmark-voices-btn" title="Benchmark TTS speed">Benchmark</button>
|
|
<button class="btn-secondary vl-tb-btn" id="copy-active-voices-btn" title="Copy active voice names">Copy active</button>
|
|
<button class="btn-secondary vl-tb-btn" id="precompute-embeddings-btn" title="Warm all active voices so the TTS engine pre-computes & caches each speaker embedding (.pt) — makes first playback instant"><span class="mdi mdi-flash-outline"></span> Precompute</button>
|
|
<button class="btn-secondary vl-tb-btn" id="seed-batch-all-btn" title="Generate & cache Seed Finder samples for every active voice (skips already-cached; resumable)"><span class="mdi mdi-dice-multiple-outline"></span> Batch seeds</button>
|
|
</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 app.js but not shown in new layout -->
|
|
<div id="vl-scaffold" style="display:none" aria-hidden="true">
|
|
|
|
<select id="library-filter-sex"><option value="">All</option><option value="F">F</option><option value="M">M</option><option value="N">N</option></select>
|
|
<select id="library-filter-rating"><option value="">Any</option><option value="5">5 stars</option><option value="4">4+ stars</option><option value="3">3+ stars</option><option value="1">Rated</option><option value="0">Unrated</option></select>
|
|
<button id="library-clear-filters" type="button">Clear filters</button>
|
|
<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>
|
|
<button id="benchmark-reset-sample-btn">Reset sample</button>
|
|
<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 · MP3 · OGG · FLAC · M4A · MP4 · MKV · 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>
|
|
</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ñol</option>
|
|
<option value="FR">Français</option><option value="PT">Portuguê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 → Microphone → Allow, then reload.</li>
|
|
<li><strong>Firefox:</strong> click the lock icon → remove Blocked → Allow, then reload.</li>
|
|
<li><strong>Safari:</strong> Settings → Websites → Microphone → 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">
|
|
<option value="EN">EN</option><option value="DE">DE</option><option value="IT">IT</option>
|
|
<option value="ES">ES</option><option value="FR">FR</option><option value="PT">PT</option>
|
|
<option value="NL">NL</option><option value="PL">PL</option>
|
|
</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 -->
|