tts-voice-creator-clone-and.../static/js/rehearser-voice-picker.js

78 lines
5.0 KiB
JavaScript

// ── Rehearser voice-picker presentation ─────────────────────────────────────
// Matching/import actions remain in rehearser.js for now; this module owns the
// reusable preview state and HTML for the online/local voice choices.
let _rehAudEl = null, _rehAudBtn = null, _rehAudObjectUrl = null;
function _rehStopAudition() {
if (_rehAudEl) { try { _rehAudEl.pause(); } catch (_) {} }
if (_rehAudObjectUrl) { URL.revokeObjectURL(_rehAudObjectUrl); _rehAudObjectUrl = null; }
if (_rehAudBtn) { _rehAudBtn.classList.remove('playing'); const i = _rehAudBtn.querySelector('.mdi'); if (i) i.className = 'mdi mdi-play'; }
_rehAudBtn = null;
}
function _rehAudition(url, btn) {
if (!url) { toast('No preview audio for this voice', 'error'); return; }
if (_rehAudBtn === btn && _rehAudEl && !_rehAudEl.paused) { _rehStopAudition(); return; }
_rehStopAudition();
if (!_rehAudEl) { _rehAudEl = new Audio(); _rehAudEl.addEventListener('ended', _rehStopAudition); }
_rehAudEl.src = url;
_rehAudEl.play().then(() => {
_rehAudBtn = btn; btn.classList.add('playing');
const i = btn.querySelector('.mdi'); if (i) i.className = 'mdi mdi-stop';
}).catch(() => toast('Could not play preview', 'error'));
}
const _REH_PREVIEW_TEXT = 'Hello — this is how this voice sounds.';
const _rehSearchCache = {}; // sp -> last fish.audio search results (simplified candidates)
function _rehVoiceRow({ name, meta, playUrl, playVoice, isCurrent, useAttrs }) {
const play = `<button type="button" class="reh-vo-play"${playUrl ? ` data-url="${escHtml(playUrl)}"` : ''}${playVoice ? ` data-voice="${escHtml(playVoice)}"` : ''} title="Preview voice"><span class="mdi mdi-play"></span></button>`;
const right = isCurrent
? `<span class="reh-vo-badge"><span class="mdi mdi-check-circle"></span> Selected</span>`
: `<button type="button" class="btn-secondary btn-sm reh-vo-use"${useAttrs || ''}>Use this</button>`;
return `<div class="reh-vo-row${isCurrent ? ' current' : ''}">${play}
<div class="reh-vo-info"><span class="reh-vo-name">${escHtml(name)}</span><span class="reh-vo-meta">${escHtml(meta)}</span></div>
${right}</div>`;
}
function castOnlinePanelHtml(sp, c) {
const o = c.online;
if (!o || !Array.isArray(o.candidates) || c.voice === 'me') return '';
const curCand = o.candidates.find(x => x.voice_id && x.voice_id === c.voice);
let curRow;
if (curCand) {
curRow = _rehVoiceRow({ name: curCand.title || c.voice, meta: [curCand.gender, curCand.language].filter(Boolean).join(' · ') || 'fish.audio', playUrl: curCand.sample_audio, isCurrent: true });
} else if (c.voice) {
const vd = getVoiceData(c.voice);
curRow = _rehVoiceRow({ name: vd?.name || c.voice, meta: vd?.group || vd?.tag || 'library voice', playVoice: c.voice, isCurrent: true });
} else {
curRow = `<div class="reh-vo-row"><div class="reh-vo-info"><span class="reh-vo-name">No voice assigned</span><span class="reh-vo-meta">pick one below</span></div></div>`;
}
const alts = o.candidates.map((cand, i) => ({ cand, i })).filter(({ cand }) => cand.voice_id !== c.voice || !cand.voice_id);
const altRows = alts.map(({ cand, i }) => _rehVoiceRow({
name: cand.title || 'Voice', meta: [cand.gender, cand.language].filter(Boolean).join(' · ') || 'fish.audio',
playUrl: cand.sample_audio, useAttrs: ` data-act="alt" data-idx="${i}"`,
})).join('');
return `<div class="reh-cc-online">
<div class="reh-cc-online-head"><span class="mdi mdi-earth"></span> Voice — try &amp; change
${alts.length ? `<button type="button" class="reh-vo-toggle">${alts.length} match${alts.length !== 1 ? 'es' : ''} ▾</button>` : ''}</div>
<div class="reh-vo-list">${curRow}</div>
${alts.length ? `<div class="reh-vo-list reh-vo-alts" hidden>${altRows}</div>` : ''}
<div class="reh-vo-tools">
<button type="button" class="reh-vo-tool" data-tool="local"><span class="mdi mdi-bookshelf"></span> From library</button>
<button type="button" class="reh-vo-tool" data-tool="search"><span class="mdi mdi-magnify"></span> Search online</button>
</div>
<div class="reh-vo-tool-panel reh-vo-local-panel" hidden>
<input type="search" class="reh-vo-local-input" placeholder="Filter your library voices…" autocomplete="off">
<div class="reh-vo-results reh-vo-local-results"></div>
</div>
<div class="reh-vo-tool-panel reh-vo-search-panel" hidden>
<div class="reh-vo-search-row"><input type="search" class="reh-vo-search-input" placeholder="Search fish.audio (name, style, language)…" autocomplete="off"><button type="button" class="btn-secondary btn-sm reh-vo-search-go">Search</button></div>
<div class="reh-vo-results reh-vo-search-results"><div class="reh-vo-hint">Type a query and hit Search to browse fish.audio voices.</div></div>
</div>
</div>`;
}
window.TTVC = window.TTVC || {};
window.TTVC.rehearser = window.TTVC.rehearser || {};
window.TTVC.rehearser.voicePicker = { castOnlinePanelHtml, preview: _rehAudition, stopPreview: _rehStopAudition };