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]
|
||||
|
||||
### 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
|
||||
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;
|
||||
|
||||
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.
|
||||
- [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.
|
||||
- [ ] **P1 / L–XL — Very high benefit — Rework segmentation from Run 3.** Inspect the earlier dialogue split/fusion
|
||||
stages and correct the dominant unmatched-segment class using the measured Run 3 artifacts.
|
||||
- [x] **P1 / L–XL — Very high benefit — Rework segmentation from Run 3.** The initial-cast final normalization now
|
||||
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
|
||||
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
|
||||
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
|
||||
canonical identity description, stored prompt version, and stale-asset review path.
|
||||
- [x] **P1 / L — Very high benefit — Unify visual identity.** Portrait and concept-sheet generation now use one
|
||||
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
|
||||
bulk-generation controls while preserving one character ID and one voice.
|
||||
|
||||
|
||||
@ -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.25.0">
|
||||
<meta name="app-version" content="1.25.1">
|
||||
<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,12 +27,12 @@
|
||||
|
||||
<!-- ── 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.25.0">
|
||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.25.0">
|
||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.25.0">
|
||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.25.0">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.25.0">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.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.1">
|
||||
<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.1">
|
||||
<link rel="stylesheet" href="/static/css/rehearser.css?v=1.25.1">
|
||||
<link rel="stylesheet" href="/static/css/audiobook.css?v=1.25.1">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- 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>
|
||||
</html>
|
||||
|
||||
@ -78,6 +78,27 @@ function _audiobookMergeAdjacentSameSpeaker(segments) {
|
||||
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
|
||||
// dialogue-only normalization pass; narration is intentionally left alone.
|
||||
function _audiobookMergeAdjacentDialogue(segments) {
|
||||
@ -3491,6 +3512,7 @@ window.TTVC.audiobook.cast = {
|
||||
toMarkdown: _abCastMarkdown,
|
||||
stampPages: _abStampSegmentPages,
|
||||
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
||||
mergeAdjacentNarration: _audiobookMergeAdjacentNarration,
|
||||
mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue,
|
||||
mergeSplitSentences: _audiobookMergeSplitSentences,
|
||||
dedupNearbyDuplicates: _audiobookDedupNearbyDuplicates,
|
||||
@ -3500,7 +3522,7 @@ window.TTVC.audiobook.cast = {
|
||||
|
||||
Object.assign(root, {
|
||||
_abCastMarkdown, _abStampSegmentPages, audiobookCastView,
|
||||
_audiobookMergeAdjacentSameSpeaker, _audiobookMergeAdjacentDialogue,
|
||||
_audiobookMergeAdjacentSameSpeaker, _audiobookMergeAdjacentNarration, _audiobookMergeAdjacentDialogue,
|
||||
_audiobookMergeSplitSentences, _audiobookDedupNearbyDuplicates,
|
||||
_audiobookDropPhantomSpeakers, _audiobookConsolidateSpeakerAliases,
|
||||
});
|
||||
|
||||
@ -1000,7 +1000,10 @@ async function audiobookCast(overrideUrl, overrideModel, resume) {
|
||||
const _quoteFixedSegs = _audiobookFixOrphanedQuoteMarks(allSegments);
|
||||
const { segments: _rejoined, merged: _mergedCount } = _audiobookMergeSplitSentences(_quoteFixedSegs);
|
||||
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.push(..._mergedSegs);
|
||||
{ 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,
|
||||
}))).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