// ── Seed Finder — find the best RNG seed for a voice ───────────────────────── // // Appended to the voice inspector body by voice-inspector.js. // Generates audio samples for a voice across a range of seeds so the user // can listen and pick the most natural-sounding one, then save it to the // TTS server's voices.json with one click. const SEED_FINDER_TEXT_DE = ( 'Die 3.500 neuen High-End Geräte für das Server-Update benötigen eine ' + 'außergewöhnlich starke Kühlung und regelmäßige Maßnahmen, um die Performance ' + 'bei großer Last zu gewährleisten.' ); const SEED_FINDER_TEXT_EN = ( 'The system administrator successfully configured the customized Docker stacks ' + 'and benchmarked the inference engines at exactly 8:45 AM.' ); const SEED_FINDER_TEXT_MIXED = SEED_FINDER_TEXT_DE + ' - ' + SEED_FINDER_TEXT_EN; function _seedFinderDefaultText(voiceId) { const lc = (voiceId || '').toLowerCase(); if (lc.startsWith('de_')) return SEED_FINDER_TEXT_MIXED; if (lc.startsWith('en_') || lc.startsWith('gb_')) return SEED_FINDER_TEXT_EN; return SEED_FINDER_TEXT_MIXED; } // Called from voice-inspector.js after the persona panel is appended. function attachSeedFinder(voiceId, body) { const panel = document.createElement('div'); panel.className = 'opt-group seed-finder-group'; panel.innerHTML = `
🎲 Seed Finder Find the most natural-sounding voice
Generates audio for each seed so you can listen and pick the best one. Click "Use this seed" to save it to the TTS server.
`; body.appendChild(panel); // Toggle open/closed panel.querySelector('.opt-group-title').addEventListener('click', () => { panel.classList.toggle('open'); }); const textEl = panel.querySelector('.seed-finder-text'); const fromEl = panel.querySelector('.seed-finder-from'); const toEl = panel.querySelector('.seed-finder-to'); const backendEl = panel.querySelector('.seed-finder-backend'); const runBtn = panel.querySelector('.seed-finder-run-btn'); const cancelBtn = panel.querySelector('.seed-finder-cancel-btn'); const statusEl = panel.querySelector('.seed-finder-status'); const progressEl = panel.querySelector('.seed-finder-progress'); const progLabel = panel.querySelector('.seed-finder-progress-label'); const progCount = panel.querySelector('.seed-finder-progress-count'); const progBar = panel.querySelector('.seed-finder-bar'); const resultsEl = panel.querySelector('.seed-finder-results'); textEl.value = _seedFinderDefaultText(voiceId); let _cancelled = false; let _currentAudio = null; function _playBlob(blob) { if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; } const url = URL.createObjectURL(blob); const audio = new Audio(url); _currentAudio = audio; audio.play().catch(() => {}); audio.addEventListener('ended', () => { URL.revokeObjectURL(url); _currentAudio = null; }); } function _addResultRow(seed, blob, durationSec, err, activeSeedSet) { const row = document.createElement('div'); row.className = 'seed-finder-result-row' + (err ? ' seed-finder-result-error' : ''); row.dataset.seed = seed; if (err) { row.innerHTML = ` Seed ${seed} Failed: ${escHtml(String(err))} `; } else { const durStr = durationSec ? `${durationSec.toFixed(1)}s` : ''; row.innerHTML = ` Seed ${seed} ${durStr} `; row.querySelector('.sfr-play-btn').addEventListener('click', () => _playBlob(blob)); row.querySelector('.sfr-use-btn').addEventListener('click', async () => { // Stop any playback if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; } try { const resp = await fetch('/api/tts-voice-seed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ voice: voiceId, seed }), }); if (!resp.ok) { const e = await resp.json().catch(() => ({})); throw new Error(e.detail || resp.statusText); } // Clear all other "saved" markers resultsEl.querySelectorAll('.sfr-saved').forEach(el => el.textContent = ''); resultsEl.querySelectorAll('.seed-finder-result-row').forEach(r => r.classList.remove('sfr-active')); row.classList.add('sfr-active'); row.querySelector('.sfr-saved').textContent = '✓ Saved'; if (typeof toast === 'function') toast(`Seed ${seed} saved for ${voiceId}`, 'success'); } catch (e) { if (typeof toast === 'function') toast('Save failed: ' + e.message, 'error'); } }); } resultsEl.appendChild(row); } async function _runFinder() { const text = textEl.value.trim(); if (!text) { statusEl.textContent = 'Enter a test sentence.'; return; } const seedFrom = Math.max(0, parseInt(fromEl.value, 10) || 1); const seedTo = Math.max(seedFrom, parseInt(toEl.value, 10) || 15); const backend = backendEl.value; const seeds = []; for (let s = seedFrom; s <= seedTo; s++) seeds.push(s); const total = seeds.length; _cancelled = false; resultsEl.innerHTML = ''; progressEl.style.display = ''; runBtn.style.display = 'none'; cancelBtn.style.display = ''; statusEl.textContent = ''; progBar.style.width = '0%'; progCount.textContent = `0 / ${total}`; progLabel.textContent = 'Generating…'; for (let i = 0; i < seeds.length; i++) { if (_cancelled) break; const seed = seeds[i]; progLabel.textContent = `Seed ${seed}…`; progCount.textContent = `${i} / ${total}`; progBar.style.width = `${Math.round(i / total * 100)}%`; try { const blob = await fetchTtsPreviewBlob( voiceId, text, 'wav', '', backend, false, { seed } ); // Estimate duration from WAV byte length: (bytes - 44 header) / (24000 * 2) const durSec = Math.max(0, (blob.size - 44) / (24000 * 2)); _addResultRow(seed, blob, durSec, null, false); } catch (e) { _addResultRow(seed, null, 0, e.message || String(e), false); } } progBar.style.width = '100%'; progCount.textContent = `${seeds.length} / ${total}`; progLabel.textContent = _cancelled ? 'Cancelled.' : 'Done — listen and pick your seed.'; runBtn.style.display = ''; cancelBtn.style.display = 'none'; } runBtn.addEventListener('click', _runFinder); cancelBtn.addEventListener('click', () => { _cancelled = true; }); }