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]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### 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 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-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
|
- **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 voice-picker presentation and preview state into `rehearser-voice-picker.js`.
|
||||||
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
- [x] Extract Rehearser library persistence and IndexedDB migration into `rehearser-library.js`.
|
||||||
- [ ] Extract Audiobook parsing and casting.
|
- [ ] Extract Audiobook parsing and casting.
|
||||||
|
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
||||||
- [ ] Split `style.css` by feature.
|
- [ ] Split `style.css` by feature.
|
||||||
- [ ] Do not migrate to TypeScript or rewrite the frontend until behavior is covered by tests.
|
- [ ] 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-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-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`.
|
- ✅ `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-train.js` — mic record / STT compare.
|
||||||
- ✅ `rehearser-library.js` — SQLite persistence helpers and IndexedDB migration; import/export and auto-save remain
|
- ✅ `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.
|
in `rehearser.js` until their UI/event-handler dependencies are isolated.
|
||||||
|
|||||||
@ -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.26">
|
<meta name="app-version" content="1.24.27">
|
||||||
<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,7 +27,7 @@
|
|||||||
|
|
||||||
<!-- ── 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.26">
|
<link rel="stylesheet" href="/static/style.css?v=1.24.27">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -392,7 +392,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.26"></script>
|
<script src="/static/loader.js?v=1.24.27"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</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
|
// dialogue delimiter; treating it as one split scare-quoted words out as
|
||||||
// speakerless "dialogue" fragments (confirmed live: "Zelt", "Skipperedikt",
|
// speakerless "dialogue" fragments (confirmed live: "Zelt", "Skipperedikt",
|
||||||
// "Seulaslintan" etc.).
|
// "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
|
// Deterministic Unknown-resolution — the LLM keeps missing mechanical
|
||||||
// patterns no matter how explicitly the prompt spells them out (measured:
|
// 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
|
// ~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';
|
modal.style.display = 'flex';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
],
|
],
|
||||||
"lazy": {
|
"lazy": {
|
||||||
"s-rehearser": ["rehearser-parse", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
"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-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", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
"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", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
"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