Last Dwarf has accumulated duplicate speakers from successive discovery and
designation scans: The Girl / The Girl helping / One Girl Helping, Guard 1 / The
first guard, Walking oldest / The Oldest, plus three lowercase spoken names that
differ from their profiles by one letter. The operator wants them eliminated and
was explicit that created and approved voices must not be lost.
umbraaudio/analysis/speaker_dedup.py
Read-only fact gathering (profiles, ledger, voice refs, chapter spread) and a
matcher that explains itself: determiner and ordinal normalization, superlative
stemming, and three VETO rules, because string similarity alone would merge
people who are not the same person:
- distinguishing modifiers: The Oldest is not The Tallest; Guard 1 is not Guard 2
- role nouns: Soldier (Rider) is not One Rider
- proper names need ratio >= 0.88: Anfisa (0.77 from Tanisha) is a different
character; Rielan/Rieland at 0.93 is a misspelling
Verdicts auto / review / blocked. A group with two different voice references is
blocked, because merging would discard a generated voice. Survivor selection
prefers an approval, then a voice reference, then utterance volume.
Two rules were written and then removed, recorded in the spec so nobody re-adds
them: a "shared token plus role noun" rule proposed The Oldest = The Old Woman,
and a "single shared token" rule proposed Scum leader = The Leader. A missed
duplicate costs one manual merge; a wrong one silently rewrites a speaker.
Two real bugs, both found by running against a COPY of Last Dwarf rather than by
reading code:
1. orphan_resolution.merge_speakers never touches voice_status.json, so merging
an approved speaker into an unapproved target would have deleted the approval
record silently. merge_group() now carries the approval to the survivor,
drops the loser's entry, and reports approvals_carried.
2. The first re-point was unscoped and moved 5,981 rows instead of 3: `narrator`
is the labelling convention for ~5,978 NARRATION rows too, and renaming them
would invalidate thousands of clips because every marker records the speaker
exactly. The re-point is now dialogue-only. Narration casing is a separate,
deliberate decision and is flagged as such, not taken by a duplicate scan.
Verified on a copy: 93 -> 84 profiles, 23 utterances re-pointed, narration
untouched, 72 -> 72 voice ref files with zero removed, 13 -> 13 approvals with
zero lost.
Last Dwarf scan: 10 auto, 2 review, 4 blocked. Blocked groups are exactly the
ones the operator must decide - The Girl (two voices for one person), Officer,
the three-way Narrator family, and Border Soldier vs Soldier (Rider).
Nothing was applied to the live project. tests/test_speaker_dedup.py: 37 tests,
vetoes weighted as heavily as positives. Full suite 575 passed / 8 failed, the
same 8 pre-existing failures. ruff clean.