tts-voice-creator-clone-and.../static/js/character-identity.js
mARTin-B78 1f812d2faf fix(characters): merge profiles only on real names, keep other cast names, reasoning and foreign quotes out of a character
Real-library audit (270 records) traced the data damage to:
- identity matching on ANY shared token ("Zwerg", "der Zwerg", "Mann") with
  "longer text wins" merging (Arthag b02 = Kolon's species/relationships), and
- csRosterCanonicalName renaming a sheet to the first cast name among its
  aliases, which saved 16 whole-sheet copies (Zwerg/Kolon = Arthag b03,
  Fremder = Mann, Alchimist = Promos ...).

Fixes:
- static/js/character-identity.js + core/character_records.py: generic labels
  are never identity evidence; csFindMergeKey, csConsolidateAliasDuplicates,
  csRosterCanonicalName, clSameIdentity use it; own name always wins; clUpsert
  writes to the exact-name record before any alias scan; no seed rename onto a
  name that has its own record.
- _charsheets_parse: in cast-target mode drop other cast members' names from
  aliases / first / last name (Kolon: "Sharraz Garthai"), strip reasoning
  leakage ("Warte:", "Der Text sagt ...") from every text field.
- _character_scene_evidence: only a quote naming the character is used,
  otherwise its own mannerisms (16 panels showed another character's scene).
- _character_age_category: Junge/Mädchen/boy/girl, number words, Sommer/
  Winter ages, evidence clause, age-tagged quotes; "15 Jahre älter" is no age.
- scripts/audit_character_records.py (read-only) and
  scripts/clean_character_records.py (dry run by default, --apply backs up).

Tests: tests/test_character_record_hygiene.py, tests/character-identity.test.mjs.
Docs: architecture/character-extraction.md, Manual, CHANGELOG.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:09:53 +02:00

82 lines
4.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ── Character identity rules ──────────────────────────────────────────────────
// Which alias/name tokens are evidence that two character sheets describe the
// SAME person. A token made only of common nouns, roles, ranks, species or an
// article-led description ("Zwerg", "der Zwerg", "Mann", "Fremder",
// "Alter Soldat", "Ork-General", "der Tunneltreiber") labels many people and
// is never such evidence. On the real library a shared "Zwerg"/"Mann" token
// merged or copied unrelated sheets (Arthag/Kolon, Mann/Fremder in book 03).
//
// Pure functions, no DOM: loaded in the browser before characters-library.js
// and character-sheets.js, and importable from Node for unit tests. The word
// list mirrors core/character_records.py (a Python test keeps them in sync).
(function (root) {
'use strict';
const ARTICLES = new Set(('a an das dem den der des die ein eine einem einen einer eines the').split(' '));
const GENERIC_WORDS = new Set((
'adept adepten admiral alte alten alter altes amazone anführer anführerin auelfe baron ' +
'baronin bauer befehlshaber begleiter begleiterin blacksmith blonde blonder bote botin boy brother ' +
'bruder bub bäckerstochter bäuerin bürger bürgerin bürgersohn captain chief child colonel commander ' +
'companion creature crowd dame daughter de del demon diener dienerin dritte dritter ' +
'druid druide dunkle dunkler dwarf dämon elf elfe elfen erste erster erzähler ' +
'executioner father feldherr figure fischer frau frauen fremde fremden fremder führer gast ' +
'gefährte gefährtin general gestalt geweihter girl graf greis greisin groß großadmiral große ' +
'großer gräfin guard hauptfrau hauptmann heerführer henker herr herrin herzog herzogin hexe ' +
'human händler häuptling inquisitor irre jung junge jungen junger junges junker jäger ' +
'kaiser kaiserin kapitän kaufmann kid kind king kleine kleiner knabe knecht knight ' +
'kommandant kommandantin kreatur krieger kriegerin kriegshäuptling kutscher könig königin lady leader lehrer ' +
'leute lord magd mage magier magierin man mann marschall marshal master meister ' +
'menge mensch menschen merchant messenger militärführer monster mother mutter mädchen männer mönch ' +
'nachbar nachbarin narrator navigator nonne oberst obrist officer offizier offizierin old onkel ' +
'orc ork orkin orks person priest priester priesterin prince princess prinz prinzessin ' +
'queen reiter ritter ritterin rote roter schamane schmied schreiber schwarzhaarige schwester schüler ' +
'scribe servant shaman sir sister sklave sklaven sklavin smith sohn soldat soldaten ' +
'soldatin soldier son stimme stimmen stranger tante tochter torwächter tote toter unbekannt ' +
'unbekannte unbekannter unknown unsicher vampir vampire vampirin van vater vermummt vermummte vermummten ' +
'vermummter veteran voice vom von wache wachen warrior weber wesen wirt wirtin ' +
'witch witwe witwer wizard woman wächter young zauberer zauberin zu zum zur ' +
'zweite zweiter zwerg zwerge zwergin'
).split(' '));
const GENERIC_SUFFIXES = [...GENERIC_WORDS].filter(w => w.length >= 5).sort((a, b) => b.length - a.length);
function normalizeToken(token) {
return String(token == null ? '' : token)
.replace(/\([^)]*\)/g, ' ')
.replace(/\s+/g, ' ')
.trim()
.replace(/^[\s.,;:!?"'»«„“”‚‘’]+|[\s.,;:!?"'»«„“”‚‘’]+$/g, '')
.toLowerCase();
}
function isGenericIdentityToken(token) {
const norm = normalizeToken(token);
if (!norm) return true;
const words = norm.split(/[\s-]+/).filter(Boolean);
if (!words.length) return true;
if (ARTICLES.has(words[0])) return true; // "der Henker": a description
return words.every(word => {
const bare = word.replace(/^['’.]+|['’.]+$/g, '');
if (bare.length <= 1 || ARTICLES.has(bare) || GENERIC_WORDS.has(bare)) return true;
return GENERIC_SUFFIXES.some(sfx => bare.length > sfx.length && bare.endsWith(sfx));
});
}
// Shared tokens of two lower-cased token collections that prove identity.
function sharedIdentityEvidence(a, b) {
const left = new Set([...(a || [])].map(normalizeToken));
const out = [];
for (const t of (b || [])) {
const n = normalizeToken(t);
if (n && left.has(n) && !isGenericIdentityToken(n) && !out.includes(n)) out.push(n);
}
return out;
}
const characterIdentityApi = { isGenericIdentityToken, sharedIdentityEvidence, normalizeToken, GENERIC_WORDS, ARTICLES };
root.TTVC = root.TTVC || {};
root.TTVC.characterIdentity = characterIdentityApi;
root.ttvcIsGenericIdentityToken = isGenericIdentityToken;
root.ttvcSharedIdentityEvidence = sharedIdentityEvidence;
if (typeof module !== 'undefined' && module.exports) module.exports = characterIdentityApi;
})(typeof window !== 'undefined' ? window : globalThis);