// ── 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.

// Test sentence: umlauts (ä ö ü ß), dates/numbers, and English words — reveals a
// voice's character per seed. Used for every voice (incl. the batch run).
const SEED_FINDER_TEXT_DE = 'Notiere dir an Midsummer 21.06. um 14 Uhr - Es ist reine Zeitverschwendung, etwas Mittelmäßiges zu tun! Schöne Grüße! — Madonna - Träume beginnt mit einem positiven Mindset.';
const SEED_FINDER_TEXT_EN = SEED_FINDER_TEXT_DE;
const SEED_FINDER_TEXT_MIXED = SEED_FINDER_TEXT_DE;

function _seedFinderDefaultText(voiceId) {
  if (window._appSettings && window._appSettings.seed_finder_text) {
    return window._appSettings.seed_finder_text;
  }
  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;
}

// ── Sample cache (IndexedDB) — generated WAVs persist so reopening a voice or
// re-running doesn't regenerate seeds that already succeeded ──────────────────
const SF_DB = 'seed-finder', SF_STORE = 'samples';
function _sfHash(s) { let h = 0; for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0; return h.toString(36); }
function _sfDbOpen() {
  return new Promise((res, rej) => {
    const r = indexedDB.open(SF_DB, 1);
    r.onupgradeneeded = e => { const db = e.target.result; if (!db.objectStoreNames.contains(SF_STORE)) db.createObjectStore(SF_STORE, { keyPath: 'key' }); };
    r.onsuccess = e => res(e.target.result); r.onerror = e => rej(e.target.error);
  });
}
async function _sfDbGet(key) {
  try { const db = await _sfDbOpen(); return await new Promise((res, rej) => { const r = db.transaction(SF_STORE, 'readonly').objectStore(SF_STORE).get(key); r.onsuccess = e => res(e.target.result || null); r.onerror = e => rej(e.target.error); }); }
  catch (_) { return null; }
}
async function _sfDbPut(rec) {
  try { const db = await _sfDbOpen(); await new Promise((res, rej) => { const r = db.transaction(SF_STORE, 'readwrite').objectStore(SF_STORE).put(rec); r.onsuccess = () => res(); r.onerror = e => rej(e.target.error); }); } catch (_) {}
}
async function _sfDbAllForVoice(voiceId) {
  try {
    const db = await _sfDbOpen();
    return await new Promise((res, rej) => {
      const out = []; const cur = db.transaction(SF_STORE, 'readonly').objectStore(SF_STORE).openCursor();
      cur.onsuccess = e => { const c = e.target.result; if (c) { if (c.value.voiceId === voiceId) out.push(c.value); c.continue(); } else res(out); };
      cur.onerror = e => rej(e.target.error);
    });
  } catch (_) { return []; }
}
async function _sfDbClearVoice(voiceId) {
  try {
    const db = await _sfDbOpen();
    await new Promise((res, rej) => {
      const store = db.transaction(SF_STORE, 'readwrite').objectStore(SF_STORE);
      const cur = store.openCursor();
      cur.onsuccess = e => { const c = e.target.result; if (c) { if (c.value.voiceId === voiceId) store.delete(c.primaryKey); c.continue(); } else res(); };
      cur.onerror = e => rej(e.target.error);
    });
  } catch (_) {}
}

