// ── Audiobook quote segmentation ──────────────────────────────────────────── // Quote-boundary segmentation and conservative turn-taking are kept separate // from the live LLM casting workflow. Their shared quote/heuristic helpers are // resolved when a passage is processed, preserving the legacy global API. (function (root) { function _abImplausibleDialoguePiece(text) { const t = String(text || '').trim(); if (!t) return true; const words = t.split(/\s+/).filter(Boolean); return words.length === 1 && !/[.!?…]["'’”»]?$/.test(t); } function audiobookSplitByQuotes(text) { const spans = []; let next = 0; while (next < text.length) { const i = next++; const open = text[i], close = AB_QUOTE_PAIRS[open]; if (!close) continue; const minStart = spans.length ? spans[spans.length - 1].end : 0; const orphan = _abOrphanClosingQuoteSpan(text, i, minStart); if (orphan) { spans.push(orphan); next = orphan.end; continue; } if ((open === '«' || open === '»') && i > 0 && !/[\s([{—–-]/.test(text[i - 1])) continue; const raw = text.slice(i + 1); const closeIdx = raw.indexOf(close); const endInRaw = _abUnclosedQuoteEnd(raw, closeIdx); const quote = raw.slice(0, endInRaw).trim(); if (!quote) continue; const consumedClose = closeIdx >= 0 && endInRaw === closeIdx; spans.push({ start: i, end: i + 1 + endInRaw + (consumedClose ? 1 : 0), quote }); next = spans[spans.length - 1].end; } if (!spans.length) return [{ speaker: 'Narrator', type: 'narration', text, emotion: '' }]; const out = []; let last = 0; for (let k = 0; k < spans.length; k++) { const sp = spans[k], pre = text.slice(last, sp.start); if (pre.trim()) out.push({ speaker: 'Narrator', type: 'narration', text: pre.trim(), emotion: '' }); if (sp.quote) { const after = text.slice(sp.end, k + 1 < spans.length ? spans[k + 1].start : text.length); out.push({ speaker: audiobookGuessSpeaker(after, pre) || 'Unknown', type: 'dialogue', text: sp.quote, emotion: '' }); } last = sp.end; } const tail = text.slice(last).trimEnd(); if (tail.trim()) out.push({ speaker: 'Narrator', type: 'narration', text: tail.trim(), emotion: '' }); return audiobookTurnTaking(out); } function audiobookTurnTaking(segs) { let a = null, b = null; for (const s of segs) { if (s.type !== 'dialogue') continue; if (s.speaker && !/^Unknown|Unbekannt/i.test(s.speaker)) { if (s.speaker !== b) { a = b; b = s.speaker; } } else if (a && b && a !== b) { s.speaker = a; const t = a; a = b; b = t; } } return segs; } root._abImplausibleDialoguePiece = _abImplausibleDialoguePiece; root.audiobookSplitByQuotes = audiobookSplitByQuotes; root.audiobookTurnTaking = audiobookTurnTaking; root.TTVC = root.TTVC || {}; root.TTVC.audiobook = root.TTVC.audiobook || {}; root.TTVC.audiobook.segmentation = { splitByQuotes: audiobookSplitByQuotes, turnTaking: audiobookTurnTaking }; })(window);