refactor: extract audiobook cast cleanup

This commit is contained in:
mARTin-B78 2026-09-04 11:30:12 +02:00
parent 1951ce0646
commit 0d1299b19c
6 changed files with 56 additions and 56 deletions

View File

@ -9,7 +9,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
### Changed
- **Audiobook cast normalization extraction.** Same-speaker post-cast merging now lives in
`audiobook-cast.js`, including dialogue-only merging, keeping deterministic segment cleanup separate from live casting orchestration.
`audiobook-cast.js`, including dialogue-only merging, split-sentence repair, and nearby-duplicate removal,
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

View File

@ -1 +1 @@
1.24.40
1.24.41

View File

@ -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, legacy page-stamping migration, and deterministic same-speaker/dialogue post-cast normalization helpers; live casting remains in `audiobook.js`.
- ✅ `audiobook-cast.js` — cast Markdown export, legacy page-stamping migration, and deterministic speaker/sentence/duplicate 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.

View File

@ -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.40">
<meta name="app-version" content="1.24.41">
<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.40">
<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.40">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.40">
<link rel="stylesheet" href="/static/style.css?v=1.24.41">
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.41">
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.41">
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.41">
<!-- ── 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.40"></script>
<script src="/static/loader.js?v=1.24.41"></script>
</body>
</html>

View File

@ -100,6 +100,50 @@ function _audiobookMergeAdjacentDialogue(segments) {
return { segments: out, merged };
}
function _audiobookMergeSplitSentences(segments) {
const out = [];
let merged = 0;
for (const s of segments) {
const prev = out[out.length - 1];
const cur = String(s?.text || '').trim();
if (prev && prev.type === 'narration' && s?.type === 'narration' && cur) {
const prevText = String(prev.text || '').trimEnd();
const endsOpen = prevText && !/[.!?:;…»"'\)\]]$/.test(prevText);
const startsLower = /^[a-zäöüß]/.test(cur);
const prevIsInquit = new RegExp(_AB_INQUIT_VERBS + "[^.!?]{0,40},\\s*$", 'i').test(prevText);
if (endsOpen && startsLower && !prevIsInquit) {
prev.text = prevText + ' ' + cur;
merged++;
continue;
}
}
out.push(s);
}
return { segments: out, merged };
}
function _audiobookDedupNearbyDuplicates(segments) {
const QUOTE_CHARS = '»«„"‘’›‹';
const stripRe = new RegExp(`(^[${QUOTE_CHARS}\\s]+)|([${QUOTE_CHARS}\\s]+$)`, 'g');
const norm = (t) => String(t || '').replace(stripRe, '').toLowerCase().replace(/\s+/g, ' ').trim();
const WINDOW = 30;
const MIN_LEN = 20;
const out = [];
let removed = 0;
for (const s of segments) {
const nt = norm(s?.text);
if (nt.length >= MIN_LEN) {
let isDup = false;
for (let k = out.length - 1; k >= 0 && out.length - k <= WINDOW; k--) {
if (out[k].type === s.type && norm(out[k].text) === nt) { isDup = true; break; }
}
if (isDup) { removed++; continue; }
}
out.push(s);
}
return { segments: out, removed };
}
window.TTVC = window.TTVC || {};
window.TTVC.audiobook = window.TTVC.audiobook || {};
window.TTVC.audiobook.cast = {
@ -107,4 +151,6 @@ window.TTVC.audiobook.cast = {
stampPages: _abStampSegmentPages,
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue,
mergeSplitSentences: _audiobookMergeSplitSentences,
dedupNearbyDuplicates: _audiobookDedupNearbyDuplicates,
};

View File

@ -979,53 +979,6 @@ function _audiobookConsolidateSpeakerAliases(segments) {
}
window._audiobookConsolidateSpeakerAliases = _audiobookConsolidateSpeakerAliases;
function _audiobookMergeSplitSentences(segments) {
const out = [];
let merged = 0;
for (const s of segments) {
const prev = out[out.length - 1];
const cur = String(s?.text || '').trim();
if (prev && prev.type === 'narration' && s?.type === 'narration' && cur) {
const prevText = String(prev.text || '').trimEnd();
const endsOpen = prevText && !/[.!?:;…»"'\)\]]$/.test(prevText);
const startsLower = /^[a-zäöüß]/.test(cur);
// Never merge across an inquit (", flüsterte der Geist,") — that is a
// SPLIT QUOTE whose second half is really dialogue; fusing it into
// narration would bury a line the attribution passes could still fix.
const prevIsInquit = new RegExp(_AB_INQUIT_VERBS + "[^.!?]{0,40},\\s*$", 'i').test(prevText);
if (endsOpen && startsLower && !prevIsInquit) {
prev.text = prevText + ' ' + cur;
merged++;
continue;
}
}
out.push(s);
}
return { segments: out, merged };
}
function _audiobookDedupNearbyDuplicates(segments) {
const QUOTE_CHARS = '»«„"‘’›‹';
const stripRe = new RegExp(`(^[${QUOTE_CHARS}\\s]+)|([${QUOTE_CHARS}\\s]+$)`, 'g');
const norm = (t) => String(t || '').replace(stripRe, '').toLowerCase().replace(/\s+/g, ' ').trim();
const WINDOW = 30; // generous vs. the ~9-segment max context overlap between adjacent recast groups
const MIN_LEN = 20; // skip short common lines ("Ja." "Nein.") that can legitimately repeat
const out = [];
let removed = 0;
for (const s of segments) {
const nt = norm(s?.text);
if (nt.length >= MIN_LEN) {
let isDup = false;
for (let k = out.length - 1; k >= 0 && out.length - k <= WINDOW; k--) {
if (out[k].type === s.type && norm(out[k].text) === nt) { isDup = true; break; }
}
if (isDup) { removed++; continue; }
}
out.push(s);
}
return { segments: out, removed };
}
// A bare one-word "dialogue" piece with no sentence-ending punctuation is
// essentially never a real spoken turn ("Nein!", "Halt." always carry
// punctuation) — it's the signature of »quoted title/name« text (German