Root cause: VERSION was not volume-mounted, so the server always reported 1.2.0, causing browsers to serve 1-year-immutable cached JS even after code changes. Fixes: - docker-compose.yml: add ./VERSION:/app/VERSION:ro volume mount. After `docker compose up -d`, the server reads the current VERSION file and JS is cache-busted by the correct version string. - loader.js: append session timestamp to _appVersion so every page load generates a unique JS URL. JS is always fresh regardless of whether VERSION is current, at the cost of one network round-trip per file per session (acceptable for a local tool). - s-clone.html: embed EN default text directly in the textarea so the field is never empty even before JS runs. - voice-clone.js: remove the 'skip if already filled' guard in initCloneSampleText so navigating back always resets to the language text; call refreshSttBackends on load with sttReady event fallback. - stt.js: dispatch 'sttReady' event after all STT listeners are wired. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
155 lines
8.9 KiB
HTML
155 lines
8.9 KiB
HTML
<div class="section-head">
|
||
<span class="section-icon"><span class="mdi mdi-microphone-variant"></span></span>
|
||
<div class="section-title">
|
||
<h2>Clone a Voice</h2>
|
||
<p>Upload or record 3-20 seconds of audio, trim it, then save it as a reusable voice clone.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Step 1: Load audio -->
|
||
<div class="tab-content" id="tab-source">
|
||
<div class="card">
|
||
<h2>Step 1 — Load audio</h2>
|
||
<p class="card-subtitle">Drop a file, paste a URL, or record directly from your microphone.</p>
|
||
<div id="drop-zone">
|
||
<strong>Drop an audio / video file here</strong>
|
||
<span>WAV · MP3 · OGG · FLAC · M4A · MP4 · MKV · WEBM</span>
|
||
<span style="margin-top:10px;font-size:12px;color:var(--subtext)">or click to browse</span>
|
||
<input type="file" id="file-input" accept=".wav,.mp3,.ogg,.opus,.flac,.m4a,.mp4,.mkv,.webm,.mov,.avi,audio/*,video/*">
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>YouTube / URL</h2>
|
||
<p class="card-subtitle">Download audio from a direct link or YouTube video.</p>
|
||
<div class="url-row">
|
||
<input type="text" id="yt-url" placeholder="https://www.youtube.com/watch?v=...">
|
||
<button class="btn-primary" id="yt-btn">Download</button>
|
||
</div>
|
||
<div id="yt-progress"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Microphone</h2>
|
||
<p class="card-subtitle">Record a fresh sample with live input level monitoring.</p>
|
||
<div class="sample-read-box" style="margin-bottom:12px">
|
||
<div class="sample-head">
|
||
<label for="clone-sample-lang">Read aloud</label>
|
||
<select id="clone-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="clone-sample-text" spellcheck="true">Hello! My name is Sam, and this is my voice. I can speak softly or with great strength. The crisp winter air, warm firelight, and the gentle sound of rain — these are the things I love. Can you hear how clearly I speak?</textarea>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn-secondary" id="clone-monitor-btn"><span class="mdi mdi-waveform"></span> Check level</button>
|
||
<button class="btn-secondary" id="clone-monitor-stop" disabled>Stop monitor</button>
|
||
<button class="btn-red" id="rec-start-btn"><span class="mdi mdi-record-circle-outline"></span> Record</button>
|
||
<button class="btn-secondary" id="rec-stop-btn" disabled><span class="mdi mdi-stop-circle-outline"></span> Stop</button>
|
||
<div id="rec-indicator"><div id="rec-dot"></div><span id="rec-time">0:00</span></div>
|
||
</div>
|
||
<div class="mic-monitor-box" id="clone-monitor-box" style="margin-top:10px">
|
||
<div class="mic-monitor-head">
|
||
<span>Input level</span>
|
||
<span class="meter-readout" id="clone-db-readout">-∞ dB</span>
|
||
</div>
|
||
<div class="mic-meter" id="clone-mic-meter" aria-hidden="true"></div>
|
||
<canvas id="clone-live-wave" class="mic-live-wave" width="300" height="48" aria-hidden="true"></canvas>
|
||
<div class="mic-gain-row">
|
||
<label for="clone-mic-gain">Mic gain</label>
|
||
<input id="clone-mic-gain" type="range" min="0" max="2" step="0.05" value="1">
|
||
<span class="mic-gain-value" id="clone-mic-gain-value">1.00x</span>
|
||
</div>
|
||
<div class="note" style="margin-top:4px">Best peaks: −18 to −9 dB. Never red.</div>
|
||
</div>
|
||
</div>
|
||
<div class="card" id="waveform-card" style="display:none">
|
||
<h2>Step 2 — Trim selection</h2>
|
||
<p class="card-subtitle">Select the best 3–20 s of clean speech from the waveform.</p>
|
||
<div id="waveform-wrap"><div id="waveform"></div></div>
|
||
<div class="time-row">
|
||
<label>Start (s) <input type="number" id="trim-start" value="0" min="0" step="0.1"></label>
|
||
<label>End (s) <input type="number" id="trim-end" value="0" min="0" step="0.1"></label>
|
||
<span id="trim-duration" class="dur-ok">0.0 s</span>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn-primary" id="trim-btn">Confirm trim → Name & Save</button>
|
||
<button class="btn-secondary" id="auto-trim-btn">Auto trim</button>
|
||
<button class="btn-secondary" id="play-btn"><span class="mdi mdi-play"></span> Play</button>
|
||
<button class="btn-secondary" id="play-selection-btn"><span class="mdi mdi-play"></span> Play selection</button>
|
||
</div>
|
||
</div>
|
||
</div><!-- /tab-source -->
|
||
|
||
<!-- Step 3: Name and save -->
|
||
<div class="tab-content" id="tab-save">
|
||
<div class="card">
|
||
<h2>Step 3 — Name the voice</h2>
|
||
<p class="card-subtitle">Assign a unique ID, language, and gender — e.g. <code>EN_F_Anna</code>.</p>
|
||
<div class="voice-id-row">
|
||
<input type="text" id="voice-id-input" placeholder="e.g. EN_F_Anna or MyCustomVoice" spellcheck="false">
|
||
</div>
|
||
<div id="voice-id-hint" class="note" style="min-height:1.2em"></div>
|
||
<details class="helper">
|
||
<summary>Build from parts (LANG · GENDER · Name helper)</summary>
|
||
<div class="helper-body">
|
||
<div class="field">
|
||
<label>Language</label>
|
||
<select id="lang-select">
|
||
<option>EN</option><option>DE</option><option>ZH</option><option>FR</option>
|
||
<option>ES</option><option>JA</option><option>KO</option><option>IT</option>
|
||
<option>PT</option><option>RU</option><option>AR</option><option>PL</option>
|
||
<option>NL</option><option>SV</option><option>TR</option><option>HI</option>
|
||
</select>
|
||
</div>
|
||
<div class="field">
|
||
<label>Gender</label>
|
||
<select id="gender-select">
|
||
<option value="F">F — Female</option>
|
||
<option value="M">M — Male</option>
|
||
<option value="N">N — Neutral</option>
|
||
</select>
|
||
</div>
|
||
<div class="field" style="flex:1;min-width:120px">
|
||
<label>Name (no spaces)</label>
|
||
<input type="text" id="name-input" placeholder="Anna">
|
||
</div>
|
||
<button class="btn-secondary" id="helper-apply-btn" style="align-self:flex-end">Apply →</button>
|
||
</div>
|
||
</details>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Transcript (reference text)</h2>
|
||
<p class="card-subtitle">The spoken text that matches this audio — type it or auto-transcribe.</p>
|
||
<div class="btn-row" style="gap:6px;align-items:center;flex-wrap:wrap;margin-bottom:8px">
|
||
<label style="font-size:13px;color:var(--subtext);white-space:nowrap">Recognition engine:</label>
|
||
<select id="clone-stt-backend" style="flex:1;min-width:160px"><option value="configured">Configured Whisper/STT</option></select>
|
||
<button class="btn-secondary" id="clone-refresh-stt-btn" type="button">Refresh</button>
|
||
</div>
|
||
<div class="btn-row">
|
||
<button class="btn-secondary" id="transcribe-btn"><span class="mdi mdi-message-text-outline"></span> Auto-transcribe</button>
|
||
<span id="transcribe-status" style="font-size:13px;color:var(--subtext)"></span>
|
||
</div>
|
||
<textarea id="transcript-area" placeholder="Type or auto-transcribe the spoken text…"></textarea>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Audio preview</h2>
|
||
<p class="card-subtitle">Play back the trimmed audio before saving to the library.</p>
|
||
<audio id="trim-audio" controls style="display:none"></audio>
|
||
<p id="no-audio-hint" class="note">No audio loaded yet. Load a file above (Step 1) or use Voice Design below.</p>
|
||
</div>
|
||
<div class="card">
|
||
<h2>Step 4 — Save to library</h2>
|
||
<p class="card-subtitle">Write the cloned voice to your active voices folder.</p>
|
||
<div class="btn-row">
|
||
<button class="btn-green" id="save-btn"><span class="mdi mdi-memory"></span> Save to Voice Library</button>
|
||
</div>
|
||
<div id="save-result" class="note" style="display:none"></div>
|
||
</div>
|
||
</div><!-- /tab-save -->
|