diff --git a/CHANGELOG.md b/CHANGELOG.md index ca45757..f40e8b6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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; diff --git a/TODO.md b/TODO.md index ccdfd16..c7773af 100644 --- a/TODO.md +++ b/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. diff --git a/VERSION b/VERSION index ad21919..d905a6d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.25.0 +1.25.1 diff --git a/static/index.html b/static/index.html index 1d775b9..3a36832 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,12 +27,12 @@ - - - - - - + + + + + + @@ -397,7 +397,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook-cast.js b/static/js/audiobook-cast.js index b6c14af..574af32 100644 --- a/static/js/audiobook-cast.js +++ b/static/js/audiobook-cast.js @@ -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, }); diff --git a/static/js/audiobook-pipeline.js b/static/js/audiobook-pipeline.js index 06948cc..cad889f 100644 --- a/static/js/audiobook-pipeline.js +++ b/static/js/audiobook-pipeline.js @@ -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); diff --git a/tests/lazy-loading.spec.js b/tests/lazy-loading.spec.js index 4d4faee..fec68db 100644 --- a/tests/lazy-loading.spec.js +++ b/tests/lazy-loading.spec.js @@ -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.']); +});