Reject phantom speakers invented from German noun phrases (v1.20.35)
The caster emits fragments of noun phrases as characters — "in seinem Inneren" -> Inneren, "die Hand" -> Hand, "mein Geliebter" -> Geliebten, "der Wind" -> Wind. Each became a cast entry with its own voice while the real speaker's line was lost to a phantom; measured against a hand-corrected book this was a large part of why a fresh cast produced 73 speakers where the human had 62. Such lines are now reset to Unknown — visibly unresolved and still recoverable by the repair passes, rather than silently wrong. Applied to a real cast: 5 phantoms removed, 73 -> 68 speakers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
4d3e4f8e7c
commit
35cb21ad70
6
static/dist/main.min.js
vendored
6
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="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.20.34">
|
<meta name="app-version" content="1.20.35">
|
||||||
<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.20.34">
|
<link rel="stylesheet" href="/static/style.css?v=1.20.35">
|
||||||
|
|
||||||
|
|
||||||
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
<!-- ── Flag icons — non-blocking (loaded async, icons appear after JS) ── -->
|
||||||
@ -378,7 +378,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.20.34"></script>
|
<script src="/static/loader.js?v=1.20.35"></script>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1079,6 +1079,36 @@ function _abIdentityTokens(name) {
|
|||||||
while (prev !== n) { prev = n; n = n.replace(_AB_TITLE_PREFIX, ''); }
|
while (prev !== n) { prev = n; n = n.replace(_AB_TITLE_PREFIX, ''); }
|
||||||
return n.split(/[\s\-]+/).filter(Boolean);
|
return n.split(/[\s\-]+/).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
// Speaker labels that are really fragments of a German noun phrase, not people:
|
||||||
|
// "in seinem Inneren" -> Inneren, "die Hand" -> Hand, "mein Geliebter" ->
|
||||||
|
// Geliebten, "der Wind" -> Wind. The caster emits these as characters, each then
|
||||||
|
// gets a cast entry and its own voice, and the real speaker's line is lost to a
|
||||||
|
// phantom — measured against a hand-corrected book, they were a large part of
|
||||||
|
// why a fresh cast produced 73 speakers where the human had 62. A line credited
|
||||||
|
// to one of these is better left Unknown: visibly unresolved, and still
|
||||||
|
// recoverable by the repair passes, instead of silently wrong.
|
||||||
|
const _AB_NOT_A_CHARACTER = new Set([
|
||||||
|
'inneren','innere','herz','herzen','hand','hände','haenden','kopf','stimme','stimmen','wind','feuer','flamme','flammen',
|
||||||
|
'dunkelheit','stille','schatten','licht','nacht','angst','schmerz','gedanke','gedanken','traum','träume','geliebten',
|
||||||
|
'geliebter','geliebte','blondschopf','antwort','echo','ferne','nähe','naehe','tiefe','wasser','regen','donner','erde',
|
||||||
|
'himmel','sonne','mond','tod','leben','seele','geist der','wahrheit','zweifel','hoffnung','erinnerung','vergangenheit'
|
||||||
|
]);
|
||||||
|
function _abIsImplausibleSpeaker(name) {
|
||||||
|
const n = String(name || '').trim().toLowerCase().replace(/^(?:der|die|das|den|dem|ein|eine|einer|einem)\s+/, '');
|
||||||
|
if (!n) return false;
|
||||||
|
return _AB_NOT_A_CHARACTER.has(n);
|
||||||
|
}
|
||||||
|
function _audiobookDropPhantomSpeakers(segments) {
|
||||||
|
let dropped = 0;
|
||||||
|
const names = new Set();
|
||||||
|
for (const s of segments) {
|
||||||
|
if (s?.type !== 'dialogue') continue;
|
||||||
|
if (_abIsImplausibleSpeaker(s.speaker)) { names.add(s.speaker); s.speaker = 'Unknown'; dropped++; }
|
||||||
|
}
|
||||||
|
return { dropped, names: [...names] };
|
||||||
|
}
|
||||||
|
window._audiobookDropPhantomSpeakers = _audiobookDropPhantomSpeakers;
|
||||||
|
|
||||||
function _audiobookConsolidateSpeakerAliases(segments) {
|
function _audiobookConsolidateSpeakerAliases(segments) {
|
||||||
const count = new Map();
|
const count = new Map();
|
||||||
for (const s of segments) {
|
for (const s of segments) {
|
||||||
@ -4790,6 +4820,8 @@ async function audiobookRecastUnknown(overrideUrl, overrideModel, options = {})
|
|||||||
// label variants the cast-time consolidation just removed — confirmed live:
|
// label variants the cast-time consolidation just removed — confirmed live:
|
||||||
// a clean cast gained "Alrik" alongside "Alrik von Blautann" during
|
// a clean cast gained "Alrik" alongside "Alrik von Blautann" during
|
||||||
// auto-repair, which would have split one character across two voices again.
|
// auto-repair, which would have split one character across two voices again.
|
||||||
|
{ const _ph = _audiobookDropPhantomSpeakers(segs);
|
||||||
|
if (_ph.dropped) console.info('[recast] dropped', _ph.dropped, 'phantom speaker line(s):', _ph.names.join(', ')); }
|
||||||
{ const _ident = _audiobookConsolidateSpeakerAliases(segs);
|
{ const _ident = _audiobookConsolidateSpeakerAliases(segs);
|
||||||
if (_ident.merged) {
|
if (_ident.merged) {
|
||||||
console.info('[recast] consolidated', _ident.merged, 'speaker label variant(s),', _ident.moved, 'line(s)');
|
console.info('[recast] consolidated', _ident.merged, 'speaker label variant(s),', _ident.moved, 'line(s)');
|
||||||
@ -5352,6 +5384,8 @@ async function audiobookCast(overrideUrl, overrideModel, resume) {
|
|||||||
const _mergedSegs = _audiobookMergeAdjacentSameSpeaker(_rejoined);
|
const _mergedSegs = _audiobookMergeAdjacentSameSpeaker(_rejoined);
|
||||||
allSegments.length = 0;
|
allSegments.length = 0;
|
||||||
allSegments.push(..._mergedSegs);
|
allSegments.push(..._mergedSegs);
|
||||||
|
{ const _ph = _audiobookDropPhantomSpeakers(allSegments);
|
||||||
|
if (_ph.dropped) console.info('[cast] dropped', _ph.dropped, 'phantom speaker line(s):', _ph.names.join(', ')); }
|
||||||
{ const _ident = _audiobookConsolidateSpeakerAliases(allSegments);
|
{ const _ident = _audiobookConsolidateSpeakerAliases(allSegments);
|
||||||
if (_ident.merged) {
|
if (_ident.merged) {
|
||||||
console.info('[cast] consolidated', _ident.merged, 'speaker label variant(s),', _ident.moved, 'line(s)');
|
console.info('[cast] consolidated', _ident.merged, 'speaker label variant(s),', _ident.moved, 'line(s)');
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user