// 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 = `
    <div class="opt-group-title">
      <span class="opt-chevron"></span>
      <span class="opt-title-text">🎲 Seed Finder</span>
      <span class="opt-group-meta">Find the most natural-sounding voice</span>
    </div>
    <div class="opt-group-subtitle">
      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.
    </div>
    <div class="opt-group-body seed-finder-body">
      <div class="seed-finder-config">
        <div class="seed-finder-row">
          <label class="seed-finder-label">Test sentence</label>
          <textarea class="seed-finder-text" rows="3" spellcheck="false"></textarea>
        </div>
        <div class="seed-finder-row seed-finder-params">
          <div class="seed-finder-field">
            <label>Seeds: from</label>
            <input class="seed-finder-from" type="number" min="0" max="9999" value="1" step="1">
            <label>to</label>
            <input class="seed-finder-to" type="number" min="1" max="9999" value="15" step="1">
          </div>
          <div class="seed-finder-field">
            <label>Backend</label>
            <select class="seed-finder-backend">
              <option value="voice_clone" selected>Voice Clone</option>
              <option value="streaming">Streaming</option>
            </select>
          </div>
        </div>
        <div class="seed-finder-actions">
          <button class="btn-primary seed-finder-run-btn">▶ Run Seed Finder</button>
          <button class="btn-secondary seed-finder-cancel-btn" style="display:none">✕ Cancel</button>
          <button class="btn-secondary seed-finder-clear-btn" style="display:none" title="Delete the saved samples cached for this voice">🗑 Clear saved</button>
          <span class="seed-finder-status"></span>
        </div>
        <div class="seed-finder-row seed-finder-pin">
          <label class="seed-finder-label">Or fix this voice to a specific seed</label>
          <div class="seed-finder-pin-controls">
            <input class="seed-finder-pin-input" type="number" min="0" max="9999" step="1" placeholder="seed #">
            <button class="btn-secondary seed-finder-pin-btn">★ Pin seed</button>
            <button class="btn-secondary seed-finder-pin-clear" title="Use a random seed each time (unpin)">Unpin</button>
            <span class="seed-finder-pin-status"></span>
          </div>
        </div>
      </div>

      <div class="seed-finder-progress" style="display:none">
        <div class="seed-finder-progress-head">
          <span class="seed-finder-progress-label">Generating…</span>
          <span class="seed-finder-progress-count">0 / 0</span>
        </div>
        <div class="benchmark-progress-track">
          <div class="seed-finder-bar" style="width:0%"></div>
        </div>
      </div>

      <div class="seed-finder-results"></div>
    </div>
  `;

  body.appendChild(panel);

  // Toggle open/closed; load batch samples the first time the panel opens
  let _batchLoaded = false;
  panel.querySelector('.opt-group-title').addEventListener('click', () => {
    panel.classList.toggle('open');
    if (panel.classList.contains('open') && !_batchLoaded) {
      _batchLoaded = true;
      _loadBatchSamples();
    }
  });

  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 clearBtn    = panel.querySelector('.seed-finder-clear-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);

  const pinInput  = panel.querySelector('.seed-finder-pin-input');
  const pinBtn    = panel.querySelector('.seed-finder-pin-btn');
  const pinClear  = panel.querySelector('.seed-finder-pin-clear');
  const pinStatus = panel.querySelector('.seed-finder-pin-status');

  const voiceObj = (window._voices || []).find(v => v.id === voiceId) || {};
  if (voiceObj.seed !== undefined && voiceObj.seed !== null) {
    pinInput.value = voiceObj.seed;
    pinStatus.textContent = `✓ Pinned seed ${voiceObj.seed}`;
  }

  let _cancelled = false;
  let _currentAudio = null;

  // ── Load pre-generated batch samples on panel open ────────────────────────
  async function _loadBatchSamples() {
    try {
      const resp = await fetch(`/api/seed-samples/${encodeURIComponent(voiceId)}`);
      if (!resp.ok) return;
      const { seeds } = await resp.json();
      if (!seeds || seeds.length === 0) return;

      resultsEl.innerHTML = '';
      const header = document.createElement('div');
      header.className = 'sfr-batch-header';
      header.textContent = `${seeds.length} pre-generated sample${seeds.length !== 1 ? 's' : ''} from batch run — click Play to listen`;
      resultsEl.appendChild(header);

      for (const seed of seeds) {
        const row = document.createElement('div');
        row.className = 'seed-finder-result-row sfr-batch';
        row.dataset.seed = seed;
        row.innerHTML = `
          <span class="sfr-seed">Seed ${seed}</span>
          <span class="sfr-dur sfr-batch-tag">pre-generated</span>
          <button class="btn-secondary sfr-play-btn"><span class="mdi mdi-play"></span> Play</button>
          <button class="btn-primary sfr-use-btn">★ Use seed ${seed}</button>
          <span class="sfr-saved"></span>
        `;

        let _cachedBlob = null;
        row.querySelector('.sfr-play-btn').addEventListener('click', async () => {
          if (!_cachedBlob) {
            const r = await fetch(`/api/seed-sample/${encodeURIComponent(voiceId)}/${seed}`);
            if (!r.ok) { if (typeof toast === 'function') toast('Failed to load sample', 'error'); return; }
            _cachedBlob = await r.blob();
          }
          _playBlob(_cachedBlob);
        });

        row.querySelector('.sfr-use-btn').addEventListener('click', async () => {
          try {
            await _saveSeed(seed);
            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 (pinInput) pinInput.value = seed;
            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);
      }
    } catch (_) {}
  }

  // Save (or clear, with seed=null) the voice's seed in the TTS server's voices.json.
  async function _saveSeed(seed) {
    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); }

    const v = (window._voices || []).find(vv => vv.id === voiceId);
    if (v) v.seed = seed;

    const pinnedSeedEl = document.querySelector('.insp-pinned-seed');
    if (pinnedSeedEl) {
      if (seed !== null && seed !== undefined) {
        pinnedSeedEl.textContent = `Pin Seed # ${seed}`;
        pinnedSeedEl.style.display = '';
      } else {
        pinnedSeedEl.style.display = 'none';
        pinnedSeedEl.textContent = '';
      }
    }
  }

  // Retry transient failures (e.g. "Failed to fetch" when a long generation drops).
  async function _fetchWithRetry(fn, tries = 2, delay = 1200) {
    let lastErr;
    for (let t = 0; t <= tries; t++) {
      if (_cancelled) throw new Error('cancelled');
      try { return await fn(); }
      catch (e) { lastErr = e; if (t < tries) await new Promise(r => setTimeout(r, delay)); }
    }
    throw lastErr;
  }

  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; });
  }

  const seedRows = new Map();   // seed → row element
  const _keyFor = (seed, text, backend) => `${voiceId}|${backend}|${_sfHash(text)}|${seed}`;

  async function _useSeed(seed, row) {
    if (_currentAudio) { _currentAudio.pause(); _currentAudio = null; }
    try {
      await _saveSeed(seed);
      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 (pinInput) pinInput.value = seed;
      if (typeof toast === 'function') toast(`Seed ${seed} saved for ${voiceId}`, 'success');
    } catch (e) {
      if (typeof toast === 'function') toast('Save failed: ' + e.message, 'error');
    }
  }

  function _renderSeed(seed, data) {
    const row = document.createElement('div');
    row.className = 'seed-finder-result-row' + (data.err ? ' seed-finder-result-error' : '');
    row.dataset.seed = seed;
    if (data.err) {
      row.innerHTML = `<span class="sfr-seed">Seed ${seed}</span>
        <span class="sfr-error">Failed: ${escHtml(String(data.err))}</span>
        <button class="btn-secondary sfr-retry-btn"><span class="mdi mdi-refresh"></span> Retry</button>`;
      row.querySelector('.sfr-retry-btn').addEventListener('click', () => _retrySeed(seed));
    } else {
      const durStr = data.dur ? `${data.dur.toFixed(1)}s` : '';
      row.innerHTML = `<span class="sfr-seed">Seed ${seed}</span>
        <span class="sfr-dur">${durStr}${data.cached ? ' · saved' : ''}</span>
        <button class="btn-secondary sfr-play-btn"><span class="mdi mdi-play"></span> Play</button>
        <button class="btn-primary sfr-use-btn">★ Use seed ${seed}</button>
        <span class="sfr-saved"></span>`;
      row.querySelector('.sfr-play-btn').addEventListener('click', () => _playBlob(data.blob));
      row.querySelector('.sfr-use-btn').addEventListener('click', () => _useSeed(seed, row));
    }
    const existing = seedRows.get(seed);
    if (existing) existing.replaceWith(row); else resultsEl.appendChild(row);
    seedRows.set(seed, row);
  }

  // Generate one seed — reuses the cached WAV unless force, and caches new ones.
  async function _generateSeed(seed, text, backend, force) {
    const key = _keyFor(seed, text, backend);
    if (!force) { const c = await _sfDbGet(key); if (c && c.blob) return { blob: c.blob, dur: c.dur || 0, cached: true }; }
    const blob = await _fetchWithRetry(() => fetchTtsPreviewBlob(voiceId, text, 'wav', '', backend, false, { seed }));
    const dur = Math.max(0, (blob.size - 44) / (24000 * 2));   // WAV bytes → seconds (24 kHz, 16-bit)
    _sfDbPut({ key, voiceId, backend, textHash: _sfHash(text), seed, blob, dur, ts: Date.now() });
    return { blob, dur, cached: false };
  }

  async function _retrySeed(seed) {
    const text = textEl.value.trim(), backend = backendEl.value;
    const row = seedRows.get(seed); const err = row && row.querySelector('.sfr-error'); if (err) err.textContent = 'Retrying…';
    try { _renderSeed(seed, await _generateSeed(seed, text, backend, true)); }
    catch (e) { _renderSeed(seed, { err: e.message || String(e) }); }
  }

  // Show any cached samples for the current text+backend without regenerating.
  async function _loadCached() {
    const text = textEl.value.trim(), backend = backendEl.value, th = _sfHash(text);
    const all = (await _sfDbAllForVoice(voiceId)).filter(r => r.backend === backend && r.textHash === th && r.blob);
    if (!all.length) { clearBtn.style.display = 'none'; return; }
    all.sort((a, b) => a.seed - b.seed);
    resultsEl.innerHTML = ''; seedRows.clear();
    all.forEach(r => _renderSeed(r.seed, { blob: r.blob, dur: r.dur || 0, cached: true }));
    clearBtn.style.display = '';
    statusEl.textContent = `${all.length} saved sample${all.length !== 1 ? 's' : ''} loaded — Run to fill the rest.`;
  }

  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 = ''; seedRows.clear();
    progressEl.style.display = '';
    runBtn.style.display = 'none';
    cancelBtn.style.display = ''; clearBtn.style.display = 'none';
    statusEl.textContent = '';
    progBar.style.width = '0%';
    progCount.textContent = `0 / ${total}`;
    progLabel.textContent = 'Generating…';

    let i = 0, cached = 0;
    for (const seed of seeds) {
      if (_cancelled) break;
      progLabel.textContent = `Seed ${seed}…`;
      progCount.textContent = `${i} / ${total}`;
      progBar.style.width = `${Math.round(i / total * 100)}%`;
      try {
        const d = await _generateSeed(seed, text, backend, false);
        if (d.cached) cached++;
        _renderSeed(seed, d);
      } catch (e) {
        if (_cancelled) break;
        _renderSeed(seed, { err: e.message || String(e) });
      }
      i++;
    }

    progBar.style.width = '100%';
    progCount.textContent = `${i} / ${total}`;
    progLabel.textContent = _cancelled ? 'Cancelled.' : `Done — listen and pick your seed.${cached ? ` (${cached} from cache)` : ''}`;
    runBtn.style.display = '';
    cancelBtn.style.display = 'none';
    if (seedRows.size) clearBtn.style.display = '';
  }

  runBtn.addEventListener('click', _runFinder);
  cancelBtn.addEventListener('click', () => { _cancelled = true; });
  clearBtn.addEventListener('click', async () => {
    if (!confirm('Delete the cached seed samples for this voice?')) return;
    await _sfDbClearVoice(voiceId);
    resultsEl.innerHTML = ''; seedRows.clear(); clearBtn.style.display = 'none';
    statusEl.textContent = 'Saved samples cleared.';
  });

  _loadCached();   // show previously generated samples on open (no regeneration)

  // Fix the voice to a specific seed directly (no generation needed)
  pinBtn.addEventListener('click', async () => {
    const seed = parseInt(pinInput.value, 10);
    if (isNaN(seed) || seed < 0) { pinStatus.textContent = 'Enter a seed number.'; return; }
    pinBtn.disabled = true; pinStatus.textContent = 'Saving…';
    try {
      await _saveSeed(seed);
      pinStatus.textContent = `✓ Pinned seed ${seed}`;
      if (typeof toast === 'function') toast(`Voice ${voiceId} fixed to seed ${seed}`, 'success');
    } catch (e) {
      pinStatus.textContent = '';
      if (typeof toast === 'function') toast('Pin failed: ' + e.message, 'error');
    } finally { pinBtn.disabled = false; }
  });

  // Unpin → random seed each generation
  pinClear.addEventListener('click', async () => {
    pinClear.disabled = true; pinStatus.textContent = 'Clearing…';
    try {
      await _saveSeed(null);
      pinInput.value = '';
      pinStatus.textContent = '✓ Unpinned (random seed)';
      resultsEl.querySelectorAll('.sfr-saved').forEach(el => el.textContent = '');
      resultsEl.querySelectorAll('.seed-finder-result-row').forEach(r => r.classList.remove('sfr-active'));
      if (typeof toast === 'function') toast(`Voice ${voiceId} unpinned — random seed`, 'success');
    } catch (e) {
      pinStatus.textContent = '';
      if (typeof toast === 'function') toast('Unpin failed: ' + e.message, 'error');
    } finally { pinClear.disabled = false; }
  });
}

