69 lines
3.0 KiB
JavaScript
69 lines
3.0 KiB
JavaScript
// ── 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 = [];
|
|
for (let i = 0; i < text.length; i++) {
|
|
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); i = orphan.end - 1; 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 });
|
|
i = spans[spans.length - 1].end - 1;
|
|
}
|
|
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);
|