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
|
### Changed
|
||||||
- **Audiobook cast normalization extraction.** Same-speaker post-cast merging now lives in
|
- **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
|
- **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.
|
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
|
- **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.
|
- ✅ `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-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-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/resource-diagnostics.css` — isolated Performance/resource diagnostic panel styles.
|
||||||
- ✅ `static/css/background-jobs.css` — isolated background-job history and retry-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.
|
- ✅ `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="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.24.40">
|
<meta name="app-version" content="1.24.41">
|
||||||
<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,10 +27,10 @@
|
|||||||
|
|
||||||
<!-- ── 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.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.40">
|
<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.40">
|
<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.40">
|
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.41">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -395,7 +395,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.24.40"></script>
|
<script src="/static/loader.js?v=1.24.41"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -100,6 +100,50 @@ function _audiobookMergeAdjacentDialogue(segments) {
|
|||||||
return { segments: out, merged };
|
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 = window.TTVC || {};
|
||||||
window.TTVC.audiobook = window.TTVC.audiobook || {};
|
window.TTVC.audiobook = window.TTVC.audiobook || {};
|
||||||
window.TTVC.audiobook.cast = {
|
window.TTVC.audiobook.cast = {
|
||||||
@ -107,4 +151,6 @@ window.TTVC.audiobook.cast = {
|
|||||||
stampPages: _abStampSegmentPages,
|
stampPages: _abStampSegmentPages,
|
||||||
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
mergeAdjacentSameSpeaker: _audiobookMergeAdjacentSameSpeaker,
|
||||||
mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue,
|
mergeAdjacentDialogue: _audiobookMergeAdjacentDialogue,
|
||||||
|
mergeSplitSentences: _audiobookMergeSplitSentences,
|
||||||
|
dedupNearbyDuplicates: _audiobookDedupNearbyDuplicates,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -979,53 +979,6 @@ function _audiobookConsolidateSpeakerAliases(segments) {
|
|||||||
}
|
}
|
||||||
window._audiobookConsolidateSpeakerAliases = _audiobookConsolidateSpeakerAliases;
|
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
|
// A bare one-word "dialogue" piece with no sentence-ending punctuation is
|
||||||
// essentially never a real spoken turn ("Nein!", "Halt." always carry
|
// essentially never a real spoken turn ("Nein!", "Halt." always carry
|
||||||
// punctuation) — it's the signature of »quoted title/name« text (German
|
// punctuation) — it's the signature of »quoted title/name« text (German
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user