// ── Batch: pre-generate & cache Seed Finder samples for ALL active voices ─────
// Sequential (single-GPU friendly), skips already-cached seeds (resumable), and
// writes to the same cache the per-voice Seed Finder reads, so opening any voice
// later shows its samples instantly.
function seedFinderBatchAll() {
  const ids = (typeof activeVoiceIds === 'function')
    ? activeVoiceIds()
    : (window._voices || []).filter(v => v.enabled !== false).map(v => v.id);
  if (!ids.length) { if (typeof toast === 'function') toast('No active voices', 'error'); return; }

  // In-app config dialog (no browser prompts): pick the seed range, then Start.
  document.getElementById('seedbatch-overlay')?.remove();
  const ov = document.createElement('div');
  ov.className = 'audiobook-overlay'; ov.id = 'seedbatch-overlay';
  ov.innerHTML = `<div class="audiobook-box">
    <div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div>
    <div class="audiobook-msg">Generate &amp; cache Seed Finder samples for all <b>${ids.length}</b> active voices. Already-cached seeds are skipped, so it's resumable; you can cancel anytime.</div>
    <div class="seed-finder-row seed-finder-params" style="margin:12px 0">
      <div class="seed-finder-field">
        <label>Seeds: from</label>
        <input class="sb-from" type="number" min="0" max="9999" value="1" step="1">
        <label>to</label>
        <input class="sb-to" type="number" min="1" max="9999" value="8" step="1">
      </div>
      <span class="sb-estimate seed-finder-status"></span>
    </div>
    <div class="audiobook-actions">
      <button class="btn-secondary btn-sm" id="sb-cancel-cfg">Cancel</button>
      <button class="btn-primary btn-sm" id="sb-start"><span class="mdi mdi-play"></span> Start</button>
    </div>
  </div>`;
  document.body.appendChild(ov);
  const box = ov.querySelector('.audiobook-box');
  const fromI = ov.querySelector('.sb-from'), toI = ov.querySelector('.sb-to'), est = ov.querySelector('.sb-estimate');
  const updEst = () => {
    const f = Math.max(0, parseInt(fromI.value, 10) || 0), t = Math.max(f, parseInt(toI.value, 10) || f);
    est.textContent = `${ids.length} voices × ${t - f + 1} seeds = up to ${ids.length * (t - f + 1)} samples`;
  };
  fromI.addEventListener('input', updEst); toI.addEventListener('input', updEst); updEst();
  ov.querySelector('#sb-cancel-cfg').addEventListener('click', () => ov.remove());
  ov.querySelector('#sb-start').addEventListener('click', () => {
    const from = Math.max(0, parseInt(fromI.value, 10) || 1);
    const to   = Math.max(from, parseInt(toI.value, 10) || 8);
    _seedBatchRun(ids, from, to, box, ov);
  });
}

