refactor: extract audiobook parsing primitives
This commit is contained in:
parent
0bfb6d694b
commit
66ebd1a8b8
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
## [Unreleased]
|
||||
|
||||
### Changed
|
||||
- **Audiobook parser extraction.** Dialogue detection, PDF dehyphenation, and shared speaker-heuristic constants now
|
||||
live in `audiobook-parse.js`, loaded before the Audiobook casting workflow without changing its legacy API.
|
||||
- **Rehearser voice-picker extraction.** Voice preview state and matched voice-choice presentation now live in
|
||||
`rehearser-voice-picker.js`; matching, import, and assignment actions remain compatible with the existing cast API.
|
||||
- **Rehearser stage extraction.** Stage font scaling, cast-sidebar collapse state, cast filtering/sorting, and cast-strip
|
||||
|
||||
1
TODO.md
1
TODO.md
@ -127,6 +127,7 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
||||
- [x] Extract Rehearser voice-picker presentation and preview state into `rehearser-voice-picker.js`.
|
||||
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
||||
- [ ] Extract Audiobook parsing and casting.
|
||||
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
||||
- [ ] Split `style.css` by feature.
|
||||
- [ ] Do not migrate to TypeScript or rewrite the frontend until behavior is covered by tests.
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed)
|
||||
- ⬜ `rehearser-cast.js` — `renderCastList`, sort/filter, online voice picker, character research.
|
||||
- ✅ `rehearser-stage.js` — stage font/cast sidebar controls and cast-strip rendering; playback and transport remain in `rehearser.js`.
|
||||
- ✅ `rehearser-voice-picker.js` — voice preview state and matched-choice presentation; import and assignment actions remain in `rehearser.js`.
|
||||
- ✅ `audiobook-parse.js` — dialogue detection, dehyphenation, and speaker-heuristic constants; the casting pipeline remains in `audiobook.js`.
|
||||
- ⬜ `rehearser-train.js` — mic record / STT compare.
|
||||
- ✅ `rehearser-library.js` — SQLite persistence helpers and IndexedDB migration; import/export and auto-save remain
|
||||
in `rehearser.js` until their UI/event-handler dependencies are isolated.
|
||||
|
||||
@ -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.26">
|
||||
<meta name="app-version" content="1.24.27">
|
||||
<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,7 +27,7 @@
|
||||
|
||||
<!-- ── 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.26">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.24.27">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -392,7 +392,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.24.26"></script>
|
||||
<script src="/static/loader.js?v=1.24.27"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
25
static/js/audiobook-parse.js
Normal file
25
static/js/audiobook-parse.js
Normal file
@ -0,0 +1,25 @@
|
||||
// ── Audiobook parsing primitives ────────────────────────────────────────────
|
||||
// Kept dependency-free so the Reader can load these helpers before the larger
|
||||
// audiobook casting workflow. The casting pipeline continues to use the same
|
||||
// legacy global function names during the incremental migration.
|
||||
|
||||
const AB_DIALOGUE_RE = /[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/;
|
||||
function audiobookHasDialogue(t) { return AB_DIALOGUE_RE.test(t || ''); }
|
||||
|
||||
function audiobookDehyphenate(t) {
|
||||
return (t || '').replace(/([a-zäöüß])-\s+(?=[a-zäöüßA-ZÄÖÜ])/g, '$1');
|
||||
}
|
||||
|
||||
const AB_SPEECH_VERBS = '(?:sagte|fragte|rief|antwortete|erwiderte|entgegnete|meinte|flüsterte|wisperte|raunte|murmelte|brummte|knurrte|brüllte|schrie|stammelte|fauchte|zischte|seufzte|lachte|kicherte|befahl|wiederholte|fuhr\\s+fort|grunzte|versetzte|unterbrach|drängte|herrschte|keuchte|hauchte|ächzte|stöhnte|jammerte|spottete|höhnte|warf\\s+ein|setzte\\s+hinzu|erklärte|verkündete|widersprach|beharrte|gestand|bestätigte|erinnerte|dachte|überlegte|sinnierte|said|asked|replied|answered|whispered|murmured|muttered|shouted|cried|called|exclaimed|added|continued)';
|
||||
const AB_NOTNAME = new Set([
|
||||
'Der', 'Die', 'Das', 'Den', 'Dem', 'Ein', 'Eine', 'Einen', 'Er', 'Sie', 'Es', 'Ich', 'Du', 'Wir', 'Ihr', 'Man',
|
||||
'Und', 'Aber', 'Da', 'Dann', 'Doch', 'So', 'Nun', 'Jetzt', 'The', 'He', 'She', 'It', 'They', 'A', 'An', 'And', 'But', 'Then', 'Now',
|
||||
'Sofort', 'Plötzlich', 'Endlich', 'Schließlich', 'Stille', 'Schweigen', 'Stimme', 'Stimmen', 'Frage', 'Antwort',
|
||||
'Gelächter', 'Wieder', 'Gleich', 'Sogleich', 'Langsam', 'Leise', 'Laut', 'Kaum', 'Vielleicht', 'Natürlich', 'Wirklich',
|
||||
'Ja', 'Nein', 'Komm', 'Warte', 'Halt', 'Geh', 'Hier', 'Dort', 'Oben', 'Unten', 'Schon', 'Noch', 'Auch', 'Nur', 'Immer', 'Nie']);
|
||||
const _AB_NAME = "([A-ZÄÖÜ][A-Za-zäöüß'\\-]+)";
|
||||
const AB_PERSON_NOUNS = new Set(['Mann','Frau','Junge','Mädchen','Alte','Alter','Fremde','Fremder','Krieger','Kriegerin',
|
||||
'Wächter','Wache','Soldat','Hauptmann','Ork','Ritter','Magier','Magierin','Zwerg','Elf','Elfe','Händler','Wirt','Wirtin',
|
||||
'Bauer','Priester','Priesterin','Nachbar','Nachbarin','Sklave','Sklavin','Verweser','Inquisitor','General','König','Königin',
|
||||
'Prinz','Prinzessin','Fürst','Fürstin','Baron','Baronin','Bote','Diener','Dienerin','Knabe','Kind','Reiter','Reiterin',
|
||||
'Bogenschütze','Schmied','Schmiedin','Heiler','Heilerin','Gelehrte','Gelehrter','Kapitän','Anführer','Anführerin']);
|
||||
@ -624,34 +624,6 @@ function _abClearDraft() {
|
||||
// dialogue delimiter; treating it as one split scare-quoted words out as
|
||||
// speakerless "dialogue" fragments (confirmed live: "Zelt", "Skipperedikt",
|
||||
// "Seulaslintan" etc.).
|
||||
const AB_DIALOGUE_RE = /[«»„“”"‟‚‘’『「<]|(?:^|\n)\s*[—–]\s/;
|
||||
function audiobookHasDialogue(t) { return AB_DIALOGUE_RE.test(t || ''); }
|
||||
|
||||
// Join words hyphenated across a PDF line break ("Schwer- tes" → "Schwertes")
|
||||
// so the audiobook reads cleanly and speaker tags aren't split.
|
||||
function audiobookDehyphenate(t) { return (t || '').replace(/([a-zäöüß])-\s+(?=[a-zäöüßA-ZÄÖÜ])/g, '$1'); }
|
||||
|
||||
// Speech-tag heuristic so the book still casts with REAL names when the LLM is down.
|
||||
const AB_SPEECH_VERBS = '(?:sagte|fragte|rief|antwortete|erwiderte|entgegnete|meinte|flüsterte|wisperte|raunte|murmelte|brummte|knurrte|brüllte|schrie|stammelte|fauchte|zischte|seufzte|lachte|kicherte|befahl|wiederholte|fuhr\\s+fort|grunzte|versetzte|unterbrach|drängte|herrschte|keuchte|hauchte|ächzte|stöhnte|jammerte|spottete|höhnte|warf\\s+ein|setzte\\s+hinzu|erklärte|verkündete|widersprach|beharrte|gestand|bestätigte|erinnerte|dachte|überlegte|sinnierte|said|asked|replied|answered|whispered|murmured|muttered|shouted|cried|called|exclaimed|added|continued)';
|
||||
const AB_NOTNAME = new Set([
|
||||
'Der', 'Die', 'Das', 'Den', 'Dem', 'Ein', 'Eine', 'Einen', 'Er', 'Sie', 'Es', 'Ich', 'Du', 'Wir', 'Ihr', 'Man',
|
||||
'Und', 'Aber', 'Da', 'Dann', 'Doch', 'So', 'Nun', 'Jetzt', 'The', 'He', 'She', 'It', 'They', 'A', 'An', 'And', 'But', 'Then', 'Now',
|
||||
// common sentence-initial adverbs / interjections / abstractions that are NOT characters
|
||||
'Sofort', 'Plötzlich', 'Endlich', 'Schließlich', 'Stille', 'Schweigen', 'Stimme', 'Stimmen', 'Frage', 'Antwort',
|
||||
'Gelächter', 'Wieder', 'Gleich', 'Sogleich', 'Langsam', 'Leise', 'Laut', 'Kaum', 'Vielleicht', 'Natürlich', 'Wirklich',
|
||||
'Ja', 'Nein', 'Komm', 'Warte', 'Halt', 'Geh', 'Hier', 'Dort', 'Oben', 'Unten', 'Schon', 'Noch', 'Auch', 'Nur', 'Immer', 'Nie']);
|
||||
const _AB_NAME = "([A-ZÄÖÜ][A-Za-zäöüß'\\-]+)";
|
||||
|
||||
// Person/role nouns that are plausible speakers when no proper name is found.
|
||||
// A generic "der/die + any capitalized noun" fallback assigned scenery words
|
||||
// as characters (PLATZ from "auf dem Platz", GESICHTER, KLEINIGKEIT) — a
|
||||
// closed whitelist can't make that class of mistake.
|
||||
const AB_PERSON_NOUNS = new Set(['Mann','Frau','Junge','Mädchen','Alte','Alter','Fremde','Fremder','Krieger','Kriegerin',
|
||||
'Wächter','Wache','Soldat','Hauptmann','Ork','Ritter','Magier','Magierin','Zwerg','Elf','Elfe','Händler','Wirt','Wirtin',
|
||||
'Bauer','Priester','Priesterin','Nachbar','Nachbarin','Sklave','Sklavin','Verweser','Inquisitor','General','König','Königin',
|
||||
'Prinz','Prinzessin','Fürst','Fürstin','Baron','Baronin','Bote','Diener','Dienerin','Knabe','Kind','Reiter','Reiterin',
|
||||
'Bogenschütze','Schmied','Schmiedin','Heiler','Heilerin','Gelehrte','Gelehrter','Kapitän','Anführer','Anführerin']);
|
||||
|
||||
// Deterministic Unknown-resolution — the LLM keeps missing mechanical
|
||||
// patterns no matter how explicitly the prompt spells them out (measured:
|
||||
// ~44% Unknown on a full book WITH the rules in the prompt), so apply them
|
||||
@ -7034,4 +7006,3 @@ function audiobookExportDialog() {
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
],
|
||||
"lazy": {
|
||||
"s-rehearser": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
||||
"s-reader": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets"],
|
||||
"s-library": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
||||
"s-caststudio": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
||||
"s-reader": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets"],
|
||||
"s-library": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
||||
"s-caststudio": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user