refactor: extract audiobook quote segmentation
This commit is contained in:
parent
2fde0a176d
commit
0876527554
@ -8,6 +8,8 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
|||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
- **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
|
- **Rehearser voice-design extraction.** Character development, backend selection, and backend-refresh synchronization
|
||||||
now live in `rehearser-voice-design.js` while preserving the existing cast controls and global API.
|
now live in `rehearser-voice-design.js` while preserving the existing cast controls and global API.
|
||||||
- **Voice Design stylesheet split.** Voice-design form, sample-grid, and preview-match rules now live in
|
- **Voice Design stylesheet split.** Voice-design form, sample-grid, and preview-match rules now live in
|
||||||
|
|||||||
9
TODO.md
9
TODO.md
@ -97,15 +97,15 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
|||||||
|
|
||||||
### P2 — after behavior and safety stabilize
|
### P2 — after behavior and safety stabilize
|
||||||
|
|
||||||
- [ ] **M — Add structured logs with request/job IDs.** Report provider, endpoint,
|
- [x] **M — Add structured logs with request/job IDs.** Report provider, endpoint,
|
||||||
duration and failure reason without logging prompts, keys or book contents.
|
duration and failure reason without logging prompts, keys or book contents.
|
||||||
- [ ] **M — Separate provider health from configuration status.** Show actionable
|
- [ ] **M — Separate provider health from configuration status.** Show actionable
|
||||||
connectivity and authentication errors.
|
connectivity and authentication errors.
|
||||||
- [ ] **M — Add visible job history and retry controls.** Expose recent failures and
|
- [x] **M — Add visible job history and retry controls.** Expose recent failures and
|
||||||
safe retry state to users.
|
safe retry state to users.
|
||||||
- [ ] **L — Add memory diagnostics.** Show browser heap where supported, rendered rows,
|
- [x] **L — Add memory diagnostics.** Show browser heap where supported, rendered rows,
|
||||||
cache size, active jobs, server/container memory and recent operation sizes.
|
cache size, active jobs, server/container memory and recent operation sizes.
|
||||||
- [ ] **L — Add linting and duplicate-function checks.** Run them in CI alongside the
|
- [x] **L — Add linting and duplicate-function checks.** Run them in CI alongside the
|
||||||
existing compile, bundle and SonarQube checks.
|
existing compile, bundle and SonarQube checks.
|
||||||
|
|
||||||
## P0 — Testing, data safety, and resource limits
|
## P0 — Testing, data safety, and resource limits
|
||||||
@ -133,6 +133,7 @@ architecture change: the larger Audiobook/Rehearser/style split.
|
|||||||
- [x] Extract Rehearser cast view, sorting, filtering, and toolbar wiring into `rehearser-cast.js`.
|
- [x] Extract Rehearser cast view, sorting, filtering, and toolbar wiring into `rehearser-cast.js`.
|
||||||
- [x] Extract Rehearser training transcript comparison helpers into `rehearser-train.js`.
|
- [x] Extract Rehearser training transcript comparison helpers into `rehearser-train.js`.
|
||||||
- [x] Extract Rehearser character development and backend-selection controls into `rehearser-voice-design.js`.
|
- [x] Extract Rehearser character development and backend-selection controls into `rehearser-voice-design.js`.
|
||||||
|
- [x] Extract Audiobook quote segmentation and conservative turn-taking into `audiobook-segmentation.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`.
|
- [x] Extract Audiobook parsing primitives and speaker-heuristic constants into `audiobook-parse.js`.
|
||||||
|
|||||||
@ -35,6 +35,7 @@ Done incrementally as global-scope ordered files (no ES-module change needed)
|
|||||||
- ✅ `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`.
|
||||||
- ✅ `rehearser-cast.js` — cast view, sort/filter state handlers, and cast toolbar wiring; card rendering remains in `rehearser.js`.
|
- ✅ `rehearser-cast.js` — cast view, sort/filter state handlers, and cast toolbar wiring; card rendering remains in `rehearser.js`.
|
||||||
- ✅ `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-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 and legacy page-stamping migration helpers; live casting remains in `audiobook.js`.
|
- ✅ `audiobook-cast.js` — cast Markdown export and legacy page-stamping migration 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.
|
||||||
|
|||||||
@ -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.37">
|
<meta name="app-version" content="1.24.38">
|
||||||
<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.37">
|
<link rel="stylesheet" href="/static/style.css?v=1.24.38">
|
||||||
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.37">
|
<link rel="stylesheet" href="/static/css/resource-diagnostics.css?v=1.24.38">
|
||||||
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.37">
|
<link rel="stylesheet" href="/static/css/background-jobs.css?v=1.24.38">
|
||||||
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.37">
|
<link rel="stylesheet" href="/static/css/voice-design.css?v=1.24.38">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── 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.37"></script>
|
<script src="/static/loader.js?v=1.24.38"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
63
static/js/audiobook-segmentation.js
Normal file
63
static/js/audiobook-segmentation.js
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// ── Audiobook quote segmentation ────────────────────────────────────────────
|
||||||
|
// Quote-boundary segmentation and conservative turn-taking are kept separate
|
||||||
|
// from the live LLM casting workflow. Their shared quote/heuristic helpers are
|
||||||
|
// resolved when a passage is processed, preserving the legacy global API.
|
||||||
|
|
||||||
|
function _abImplausibleDialoguePiece(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t) return true;
|
||||||
|
const words = t.split(/\s+/).filter(Boolean);
|
||||||
|
return words.length === 1 && !/[.!?…]["'’”»]?$/.test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
function audiobookSplitByQuotes(text) {
|
||||||
|
const spans = [];
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const open = text[i], close = AB_QUOTE_PAIRS[open];
|
||||||
|
if (!close) continue;
|
||||||
|
const minStart = spans.length ? spans[spans.length - 1].end : 0;
|
||||||
|
const orphan = _abOrphanClosingQuoteSpan(text, i, minStart);
|
||||||
|
if (orphan) { spans.push(orphan); i = orphan.end - 1; continue; }
|
||||||
|
if ((open === '«' || open === '»') && i > 0 && !/[\s([{—–-]/.test(text[i - 1])) continue;
|
||||||
|
const raw = text.slice(i + 1);
|
||||||
|
const closeIdx = raw.indexOf(close);
|
||||||
|
const endInRaw = _abUnclosedQuoteEnd(raw, closeIdx);
|
||||||
|
const quote = raw.slice(0, endInRaw).trim();
|
||||||
|
if (!quote) continue;
|
||||||
|
const consumedClose = closeIdx >= 0 && endInRaw === closeIdx;
|
||||||
|
spans.push({ start: i, end: i + 1 + endInRaw + (consumedClose ? 1 : 0), quote });
|
||||||
|
i = spans[spans.length - 1].end - 1;
|
||||||
|
}
|
||||||
|
if (!spans.length) return [{ speaker: 'Narrator', type: 'narration', text, emotion: '' }];
|
||||||
|
const out = []; let last = 0;
|
||||||
|
for (let k = 0; k < spans.length; k++) {
|
||||||
|
const sp = spans[k], pre = text.slice(last, sp.start);
|
||||||
|
if (pre.trim()) out.push({ speaker: 'Narrator', type: 'narration', text: pre.trim(), emotion: '' });
|
||||||
|
if (sp.quote) {
|
||||||
|
const after = text.slice(sp.end, k + 1 < spans.length ? spans[k + 1].start : text.length);
|
||||||
|
out.push({ speaker: audiobookGuessSpeaker(after, pre) || 'Unknown', type: 'dialogue', text: sp.quote, emotion: '' });
|
||||||
|
}
|
||||||
|
last = sp.end;
|
||||||
|
}
|
||||||
|
const tail = text.slice(last).trimEnd();
|
||||||
|
if (tail.trim()) out.push({ speaker: 'Narrator', type: 'narration', text: tail.trim(), emotion: '' });
|
||||||
|
return audiobookTurnTaking(out);
|
||||||
|
}
|
||||||
|
|
||||||
|
function audiobookTurnTaking(segs) {
|
||||||
|
let a = null, b = null;
|
||||||
|
for (const s of segs) {
|
||||||
|
if (s.type !== 'dialogue') continue;
|
||||||
|
if (s.speaker && !/^Unknown|Unbekannt/i.test(s.speaker)) {
|
||||||
|
if (s.speaker !== b) { a = b; b = s.speaker; }
|
||||||
|
} else if (a && b && a !== b) {
|
||||||
|
s.speaker = a;
|
||||||
|
const t = a; a = b; b = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return segs;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.TTVC = window.TTVC || {};
|
||||||
|
window.TTVC.audiobook = window.TTVC.audiobook || {};
|
||||||
|
window.TTVC.audiobook.segmentation = { splitByQuotes: audiobookSplitByQuotes, turnTaking: audiobookTurnTaking };
|
||||||
@ -1355,71 +1355,6 @@ function _audiobookMergeAdjacentDialogue(segments) {
|
|||||||
return { segments: out, merged };
|
return { segments: out, merged };
|
||||||
}
|
}
|
||||||
|
|
||||||
function _abImplausibleDialoguePiece(text) {
|
|
||||||
const t = String(text || '').trim();
|
|
||||||
if (!t) return true;
|
|
||||||
const words = t.split(/\s+/).filter(Boolean);
|
|
||||||
return words.length === 1 && !/[.!?…]["'’”»]?$/.test(t);
|
|
||||||
}
|
|
||||||
|
|
||||||
function audiobookSplitByQuotes(text) {
|
|
||||||
const spans = [];
|
|
||||||
for (let i = 0; i < text.length; i++) {
|
|
||||||
const open = text[i];
|
|
||||||
const close = AB_QUOTE_PAIRS[open];
|
|
||||||
if (!close) continue;
|
|
||||||
const minStart = spans.length ? spans[spans.length - 1].end : 0;
|
|
||||||
const orphan = _abOrphanClosingQuoteSpan(text, i, minStart);
|
|
||||||
if (orphan) {
|
|
||||||
spans.push(orphan);
|
|
||||||
i = orphan.end - 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if ((open === '«' || open === '»') && i > 0 && !/[\s([{—–-]/.test(text[i - 1])) continue;
|
|
||||||
const raw = text.slice(i + 1);
|
|
||||||
const closeIdx = raw.indexOf(close);
|
|
||||||
const endInRaw = _abUnclosedQuoteEnd(raw, closeIdx);
|
|
||||||
const quote = raw.slice(0, endInRaw).trim();
|
|
||||||
if (!quote) continue;
|
|
||||||
const consumedClose = closeIdx >= 0 && endInRaw === closeIdx;
|
|
||||||
spans.push({ start: i, end: i + 1 + endInRaw + (consumedClose ? 1 : 0), quote });
|
|
||||||
i = spans[spans.length - 1].end - 1;
|
|
||||||
}
|
|
||||||
if (!spans.length) return [{ speaker: 'Narrator', type: 'narration', text, emotion: '' }];
|
|
||||||
const out = []; let last = 0;
|
|
||||||
for (let k = 0; k < spans.length; k++) {
|
|
||||||
const sp = spans[k];
|
|
||||||
const pre = text.slice(last, sp.start);
|
|
||||||
if (pre.trim()) out.push({ speaker: 'Narrator', type: 'narration', text: pre.trim(), emotion: '' });
|
|
||||||
if (sp.quote) {
|
|
||||||
const after = text.slice(sp.end, k + 1 < spans.length ? spans[k + 1].start : text.length);
|
|
||||||
const speaker = audiobookGuessSpeaker(after, pre) || 'Unknown';
|
|
||||||
out.push({ speaker, type: 'dialogue', text: sp.quote, emotion: '' });
|
|
||||||
}
|
|
||||||
last = sp.end;
|
|
||||||
}
|
|
||||||
const tail = text.slice(last).trimEnd();
|
|
||||||
if (tail.trim()) out.push({ speaker: 'Narrator', type: 'narration', text: tail.trim(), emotion: '' });
|
|
||||||
return audiobookTurnTaking(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fill 'Unknown' dialogue speakers by two-person alternation — but only once TWO
|
|
||||||
// distinct named speakers are established nearby (conservative: won't guess in a
|
|
||||||
// monologue, so it rarely invents a wrong name).
|
|
||||||
function audiobookTurnTaking(segs) {
|
|
||||||
let a = null, b = null; // two most recent distinct named speakers (b = latest)
|
|
||||||
for (const s of segs) {
|
|
||||||
if (s.type !== 'dialogue') continue;
|
|
||||||
if (s.speaker && !/^Unknown|Unbekannt/i.test(s.speaker)) {
|
|
||||||
if (s.speaker !== b) { a = b; b = s.speaker; }
|
|
||||||
} else if (a && b && a !== b) {
|
|
||||||
s.speaker = a; // the other of the two → alternate
|
|
||||||
const t = a; a = b; b = t; // rotate so the next Unknown alternates back
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return segs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function audiobookIsRouterModel(model) {
|
function audiobookIsRouterModel(model) {
|
||||||
return /^auto[-_ ]?router/i.test(String(model || '').trim());
|
return /^auto[-_ ]?router/i.test(String(model || '').trim());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,8 +5,8 @@
|
|||||||
],
|
],
|
||||||
"lazy": {
|
"lazy": {
|
||||||
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
"s-rehearser": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker"],
|
||||||
"s-reader": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets"],
|
"s-reader": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook", "character-sheets"],
|
||||||
"s-library": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
"s-library": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern"],
|
||||||
"s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
"s-caststudio": ["rehearser-parse", "rehearser-cast", "rehearser-train", "rehearser-voice-design", "rehearser", "rehearser-library", "rehearser-stage", "rehearser-voice-picker", "characters-library", "reader", "audiobook/network", "audiobook-parse", "audiobook-segmentation", "audiobook-cast", "audiobook", "character-sheets", "library", "character-columns", "library-characters", "sillytavern", "studio"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user