// ── 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 = ``; const right = isCurrent ? ` Selected` : ``; return `