let _rehPrefetchRunning = false; async function _rehPrefetchLines(fromIndex) { if (_rehPrefetchRunning) return; _rehPrefetchRunning = true; try { let prefetched = 0; for (let i = fromIndex; i < rehState.lines.length && prefetched < 2; i++) { if (!rehState.playing) break; const l = rehState.lines[i]; if (!l || l.ignored || l.hidden || l.type === 'pagebreak') continue; const isDialog = l.type === 'dialog'; const cast = isDialog ? (rehState.cast[l.speaker] || { voice: '' }) : null; if (isDialog && (!cast.voice || cast.voice === 'me')) continue; if (!isDialog && (!rehState.narratorVoice || !(l.text || '').trim() || rehState.skipDescriptions)) continue; if (rehState.synthCache.has(i) && !rehState.staleLines.has(i)) { prefetched++; continue; } const voice = isDialog ? cast.voice : rehState.narratorVoice; const cleanTxt = stripMarkdown(l.text); const instruct = isDialog ? _buildInstruct((cast.instruct || '').trim(), l.emotion, voice) : ''; const text = isDialog ? _rehInlineTone(cleanTxt, l.emotion) : cleanTxt; const book = _lineAudioBookName(); const cacheKey = await _lineAudioCacheKey(text, voice, instruct); let blob = await _lineAudioCacheGet(book, cacheKey); if (!blob) { try { blob = await fetchTtsPreviewBlob(voice, text, 'wav', instruct, _ttsBackendForVoice(voice, rehState.backend)); _lineAudioCachePut(book, cacheKey, blob); } catch(e) { console.warn('Prefetch failed for line', i, e); continue; // skip counting as prefetched if it failed } } rehState.synthCache.set(i, blob); rehState.staleLines.delete(i); _markSynthDot(i, 'ok'); prefetched++; } } finally { _rehPrefetchRunning = false; } }