74 lines
4.8 KiB
JavaScript
74 lines
4.8 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 & 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>`;
|
|
}
|