// Progress phase of the batch (swaps the dialog body to a live progress view).
async function _seedBatchRun(ids, from, to, box, ov) {
  const backend = 'voice_clone';
  const totalJobs = ids.length * (to - from + 1);
  box.innerHTML = `<div class="audiobook-title"><span class="mdi mdi-dice-multiple-outline"></span> Batch seed generation</div>
    <div class="audiobook-msg" id="sb-msg">Starting…</div>
    <div class="reader-synth-track"><div class="reader-synth-fill" id="sb-fill"></div></div>
    <div class="audiobook-actions"><button class="btn-secondary btn-sm" id="sb-cancel">Cancel</button></div>`;
  let cancel = false;
  box.querySelector('#sb-cancel').addEventListener('click', () => { cancel = true; });
  const fill = box.querySelector('#sb-fill'), msg = box.querySelector('#sb-msg');

  let done = 0, made = 0, cached = 0, failed = 0;
  for (let vi = 0; vi < ids.length && !cancel; vi++) {
    const voiceId = ids[vi];
    const text = _seedFinderDefaultText(voiceId);
    const th = _sfHash(text);
    for (let seed = from; seed <= to && !cancel; seed++) {
      msg.textContent = `Voice ${vi + 1}/${ids.length} · seed ${seed} — ${voiceId}`;
      const key = `${voiceId}|${backend}|${th}|${seed}`;
      const existing = await _sfDbGet(key);
      if (existing && existing.blob) { cached++; done++; fill.style.width = (done / totalJobs * 100) + '%'; continue; }
      try {
        const blob = await fetchTtsPreviewBlob(voiceId, text, 'wav', '', backend, false, { seed });
        const dur = Math.max(0, (blob.size - 44) / (24000 * 2));
        await _sfDbPut({ key, voiceId, backend, textHash: th, seed, blob, dur, ts: Date.now() });
        made++;
      } catch (_) { failed++; }
      done++; fill.style.width = (done / totalJobs * 100) + '%';
    }
  }
  ov.remove();
  if (typeof toast === 'function') {
    toast(cancel
      ? `Stopped — ${made} generated, ${cached} already cached`
      : `Batch done — ${made} generated, ${cached} cached${failed ? `, ${failed} failed` : ''}`,
      cancel ? 'error' : 'success');
  }
}

if (typeof $ === 'function') $('seed-batch-all-btn')?.addEventListener('click', seedFinderBatchAll);
