refactor: extract audiobook cast cleanup
This commit is contained in:
parent
1951ce0646
commit
0d1299b19c
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user