From 1951ce0646e7049dc358a54bf243815ad5ead2a3 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 4 Sep 2026 11:27:49 +0200 Subject: [PATCH] refactor: extract audiobook dialogue merging --- CHANGELOG.md | 2 +- VERSION | 2 +- docs/ARCHITECTURE.md | 2 +- static/index.html | 12 +++++----- static/js/audiobook-cast.js | 25 ++++++++++++++++++++ static/js/audiobook.js | 46 ------------------------------------- 6 files changed, 34 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index daa4452..b8db678 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi ### Changed - **Audiobook cast normalization extraction.** Same-speaker post-cast merging now lives in - `audiobook-cast.js`, keeping deterministic segment cleanup separate from live casting orchestration. + `audiobook-cast.js`, including dialogue-only merging, keeping deterministic segment cleanup separate from live casting orchestration. - **Audiobook segmentation extraction.** Quote segmentation, conservative turn-taking, and dialogue-piece validation now live in `audiobook-segmentation.js`; the live casting pipeline remains compatible with the same global helpers. - **Rehearser voice-design extraction.** Character development, backend selection, and backend-refresh synchronization diff --git a/VERSION b/VERSION index 2264402..6d251e8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.39 +1.24.40 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 8462b77..84ee43f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -37,7 +37,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed) - ✅ `rehearser-voice-design.js` — character development, backend selection, and backend-refresh synchronization. - ✅ `audiobook-segmentation.js` — quote segmentation, conservative turn-taking, and dialogue-piece validation. - ✅ `audiobook-parse.js` — dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains in `audiobook.js`. -- ✅ `audiobook-cast.js` — cast Markdown export, legacy page-stamping migration, and deterministic same-speaker post-cast normalization helpers; live casting remains in `audiobook.js`. +- ✅ `audiobook-cast.js` — cast Markdown export, legacy page-stamping migration, and deterministic same-speaker/dialogue post-cast normalization helpers; live casting remains in `audiobook.js`. - ✅ `static/css/resource-diagnostics.css` — isolated Performance/resource diagnostic panel styles. - ✅ `static/css/background-jobs.css` — isolated background-job history and retry-panel styles. - ✅ `static/css/voice-design.css` — isolated Voice Design form, samples, and preview-match styles. diff --git a/static/index.html b/static/index.html index 3a12ecd..5146c26 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,10 +27,10 @@ - - - - + + + + @@ -395,7 +395,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook-cast.js b/static/js/audiobook-cast.js index 9986035..4851372 100644 --- a/static/js/audiobook-cast.js +++ b/static/js/audiobook-cast.js @@ -76,10 +76,35 @@ function _audiobookMergeAdjacentSameSpeaker(segments) { return out; } +// Merge consecutive segments spoken by the same named character. This is a +// dialogue-only normalization pass; narration is intentionally left alone. +function _audiobookMergeAdjacentDialogue(segments) { + const out = []; + let merged = 0; + for (const s of segments) { + if (out.length) { + const last = out[out.length - 1]; + const spk = String(s.speaker || '').trim().toLowerCase(); + const known = spk && !/^(unknown|unbekannt|narrator)$/i.test(spk); + if (last.type === 'dialogue' && s.type === 'dialogue' && known + && String(last.speaker || '').trim().toLowerCase() === spk) { + const a = String(last.text || '').trimEnd(), b = String(s.text || '').trimStart(); + last.text = a + (/[.!?…»«"']$/.test(a) ? '\n\n' : ' ') + b; + if (!String(last.emotion || '').trim() && String(s.emotion || '').trim()) last.emotion = s.emotion; + merged++; + continue; + } + } + out.push(s); + } + return { segments: out, merged }; +} + window.TTVC = window.TTVC || {}; window.TTVC.audiobook = window.TTVC.audiobook || {}; window.TTVC.audiobook.cast = { toMarkdown: _abCastMarkdown, stampPages: _abStampSegmentPages, mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker, + mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue, }; diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 1f672c1..3320192 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -1269,52 +1269,6 @@ function _abStampConfidence(segs) { return segs; } -// Merge consecutive segments spoken by the SAME named character. -// -// Deliberately dialogue-only. The full merge (_audiobookMergeAdjacentSameSpeaker) -// runs once at the end of casting, but recast and verify then reassign speakers -// and create fresh adjacent same-speaker pairs that nothing ever merges — 213 -// of them in the measured book. Re-running the full merge there makes things -// WORSE, because it also joins narration, and measured against the human -// reference the caster's narration is already too coarse (median 152 chars vs -// the human's 123) while its dialogue is slightly too fine (62 vs 65). -// Measured effect of merging dialogue runs only: text-match failures fall from -// 727 to 588, wrong-speaker rows from 345 to 310, and the segment count moves -// from 2273 to 2059 against the human's 2110. Merging narration as well pushed -// mismatches UP to 746, so this stays one-sided on purpose. -// Unknown speakers are never merged — two adjacent Unknown lines are not -// evidence of one speaker, they are two lines nobody has identified yet. -function _audiobookMergeAdjacentDialogue(segments) { - const out = []; - let merged = 0; - for (const s of segments) { - if (out.length) { - const last = out[out.length - 1]; - const spk = String(s.speaker || '').trim().toLowerCase(); - const known = spk && !/^(unknown|unbekannt|narrator)$/i.test(spk); - if (last.type === 'dialogue' && s.type === 'dialogue' && known - && String(last.speaker || '').trim().toLowerCase() === spk) { - const a = String(last.text || '').trimEnd(), b = String(s.text || '').trimStart(); - last.text = a + (/[.!?…»«"']$/.test(a) ? '\n\n' : ' ') + b; - // Emotion is deliberately NOT required to match. Requiring it blocked - // almost every merge (42 instead of 214) and left segmentation far from - // the human's: measured, same-emotion-only lands at 2230 segments and - // 685 text mismatches, while merging regardless lands at 2059 against - // the human's own 2110, with 588 mismatches and a better speaker-match - // rate (56.4% vs 53.8%). The cost is one emotion tag per character - // block instead of per chunk — but the hand-corrected reference merges - // these blocks too, so a human reading the scene judged one emotion - // sufficient. Keep the first non-empty tag rather than dropping both. - if (!String(last.emotion || '').trim() && String(s.emotion || '').trim()) last.emotion = s.emotion; - merged++; - continue; - } - } - out.push(s); - } - return { segments: out, merged }; -} - function audiobookIsRouterModel(model) { return /^auto[-_ ]?router/i.test(String(model || '').trim()); }