refactor: extract audiobook cast normalization
This commit is contained in:
parent
0876527554
commit
41349867a1
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
## [Unreleased]
|
||||
|
||||
### 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 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
|
||||
|
||||
@ -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 and legacy page-stamping migration helpers; live casting 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`.
|
||||
- ✅ `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.
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<meta name="format-detection" content="telephone=no">
|
||||
<meta name="color-scheme" content="light dark">
|
||||
<meta name="theme-color" content="#2563EB">
|
||||
<meta name="app-version" content="1.24.38">
|
||||
<meta name="app-version" content="1.24.39">
|
||||
<link rel="manifest" href="/manifest.webmanifest">
|
||||
<link rel="icon" href="/static/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/static/icon.svg">
|
||||
@ -27,10 +27,10 @@
|
||||
|
||||
<!-- ── Core styles (local — no CDN dependency for first paint) ────────── -->
|
||||
<link rel="stylesheet" href="/static/vendor/mdi/materialdesignicons.min.css">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.24.38">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.38">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.38">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.38">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.24.39">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.39">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.39">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.39">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -395,7 +395,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.24.38"></script>
|
||||
<script src="/static/loader.js?v=1.24.39"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -51,6 +51,35 @@ function _abStampSegmentPages(segs, pageMarks, text) {
|
||||
}
|
||||
}
|
||||
|
||||
// Merge adjacent segments assigned to the same speaker without crossing a
|
||||
// page boundary. Kept separate from the live casting orchestration because it
|
||||
// is a deterministic post-cast normalization pass.
|
||||
function _audiobookMergeAdjacentSameSpeaker(segments) {
|
||||
const out = [];
|
||||
for (const s of segments) {
|
||||
const last = out[out.length - 1];
|
||||
if (last && last.type === s.type && last.page === s.page) {
|
||||
const lastSpeaker = (last.speaker || 'Narrator').trim().toLowerCase();
|
||||
const curSpeaker = (s.speaker || 'Narrator').trim().toLowerCase();
|
||||
// Dialogue keeps its emotion tag distinct so one mood is not silently
|
||||
// discarded when two adjacent rows are combined.
|
||||
const sameEmotion = s.type !== 'dialogue' || (last.emotion || '').toLowerCase() === (s.emotion || '').toLowerCase();
|
||||
const unknownSpeaker = !lastSpeaker || lastSpeaker === 'unknown' || lastSpeaker === 'unbekannt';
|
||||
if (lastSpeaker === curSpeaker && sameEmotion && !(s.type === 'dialogue' && unknownSpeaker)) {
|
||||
const lastText = last.text.trimEnd(), curText = s.text.trimStart();
|
||||
last.text = /[.!?…»«”"’']$/.test(lastText) ? lastText + '\n\n' + curText : lastText + ' ' + curText;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(Object.assign({}, s));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
window.TTVC = window.TTVC || {};
|
||||
window.TTVC.audiobook = window.TTVC.audiobook || {};
|
||||
window.TTVC.audiobook.cast = { toMarkdown: _abCastMarkdown, stampPages: _abStampSegmentPages };
|
||||
window.TTVC.audiobook.cast = {
|
||||
toMarkdown: _abCastMarkdown,
|
||||
stampPages: _abStampSegmentPages,
|
||||
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
||||
};
|
||||
|
||||
@ -863,46 +863,6 @@ function _audiobookFixOrphanedQuoteMarks(segments) {
|
||||
return segments.filter(s => s.text.trim());
|
||||
}
|
||||
|
||||
// Two adjacent segments assigned to the exact same speaker (narrator or a
|
||||
// character) with no page break between them are either a casting mistake
|
||||
// (a line that should never have been split off on its own — attribution
|
||||
// noise from the per-passage LLM pass, especially near dialogue tags) or
|
||||
// genuinely one continuous block that just got chopped into several tiny
|
||||
// rows in the casting feed. Either way, splicing them back into one segment
|
||||
// is the safe, mechanical fix: it can't make a correct attribution worse,
|
||||
// and it turns a wall of one-line "NARRATOR" cards into the actual
|
||||
// paragraph flow. Runs once over the whole book after casting finishes
|
||||
// (not just within one passage chunk, unlike the narration-only merge
|
||||
// further up) so it also catches passage-boundary duplicates.
|
||||
function _audiobookMergeAdjacentSameSpeaker(segments) {
|
||||
const out = [];
|
||||
for (const s of segments) {
|
||||
const last = out[out.length - 1];
|
||||
if (last && last.type === s.type && last.page === s.page) {
|
||||
const lastSpeaker = (last.speaker || 'Narrator').trim().toLowerCase();
|
||||
const curSpeaker = (s.speaker || 'Narrator').trim().toLowerCase();
|
||||
// Dialogue keeps its emotion tag distinct — merging "happy Marcus" text
|
||||
// into "angry Marcus" text would silently discard one of the two moods.
|
||||
const sameEmotion = s.type !== 'dialogue' || (last.emotion || '').toLowerCase() === (s.emotion || '').toLowerCase();
|
||||
// Two adjacent UNKNOWN lines are not evidence of one speaker — "Unknown"
|
||||
// means precisely that we do not know who spoke. Both normalise to the
|
||||
// same string here, so they compared as equal and were fused into a single
|
||||
// block: confirmed live, Lysandra's line and Perdia's interruption (two
|
||||
// separate paragraphs in the book) became one segment joined by a blank
|
||||
// line, after which no attribution pass could ever separate them again and
|
||||
// one voice would read both sides of the exchange. Never merge on Unknown.
|
||||
const unknownSpeaker = !lastSpeaker || lastSpeaker === 'unknown' || lastSpeaker === 'unbekannt';
|
||||
if (lastSpeaker === curSpeaker && sameEmotion && !(s.type === 'dialogue' && unknownSpeaker)) {
|
||||
const lastText = last.text.trimEnd(), curText = s.text.trimStart();
|
||||
last.text = /[.!?…»«”"’']$/.test(lastText) ? lastText + '\n\n' + curText : lastText + ' ' + curText;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.push(Object.assign({}, s));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Removes a segment whose text is a near-duplicate of another segment within
|
||||
// a nearby window — confirmed live: "Verify all characters" (audiobookRecastUnknown
|
||||
// with includeNarrator:true) can re-splice a passage's already-correct text back
|
||||
|
||||
Loading…
Reference in New Issue
Block a user