tts-voice-creator-clone-and.../static/sections/s-clone.html
mARTin-B78 c91aaabe3a
Some checks are pending
Test and SonarQube / quality (push) Waiting to run
fix(mobile): retry failed script loads; fix 8 phone-width layout bugs
Two reports from the same mobile session tied together: a voice-library
crash ("fmtDbfs is not defined") and a save button reporting "No audio
ready" despite a file being correctly loaded and trimmed. Root cause
was the same in both -- the module loader fetched each JS file once
with no retry, so a single dropped request on a flaky connection left
that module's functions/variables undefined while every other module
kept executing anyway, producing confusing symptoms nowhere near the
real cause.

loader.js: _load() now retries up to 3 times with backoff before
rejecting. If a module still can't load after that, the app says so
plainly via a toast (or a plain DOM banner if even utils.js failed to
load) instead of silently revealing a partially-broken page. Wired into
both the core-module fallback path and ensureFeatureModules' lazy path.

Loading every section of the app at a 375px viewport and checking for
elements running past the viewport edge (all now 0) found eight real,
independent layout bugs, each with a distinct root cause:

- .section-head's mobile wrap rule targeted .section-header, a class
  nothing in the app uses -- dead since it was written. Every section's
  icon/title/action row (e.g. "+ New voice") ran off-screen unreachable.
- The synth-mode toggle and Clone-a-Voice source tabs' icon-only mobile
  rules matched a <span> wrapper the markup didn't have. Added it, plus
  aria-labels so hiding the text doesn't erase the accessible name.
- voice-design.css loads after style.css and re-declares
  .design-sample-grid unscoped -- style.css's mobile override always
  lost the cascade regardless of viewport. Moved the override to the
  file that actually wins.
- Three separate minmax(380-440px,1fr) engine-card grids couldn't fit
  one column under ~460px. Collapsed to 1fr with min-width:0 on the
  items -- the column fix alone wasn't enough, since a grid item's
  content-based min-width is still a floor even inside a 1fr track.
- The routing test row (450px+ minimum) and the conversation input bar
  (a single non-wrapping flex row for text field + mic + gate + three
  toggles) now wrap under 767px.
- Try It Out's EQ sliders shrank to fit beside their toggle instead of
  ever wrapping below it, overflowing their own squeezed container.
  Forced onto their own full-width line.

Verified with a Playwright sweep of all 16 sections (0 overflowing
elements, 0 console errors) plus the existing 225-test suite and the
frontend syntax gate.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-13 17:09:24 +02:00

