refactor: extract audiobook dialogue merging
This commit is contained in:
parent
41349867a1
commit
1951ce0646
@ -9,7 +9,7 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- **Audiobook cast normalization extraction.** Same-speaker post-cast merging now lives in
|
- **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
|
- **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.
|
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
|
- **Rehearser voice-design extraction.** Character development, backend selection, and backend-refresh synchronization
|
||||||
|
|||||||
@ -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.
|
- ✅ `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-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-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/resource-diagnostics.css` — isolated Performance/resource diagnostic panel styles.
|
||||||
- ✅ `static/css/background-jobs.css` — isolated background-job history and retry-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.
|
- ✅ `static/css/voice-design.css` — isolated Voice Design form, samples, and preview-match styles.
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
<meta name="format-detection" content="telephone=no">
|
<meta name="format-detection" content="telephone=no">
|
||||||
<meta name="color-scheme" content="light dark">
|
<meta name="color-scheme" content="light dark">
|
||||||
<meta name="theme-color" content="#2563EB">
|
<meta name="theme-color" content="#2563EB">
|
||||||
<meta name="app-version" content="1.24.39">
|
<meta name="app-version" content="1.24.40">
|
||||||
<link rel="manifest" href="/manifest.webmanifest">
|
<link rel="manifest" href="/manifest.webmanifest">
|
||||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||||
@ -27,10 +27,10 @@
|
|||||||
|
|
||||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||||
<link rel="stylesheet" href="/static/style.css?v=1.24.39">
|
<link rel="stylesheet" href="/static/style.css?v=1.24.40">
|
||||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.39">
|
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.40">
|
||||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.39">
|
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.40">
|
||||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.39">
|
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.40">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -395,7 +395,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||||
<script src="/static/loader.js?v=1.24.39"></script>
|
<script src="/static/loader.js?v=1.24.40"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -76,10 +76,35 @@ function _audiobookMergeAdjacentSameSpeaker(segments) {
|
|||||||
return out;
|
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 = window.TTVC || {};
|
||||||
window.TTVC.audiobook = window.TTVC.audiobook || {};
|
window.TTVC.audiobook = window.TTVC.audiobook || {};
|
||||||
window.TTVC.audiobook.cast = {
|
window.TTVC.audiobook.cast = {
|
||||||
toMarkdown: _abCastMarkdown,
|
toMarkdown: _abCastMarkdown,
|
||||||
stampPages: _abStampSegmentPages,
|
stampPages: _abStampSegmentPages,
|
||||||
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
||||||
|
mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1269,52 +1269,6 @@ function _abStampConfidence(segs) {
|
|||||||
return 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) {
|
function audiobookIsRouterModel(model) {
|
||||||
return /^auto[-_ ]?router/i.test(String(model || '').trim());
|
return /^auto[-_ ]?router/i.test(String(model || '').trim());
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user