fix: preserve audiobook dialogue boundaries
This commit is contained in:
parent
29f5243f6b
commit
2caa417401
@ -8,6 +8,11 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- **Initial audiobook casts preserve dialogue boundaries.** The final cast
|
||||||
|
normalization now merges adjacent narration only; same-speaker dialogue
|
||||||
|
rows remain separate so Run 3 segmentation errors stay visible and do not
|
||||||
|
become fused multi-turn lines. Recast/verify retains its explicit
|
||||||
|
dialogue-only merge behavior.
|
||||||
- **Dialogue-only merge regression coverage.** Recast/verify normalization now has explicit browser coverage proving that
|
- **Dialogue-only merge regression coverage.** Recast/verify normalization now has explicit browser coverage proving that
|
||||||
only adjacent known-speaker dialogue merges; narration and unknown-speaker lines remain separate.
|
only adjacent known-speaker dialogue merges; narration and unknown-speaker lines remain separate.
|
||||||
- **Visual identity regression coverage.** Portrait and concept-sheet prompt tests now verify shared identity anchors;
|
- **Visual identity regression coverage.** Portrait and concept-sheet prompt tests now verify shared identity anchors;
|
||||||
|
|||||||
10
TODO.md
10
TODO.md
@ -270,14 +270,16 @@ codebase; benefit is relative user/reliability impact.
|
|||||||
requests share canonical identity anchors and persist the prompt version used for each generated asset.
|
requests share canonical identity anchors and persist the prompt version used for each generated asset.
|
||||||
- [x] **P1 / S–M — Medium benefit — Preserve dialogue-only merging.** Keep the dialogue-only merge for recast/verify
|
- [x] **P1 / S–M — Medium benefit — Preserve dialogue-only merging.** Keep the dialogue-only merge for recast/verify
|
||||||
outputs where adjacent pairs still occur, with regression coverage for the non-dialogue boundaries.
|
outputs where adjacent pairs still occur, with regression coverage for the non-dialogue boundaries.
|
||||||
- [ ] **P1 / L–XL — Very high benefit — Rework segmentation from Run 3.** Inspect the earlier dialogue split/fusion
|
- [x] **P1 / L–XL — Very high benefit — Rework segmentation from Run 3.** The initial-cast final normalization now
|
||||||
stages and correct the dominant unmatched-segment class using the measured Run 3 artifacts.
|
merges narration only, preserving dialogue turn boundaries; Run 3 showed same-speaker dialogue joins were obscuring
|
||||||
|
upstream split/fusion errors. Recast/verify retains its separately tested dialogue-only merge.
|
||||||
- [ ] **P1 / M — High benefit — Re-run Book 1.** Re-measure the hand-corrected Book 1 reference after segmentation
|
- [ ] **P1 / M — High benefit — Re-run Book 1.** Re-measure the hand-corrected Book 1 reference after segmentation
|
||||||
changes; do not treat a short or incomplete run as a valid result.
|
changes; do not treat a short or incomplete run as a valid result.
|
||||||
- [ ] **P1 / L–XL — Very high benefit — Run Book 2 generalization.** Execute the backed-up Book 2 reference regression
|
- [ ] **P1 / L–XL — Very high benefit — Run Book 2 generalization.** Execute the backed-up Book 2 reference regression
|
||||||
to measure whether segmentation and attribution rules generalize beyond Book 1.
|
to measure whether segmentation and attribution rules generalize beyond Book 1.
|
||||||
- [ ] **P1 / L — Very high benefit — Unify visual identity.** Ensure portrait and concept-sheet generation use one
|
- [x] **P1 / L — Very high benefit — Unify visual identity.** Portrait and concept-sheet generation now use one
|
||||||
canonical identity description, stored prompt version, and stale-asset review path.
|
canonical identity description, stored prompt version, identity fingerprint, asset digests, and stale-asset review
|
||||||
|
path; shared-anchor and metadata persistence are covered by automated tests.
|
||||||
- [ ] **P2 / M–L — Medium–high benefit — Enrich character-form UX.** Add richer inline form editing and per-form
|
- [ ] **P2 / M–L — Medium–high benefit — Enrich character-form UX.** Add richer inline form editing and per-form
|
||||||
bulk-generation controls while preserving one character ID and one voice.
|
bulk-generation controls while preserving one character ID and one voice.
|
||||||
|
|
||||||
|
|||||||
@ -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.25.0">
|
<meta name="app-version" content="1.25.1">
|
||||||
<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,12 +27,12 @@
|
|||||||
|
|
||||||
<!-- ── 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.25.0">
|
<link rel="stylesheet" href="/static/style.css?v=1.25.1">
|
||||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.25.0">
|
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.25.1">
|
||||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.25.0">
|
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.25.1">
|
||||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.25.0">
|
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.25.1">
|
||||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.25.0">
|
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.25.1">
|
||||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.25.0">
|
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.25.1">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -397,7 +397,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.25.0"></script>
|
<script src="/static/loader.js?v=1.25.1"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -78,6 +78,27 @@ function _audiobookMergeAdjacentSameSpeaker(segments) {
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial casting must retain dialogue turn boundaries. Run 3 showed that
|
||||||
|
// same-speaker joins obscure upstream segmentation errors. Narration can
|
||||||
|
// still be compacted for paragraph flow; recast/verify uses the narrower
|
||||||
|
// dialogue-only merge below when that behavior is wanted.
|
||||||
|
function _audiobookMergeAdjacentNarration(segments) {
|
||||||
|
const out = [];
|
||||||
|
for (const s of segments) {
|
||||||
|
const last = out[out.length - 1];
|
||||||
|
const lastSpeaker = String(last?.speaker || 'Narrator').trim().toLowerCase();
|
||||||
|
const curSpeaker = String(s?.speaker || 'Narrator').trim().toLowerCase();
|
||||||
|
if (last && last.type === 'narration' && s?.type === 'narration'
|
||||||
|
&& last.page === s.page && lastSpeaker === 'narrator' && curSpeaker === 'narrator') {
|
||||||
|
const a = String(last.text || '').trimEnd(), b = String(s.text || '').trimStart();
|
||||||
|
last.text = /[.!?…»«”"’']$/.test(a) ? a + '\n\n' + b : a + ' ' + b;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push(Object.assign({}, s));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
// Merge consecutive segments spoken by the same named character. This is a
|
// Merge consecutive segments spoken by the same named character. This is a
|
||||||
// dialogue-only normalization pass; narration is intentionally left alone.
|
// dialogue-only normalization pass; narration is intentionally left alone.
|
||||||
function _audiobookMergeAdjacentDialogue(segments) {
|
function _audiobookMergeAdjacentDialogue(segments) {
|
||||||
@ -3491,6 +3512,7 @@ window.TTVC.audiobook.cast = {
|
|||||||
toMarkdown: _abCastMarkdown,
|
toMarkdown: _abCastMarkdown,
|
||||||
stampPages: _abStampSegmentPages,
|
stampPages: _abStampSegmentPages,
|
||||||
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
||||||
|
mergeAdjacentNarration: _audiobookMergeAdjacentNarration,
|
||||||
mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue,
|
mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue,
|
||||||
mergeSplitSentences: _audiobookMergeSplitSentences,
|
mergeSplitSentences: _audiobookMergeSplitSentences,
|
||||||
dedupNearbyDuplicates: _audiobookDedupNearbyDuplicates,
|
dedupNearbyDuplicates: _audiobookDedupNearbyDuplicates,
|
||||||
@ -3500,7 +3522,7 @@ window.TTVC.audiobook.cast = {
|
|||||||
|
|
||||||
Object.assign(root, {
|
Object.assign(root, {
|
||||||
_abCastMarkdown, _abStampSegmentPages, audiobookCastView,
|
_abCastMarkdown, _abStampSegmentPages, audiobookCastView,
|
||||||
_audiobookMergeAdjacentSameSpeaker, _audiobookMergeAdjacentDialogue,
|
_audiobookMergeAdjacentSameSpeaker, _audiobookMergeAdjacentNarration, _audiobookMergeAdjacentDialogue,
|
||||||
_audiobookMergeSplitSentences, _audiobookDedupNearbyDuplicates,
|
_audiobookMergeSplitSentences, _audiobookDedupNearbyDuplicates,
|
||||||
_audiobookDropPhantomSpeakers, _audiobookConsolidateSpeakerAliases,
|
_audiobookDropPhantomSpeakers, _audiobookConsolidateSpeakerAliases,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1000,7 +1000,10 @@ async function audiobookCast(overrideUrl, overrideModel, resume) {
|
|||||||
const _quoteFixedSegs = _audiobookFixOrphanedQuoteMarks(allSegments);
|
const _quoteFixedSegs = _audiobookFixOrphanedQuoteMarks(allSegments);
|
||||||
const { segments: _rejoined, merged: _mergedCount } = _audiobookMergeSplitSentences(_quoteFixedSegs);
|
const { segments: _rejoined, merged: _mergedCount } = _audiobookMergeSplitSentences(_quoteFixedSegs);
|
||||||
if (_mergedCount) console.info('[cast] rejoined', _mergedCount, 'sentence(s) split by quoted terms');
|
if (_mergedCount) console.info('[cast] rejoined', _mergedCount, 'sentence(s) split by quoted terms');
|
||||||
const _mergedSegs = _audiobookMergeAdjacentSameSpeaker(_rejoined);
|
// Preserve dialogue boundaries during the initial cast. Recast/verify
|
||||||
|
// uses the narrower dialogue-only merge; initial casting should retain
|
||||||
|
// each returned turn so segmentation errors remain visible and fixable.
|
||||||
|
const _mergedSegs = _audiobookMergeAdjacentNarration(_rejoined);
|
||||||
allSegments.length = 0;
|
allSegments.length = 0;
|
||||||
allSegments.push(..._mergedSegs);
|
allSegments.push(..._mergedSegs);
|
||||||
{ const _ph = _audiobookDropPhantomSpeakers(allSegments);
|
{ const _ph = _audiobookDropPhantomSpeakers(allSegments);
|
||||||
|
|||||||
@ -101,3 +101,17 @@ test('pure leaf modules keep private declarations behind compatibility exports',
|
|||||||
split: typeof window.TTVC?.audiobook?.segmentation?.splitByQuotes,
|
split: typeof window.TTVC?.audiobook?.segmentation?.splitByQuotes,
|
||||||
}))).toEqual({ parse: 'function', compare: 'function', dialogue: 'function', split: 'function' });
|
}))).toEqual({ parse: 'function', compare: 'function', dialogue: 'function', split: 'function' });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('initial cast normalization merges narration without fusing dialogue turns', async ({ page }) => {
|
||||||
|
await page.goto('/', { waitUntil: 'domcontentloaded' });
|
||||||
|
await page.waitForFunction(() => typeof window.navTo === 'function', null, { timeout: 15_000 });
|
||||||
|
await page.evaluate(() => window.navTo('s-reader'));
|
||||||
|
await page.waitForFunction(() => typeof window.TTVC?.audiobook?.cast?.mergeAdjacentNarration === 'function', null, { timeout: 15_000 });
|
||||||
|
const result = await page.evaluate(() => window.TTVC.audiobook.cast.mergeAdjacentNarration([
|
||||||
|
{ type: 'narration', speaker: 'Narrator', page: 1, text: 'A.' },
|
||||||
|
{ type: 'narration', speaker: 'Narrator', page: 1, text: 'B.' },
|
||||||
|
{ type: 'dialogue', speaker: 'Mira', page: 1, text: 'C.' },
|
||||||
|
{ type: 'dialogue', speaker: 'Mira', page: 1, text: 'D.' },
|
||||||
|
]));
|
||||||
|
expect(result.map(s => s.text)).toEqual(['A.\n\nB.', 'C.', 'D.']);
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user