179 lines
12 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-microphone-variant"></span></span>
<div class="section-title">
<h2>Clone a Voice</h2>
<p>Capture 3–20 seconds of clean speech, trim it, name it, then save it as a reusable voice clone.</p>
</div>
</div>
<!-- Audio source — integrated tabs (record / upload / link) -->
<div class="tab-content" id="tab-source">
<div class="clone-tabbox">
<div class="clone-src-picker" id="clone-src-picker" role="tablist">
<button class="clone-src-tab active" data-src="mic" role="tab" aria-label="Microphone"><span class="mdi mdi-microphone"></span> <span>Microphone</span></button>
<button class="clone-src-tab" data-src="file" role="tab" aria-label="Upload file"><span class="mdi mdi-file-music-outline"></span> <span>Upload file</span></button>
<button class="clone-src-tab" data-src="url" role="tab" aria-label="URL / YouTube"><span class="mdi mdi-link-variant"></span> <span>URL / YouTube</span></button>
</div>
<div class="clone-srcpanel">
<!-- Microphone -->
<div class="clone-src-card" id="clone-src-mic" data-src="mic">
<p class="clone-src-intro"><span class="mdi mdi-microphone"></span> Record a fresh sample with live input-level monitoring.</p>
<div class="field" style="max-width:320px">
<label for="clone-your-name">Voice name</label>
<input type="text" id="clone-your-name" placeholder="e.g. Anna" spellcheck="false" autocomplete="off">
<p class="note">Sets the voice name and is dropped into the sentence below.</p>
</div>
<div class="sample-read-box" style="margin-top:12px">
<div class="sample-head">
<label for="clone-sample-lang">Read this 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" aria-label="Sample sentence to read aloud" 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>
<p class="note" style="margin-top:6px"><span class="mdi mdi-pencil-outline"></span> Edit the sentence freely before recording.</p>
</div>
<div class="btn-row" style="margin-top:12px">
<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>
<!-- Upload file -->
<div class="clone-src-card" id="clone-src-file" data-src="file" hidden>
<p class="clone-src-intro"><span class="mdi mdi-file-music-outline"></span> Drop an audio or video file, or click to browse.</p>
<div id="drop-zone">
<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 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>
<!-- URL / YouTube -->
<div class="clone-src-card" id="clone-src-url" data-src="url" hidden>
<p class="clone-src-intro"><span class="mdi mdi-link-variant"></span> Paste a direct audio link or a YouTube URL to grab the audio.</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><!-- /clone-srcpanel -->
</div><!-- /clone-tabbox -->
<!-- Trim (appears once audio is loaded) -->
<div class="card" id="waveform-card" style="display:none">
<h2><span class="mdi mdi-content-cut"></span> Trim your sample</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) &nbsp;<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 &rarr; Name &amp; 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 -->
<!-- Name, transcribe, preview & save -->
<div class="tab-content" id="tab-save">
<div class="card">
<h2><span class="mdi mdi-account-voice"></span> Name your voice</h2>
<p class="card-subtitle">Choose language &amp; gender and type a name — the ID builds automatically (e.g. <code>EN_F_Anna</code>). You can also type a custom ID.</p>
<div class="clone-id-row">
<div class="field">
<label for="lang-select">Language</label>
<select id="lang-select" 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="field">
<label for="gender-select">Gender</label>
<select id="gender-select" aria-label="Voice gender">
<option value="F">F &mdash; Female</option>
<option value="M">M &mdash; Male</option>
<option value="N">N &mdash; Neutral</option>
</select>
</div>
<div class="field" style="flex:1;min-width:140px">
<label>Name (no spaces)</label>
<input type="text" id="name-input" placeholder="Anna">
</div>
<button class="btn-secondary" id="helper-apply-btn" hidden>Apply &rarr;</button>
</div>
<div class="field" style="margin-top:14px">
<label for="voice-id-input">Final voice ID</label>
<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>
</div>
</div>
<div class="card">
<h2><span class="mdi mdi-message-text-outline"></span> Reference transcript</h2>
<p class="card-subtitle">The exact words spoken in the audio — type them or auto-transcribe.</p>
<div class="engine-setup-row" style="padding:10px 12px; margin-bottom:12px">
<div class="engine-setup-col no-border" style="flex:1">
<label class="engine-setup-label"><span class="mdi mdi-microphone"></span> Speech to Text <a href="#" class="engine-setup-link" onclick="navTo('s-llms'); navEnginesCat('stt'); return false;" title="Configure Speech to Text settings"><span class="mdi mdi-cog-outline"></span></a></label>
<div class="engine-setup-controls">
<select id="clone-stt-backend" aria-label="Speech-to-text recognition engine"><option value="configured">Configured Whisper/STT</option></select>
<button class="btn-secondary" id="clone-refresh-stt-btn" type="button"><span class="mdi mdi-refresh"></span></button>
</div>
</div>
</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&hellip;"></textarea>
</div>
<div class="card">
<h2><span class="mdi mdi-play-circle-outline"></span> Preview</h2>
<p class="card-subtitle">Listen to the trimmed audio before saving.</p>
<audio id="trim-audio" controls style="display:none"></audio>
<p id="no-audio-hint" class="note">No audio yet — record or load a sample above, or use Voice Design.</p>
</div>
<div class="card">
<h2><span class="mdi mdi-content-save-outline"></span> Save to library</h2>
<p class="card-subtitle">Write the cloned voice to your active voices folder.</p>
<div class="btn-row" style="align-items:center;gap:12px">
<button class="btn-green" id="save-btn"><span class="mdi mdi-memory"></span> Save to Voice Library</button>
<label class="chunk-toggle-label" style="font-size:12px;gap:5px" title="Save automatically once audio, ID and transcript are all ready (re-saves when you change details)">
<input type="checkbox" id="clone-autosave-toggle" checked>
<span>Auto-save when ready</span>
</label>
</div>
<div id="save-result" class="note" style="display:none"></div>
</div>
</div><!-- /tab-save -->