diff --git a/CHANGELOG.md b/CHANGELOG.md index f32efc5..eaa44bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/TODO.md b/TODO.md index 5b1a171..bc1fe3e 100644 --- a/TODO.md +++ b/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. diff --git a/VERSION b/VERSION index e1b911a..3823fa5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.24.26 +1.24.27 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 28f0900..85bbedd 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -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. diff --git a/static/index.html b/static/index.html index 1658b43..1f954fe 100644 --- a/static/index.html +++ b/static/index.html @@ -10,7 +10,7 @@ - + @@ -27,7 +27,7 @@ - + @@ -392,7 +392,7 @@ window.toggleNavTree = function(treeId, chevronId) { - + diff --git a/static/js/audiobook-parse.js b/static/js/audiobook-parse.js new file mode 100644 index 0000000..db2318a --- /dev/null +++ b/static/js/audiobook-parse.js @@ -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']); diff --git a/static/js/audiobook.js b/static/js/audiobook.js index 2b271bb..616ca48 100644 --- a/static/js/audiobook.js +++ b/static/js/audiobook.js @@ -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'; } } - diff --git a/static/module-manifest.json b/static/module-manifest.json index 65c1e86..aaca1a5 100644 --- a/static/module-manifest.json +++ b/static/module-manifest.json @@ -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"] } }