Consolidate speaker label variants after casting (v1.20.33)
One character is referred to by several labels across a book — bare vs full name, a title on its own, a stray fragment — and each variant became its own cast entry with its own voice, so the character audibly changed voice mid-scene. Measured against a hand-corrected book this accounted for more of the disagreements than genuine misattributions. Variants are folded together only when one label's words are a strict subset of another's after stripping articles and titles, so "Weber" and "Alter Weber" stay separate and unrelated names never merge. Applied to a real cast: Sharraz -> Sharraz Garthai, Baronin -> Baronin Ira von Seewiesen, Von -> Oberst Alrik von Blautann (40 lines, 58 -> 54 speakers). Also moves the quotation-mark toggle into the A- / A+ toolbar row. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
f837ba1a4f
commit
cd5c325265
@ -22,6 +22,14 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) · versioned wi
|
||||
- **Emotion instructions are now always written in English**, even for non-English voices (the spoken text and the native-accent clause stay in the book's own language). Confirmed by controlled A/B testing — same line, same voice, only the instruct language varying — that Qwen3-TTS follows English emotion instructions far more reliably: German instructs produced barely-differentiated output, while English instructs yield a clean, correctly-ordered prosodic gradient (whisper 128 Hz → sad 142 → neutral 179 → scared 203 → happy 225 → angry 269 Hz), with sensible duration changes too (sad slowest, scared fastest).
|
||||
- **Fish-Speech generation parameters (`temperature` / `top_p` / `repetition_penalty`) were never forwarded.** Every Fish-Speech line synthesized at the server's fixed defaults, ignoring the app's per-backend stability settings — the only backend not routed through the shared `_apply_tts_extra_params` helper.
|
||||
|
||||
## [1.20.33] — 2026-08-12
|
||||
|
||||
### Added
|
||||
- **Speaker label variants are consolidated after casting.** The caster refers to one character by several labels across a book — a bare name and a full one ("Sharraz" / "Sharraz Garthai"), a title alone ("Baronin" / "Baronin Ira von Seewiesen"), or a stray fragment ("Von" / "Oberst Alrik von Blautann"). Each variant became its own cast entry and therefore got its own voice, so a character audibly changed voice mid-scene — measured against a hand-corrected book, this accounted for more of the disagreements than genuine misattributions did. Variants are now folded together when one label's words are a strict subset of another's after stripping articles and titles, so "Weber" and "Alter Weber" stay separate and unrelated names never merge.
|
||||
|
||||
### Fixed
|
||||
- Quotation-mark toggle moved into the A− / A+ toolbar row, where it belongs.
|
||||
|
||||
## [1.20.31] — 2026-08-12
|
||||
|
||||
### Changed
|
||||
|
||||
8
static/dist/main.min.js
vendored
8
static/dist/main.min.js
vendored
File diff suppressed because one or more lines are too long
@ -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.20.31">
|
||||
<meta name="app-version" content="1.20.33">
|
||||
<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.20.31">
|
||||
<link rel="stylesheet" href="/static/style.css?v=1.20.33">
|
||||
|
||||
|
||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||
@ -378,7 +378,7 @@ window.toggleNavTree = function(treeId, chevronId) {
|
||||
</script>
|
||||
|
||||
<!-- loader.js: fetches sections → loads JS modules → removes skeleton -->
|
||||
<script src="/static/loader.js?v=1.20.31"></script>
|
||||
<script src="/static/loader.js?v=1.20.33"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1061,6 +1061,62 @@ function _audiobookMergeAdjacentSameSpeaker(segments) {
|
||||
// merges NARRATION neighbours, and only when the break is grammatically
|
||||
// impossible (no sentence-ending punctuation before, lower-case continuation
|
||||
// after), so a genuine narration→dialogue→narration sequence is never touched.
|
||||
// ── Speaker identity consolidation ──────────────────────────────────────────
|
||||
// The caster refers to one character by several labels across a book — a bare
|
||||
// name and a full one ("Sharraz" / "Sharraz Garthai"), a title on its own
|
||||
// ("Baronin" / "Baronin Ira von Seewiesen"), a stray fragment ("Von" /
|
||||
// "Oberst Alrik von Blautann"). Each variant becomes its own cast entry and so
|
||||
// gets its OWN VOICE, which is far more damaging in the finished audiobook than
|
||||
// an unresolved line: the same person audibly changes voice mid-scene.
|
||||
// Measured against a hand-corrected book, label fragmentation accounted for a
|
||||
// large share of the disagreements — more than genuine misattributions.
|
||||
// A variant is only folded into another when its words are a strict SUBSET of
|
||||
// the other's after stripping articles and titles, so "Weber" and "Alter Weber"
|
||||
// (equal after stripping) stay separate, and two unrelated names never merge.
|
||||
const _AB_TITLE_PREFIX = /^(?:der|die|das|den|dem|ein|eine|einer|herr|frau|oberst|obrist|graf|gräfin|ritter|hauptmann|kommandant|meister|bruder|schwester|prinz|prinzessin|könig|königin|alter|alte|junger|junge|blonder|blonde)\s+/i;
|
||||
function _abIdentityTokens(name) {
|
||||
let n = String(name || '').trim().toLowerCase(), prev = null;
|
||||
while (prev !== n) { prev = n; n = n.replace(_AB_TITLE_PREFIX, ''); }
|
||||
return n.split(/[\s\-]+/).filter(Boolean);
|
||||
}
|
||||
function _audiobookConsolidateSpeakerAliases(segments) {
|
||||
const count = new Map();
|
||||
for (const s of segments) {
|
||||
if (s?.type !== 'dialogue') continue;
|
||||
const sp = String(s.speaker || '').trim();
|
||||
if (!sp || /^(unknown|unbekannt|narrator)/i.test(sp)) continue;
|
||||
count.set(sp, (count.get(sp) || 0) + 1);
|
||||
}
|
||||
const names = [...count.keys()];
|
||||
const tokens = new Map(names.map(n => [n, new Set(_abIdentityTokens(n))]));
|
||||
const mapping = new Map();
|
||||
for (const a of names) {
|
||||
const ta = tokens.get(a);
|
||||
if (!ta.size) continue;
|
||||
let best = null;
|
||||
for (const b of names) {
|
||||
if (a === b) continue;
|
||||
const tb = tokens.get(b);
|
||||
if (!tb.size || tb.size <= ta.size) continue;
|
||||
let subset = true;
|
||||
for (const t of ta) if (!tb.has(t)) { subset = false; break; }
|
||||
if (!subset) continue; // strict subset only
|
||||
if (!best || count.get(b) > count.get(best) || (count.get(b) === count.get(best) && b.length > best.length)) best = b;
|
||||
}
|
||||
if (best) mapping.set(a, best);
|
||||
}
|
||||
let moved = 0;
|
||||
if (mapping.size) {
|
||||
for (const s of segments) {
|
||||
if (s?.type !== 'dialogue') continue;
|
||||
const to = mapping.get(String(s.speaker || '').trim());
|
||||
if (to) { s.speaker = to; moved++; }
|
||||
}
|
||||
}
|
||||
return { merged: mapping.size, moved, mapping };
|
||||
}
|
||||
window._audiobookConsolidateSpeakerAliases = _audiobookConsolidateSpeakerAliases;
|
||||
|
||||
function _audiobookMergeSplitSentences(segments) {
|
||||
const out = [];
|
||||
let merged = 0;
|
||||
@ -1637,7 +1693,6 @@ function audiobookCastView(total, llmUrl, defaultModel, isIdle = false) {
|
||||
<div class="ab-skel-row ab-skel-dlg"><div class="ab-skel-spk"></div><div class="ab-skel-line" style="width:64%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn-secondary ab-cv-quote-toggle" id="ab-cv-quotes" type="button" title="Show quotation marks" style="position:absolute; bottom:12px; right:16px; z-index:5; padding:8px 14px; border-radius:20px; box-shadow:0 2px 8px rgba(0,0,0,.18)"><span class="mdi mdi-format-quote-close"></span> <span class="ab-cv-quotes-label">Quotes</span></button>
|
||||
<button class="ab-cv-jump-btn" id="ab-cv-jump-btn" hidden title="Jump to latest"><span class="mdi mdi-chevron-double-down"></span> Live</button>
|
||||
</div>
|
||||
<div class="ab-cv-side" id="ab-cv-side">
|
||||
@ -2192,8 +2247,13 @@ STRIKTE FORMAT- UND TEXTREGELN:
|
||||
_abFontCtl.className = 'ab-font-ctl';
|
||||
_abFontCtl.innerHTML = `
|
||||
<button class="ab-page-nav-btn ab-font-dec" type="button" title="Decrease text size">A−</button>
|
||||
<button class="ab-page-nav-btn ab-font-inc" type="button" title="Increase text size">A+</button>`;
|
||||
<button class="ab-page-nav-btn ab-font-inc" type="button" title="Increase text size">A+</button>
|
||||
<button class="ab-page-nav-btn ab-cv-quote-toggle" id="ab-cv-quotes" type="button" title="Show quotation marks"><span class="mdi mdi-format-quote-close"></span></button>`;
|
||||
_abTopbar.appendChild(_abFontCtl);
|
||||
{ const _qb = _abFontCtl.querySelector('#ab-cv-quotes');
|
||||
if (_qb) { _qb.addEventListener('click', () => audiobookToggleQuotes());
|
||||
_qb.classList.toggle('is-on', _abShowQuotes);
|
||||
_qb.title = _abShowQuotes ? 'Hide quotation marks' : 'Show quotation marks'; } }
|
||||
const _abApplyFont = () => {
|
||||
let v = 1;
|
||||
try { v = parseFloat(localStorage.getItem('ttsvc_reh_stage_scale')) || 1; } catch (_) {}
|
||||
@ -5283,6 +5343,11 @@ async function audiobookCast(overrideUrl, overrideModel, resume) {
|
||||
const _mergedSegs = _audiobookMergeAdjacentSameSpeaker(_rejoined);
|
||||
allSegments.length = 0;
|
||||
allSegments.push(..._mergedSegs);
|
||||
{ const _ident = _audiobookConsolidateSpeakerAliases(allSegments);
|
||||
if (_ident.merged) {
|
||||
console.info('[cast] consolidated', _ident.merged, 'speaker label variant(s),', _ident.moved, 'line(s)');
|
||||
try { toast(`Merged ${_ident.merged} duplicate character label${_ident.merged !== 1 ? 's' : ''} (${_ident.moved} lines)`, 'success'); } catch (_) {}
|
||||
} }
|
||||
_audiobook.segments = allSegments;
|
||||
_audiobook.lastText = text;
|
||||
_audiobook.roster = roster;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user