tts-voice-creator-clone-and.../tests/test_character_identity.py
mARTin-B78 9edafe8f4a fix(characters): revert per-view turnaround renders to the single triptych
Drawing front/side/rear separately with the edit model reproduced the exact
failure the triptych was introduced to fix: three near-identical front views,
mixed styles between panels, cropped bodies, and the wireframe pose guide
rendered into the costume.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-21 09:33:23 +02:00

254 lines
12 KiB
Python
Raw 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 visual identity: fingerprint parity with the browser, and the form-aware pair review."""
from __future__ import annotations
import base64
import json
import os
import re
import subprocess
import tempfile
import unittest
from pathlib import Path
from unittest import mock
os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-charid-test-config-"))
from fastapi import FastAPI
from fastapi.testclient import TestClient
import routes.characters as characters
from routes.conversation import (
_CHARACTER_IDENTITY_FIELDS, _CHARACTER_IDENTITY_OVERRIDE_FIELDS, _CHARACTER_VISUAL_PROMPT_VERSION,
_character_identity_fingerprint,
)
ROOT = Path(__file__).resolve().parent.parent
JS = (ROOT / "static/js/library-characters.js").read_text(encoding="utf-8")
def _js_list(name: str) -> list[str]:
m = re.search(rf"const {name} = \[(.*?)\];", JS, re.S)
return re.findall(r"'([^']+)'", m.group(1))
def _js_fingerprint(sheets: list[dict]) -> list[str]:
"""Run the browser's own fingerprint code in Node."""
def grab(pattern: str) -> str:
return re.search(pattern, JS, re.S).group(0)
src = "\n".join([
grab(r"function _libStr\(v\) \{.*?\n\}"),
grab(r"const LIB_IDENTITY_FIELDS = \[.*?\];"),
grab(r"const LIB_IDENTITY_OVERRIDE_FIELDS = \[.*?\];"),
grab(r"function _libIdentityFingerprint\(sheet\) \{.*?\n\}"),
f"console.log(JSON.stringify({json.dumps(sheets)}.map(_libIdentityFingerprint)));",
])
out = subprocess.run(["node", "-e", src], capture_output=True, text=True, check=True, timeout=30)
return json.loads(out.stdout)
def _legacy_fingerprint(sheet: dict) -> str:
"""The algorithm before override fields existed, for backward-compat checks."""
raw = "|".join(f"{k}:{sheet.get(k) if sheet.get(k) is not None else ''}" for k in _CHARACTER_IDENTITY_FIELDS)
value = 0
for ch in raw:
value = ((value << 5) - value + ord(ch)) & 0xFFFFFFFF
return format(value, "x")
class FingerprintTests(unittest.TestCase):
def test_browser_and_server_use_the_same_field_lists(self):
self.assertEqual(_js_list("LIB_IDENTITY_FIELDS"), list(_CHARACTER_IDENTITY_FIELDS))
self.assertEqual(_js_list("LIB_IDENTITY_OVERRIDE_FIELDS"), list(_CHARACTER_IDENTITY_OVERRIDE_FIELDS))
def test_browser_and_server_compute_identical_fingerprints(self):
sheets = [
{},
{"physical": "tall, white hair", "clothing": "grey coat", "gender": "female", "age_estimate": "40"},
{"physical": "x", "visual_appearance": "copper hair, green eyes", "visual_costume": "blue wool coat"},
{"physical": "ä ö ü ß – »Zitat« 😀", "marks": ["scar", "tattoo"], "visual_species": "Elf"},
]
self.assertEqual(_js_fingerprint(sheets), [_character_identity_fingerprint(s) for s in sheets])
def test_a_pair_made_before_overrides_existed_keeps_a_valid_fingerprint(self):
sheet = {"physical": "tall, white hair", "clothing": "grey coat", "gender": "female"}
self.assertEqual(_character_identity_fingerprint(sheet), _legacy_fingerprint(sheet))
def test_editing_an_override_marks_the_pair_stale(self):
base = {"physical": "tall", "clothing": "coat"}
before = _character_identity_fingerprint(base)
for key in _CHARACTER_IDENTITY_OVERRIDE_FIELDS:
with self.subTest(key=key):
self.assertNotEqual(_character_identity_fingerprint({**base, key: "changed"}), before)
def test_active_form_changes_the_fingerprint_but_not_the_base(self):
sheet = {"physical": "human", "forms": [{"id": "wolf", "name": "Wolf", "physical": "grey fur"}]}
base = _character_identity_fingerprint({**sheet, "active_form_id": ""})
self.assertNotEqual(_character_identity_fingerprint({**sheet, "active_form_id": "wolf"}), base)
_PNG = base64.b64decode("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=")
def _img(n: int) -> str:
"""A distinct valid data URI per n (digests differ)."""
return "data:image/png;base64," + base64.b64encode(_PNG + bytes([n])).decode()
def _pair_meta(sheet: dict) -> dict:
return {
"consistent_prompt_version": _CHARACTER_VISUAL_PROMPT_VERSION,
"consistent_identity_fingerprint": _character_identity_fingerprint(sheet),
"identity_consistent": True,
}
class VisualPairReviewTests(unittest.TestCase):
def setUp(self):
app = FastAPI()
app.include_router(characters.router)
self.client = TestClient(app)
def review(self, records):
with mock.patch.object(characters, "char_get_all", return_value=records):
r = self.client.get("/api/characters/visual-pair-review")
self.assertEqual(r.status_code, 200)
return r.json()
def test_a_healthy_base_pair_and_a_healthy_form_pair_pass(self):
base = {"physical": "human", "clothing": "coat"}
form = {"id": "wolf", "name": "Wolf", "physical": "grey fur"}
form_fp = _character_identity_fingerprint({**base, "forms": [form], "active_form_id": "wolf"})
rec = {"id": "c1", "name": "Zerwas", "image": _img(1),
"sheet": {**base, **_pair_meta(base), "concept_art_image": _img(2),
"forms": [{**form, "image": _img(3), "concept_art_image": _img(4),
"consistent_prompt_version": _CHARACTER_VISUAL_PROMPT_VERSION,
"consistent_identity_fingerprint": form_fp, "identity_consistent": True}],
"active_form_id": "wolf"}}
self.assertEqual(self.review([rec])["issues"], [])
def test_a_stale_form_pair_is_reported_under_the_form_name(self):
base = {"physical": "human"}
form = {"id": "wolf", "name": "Wolf", "physical": "grey fur", "image": _img(3), "concept_art_image": _img(4),
"consistent_prompt_version": _CHARACTER_VISUAL_PROMPT_VERSION,
"consistent_identity_fingerprint": "deadbeef", "identity_consistent": True}
rec = {"id": "c1", "name": "Zerwas", "image": _img(1),
"sheet": {**base, **_pair_meta(base), "concept_art_image": _img(2), "forms": [form]}}
issues = self.review([rec])["issues"]
self.assertEqual(len(issues), 1)
self.assertEqual(issues[0]["form_id"], "wolf")
self.assertEqual(issues[0]["form_name"], "Wolf")
self.assertIn("appearance_changed_since_generation", issues[0]["issues"])
def test_an_active_form_no_longer_makes_the_base_pair_look_unfingerprinted(self):
base = {"physical": "human"}
rec = {"id": "c1", "name": "Zerwas", "image": _img(1),
"sheet": {**base, **_pair_meta(base), "concept_art_image": _img(2),
"forms": [{"id": "wolf", "name": "Wolf", "physical": "grey fur"}], "active_form_id": "wolf"}}
self.assertEqual(self.review([rec])["issues"], [])
def test_a_form_without_images_is_not_reviewed(self):
base = {"physical": "human"}
rec = {"id": "c1", "name": "A", "image": _img(1),
"sheet": {**base, **_pair_meta(base), "concept_art_image": _img(2),
"forms": [{"id": "f", "name": "F", "physical": "x"}]}}
self.assertEqual(self.review([rec])["issues"], [])
def test_the_same_character_reusing_its_picture_in_another_book_is_not_flagged(self):
s = {"physical": "a"}
recs = [{"id": f"b{i}::darrag", "book": f"Book {i}", "name": "Darrag", "image": _img(9),
"sheet": {**s, **_pair_meta(s), "concept_art_image": _img(20 + i)}} for i in range(2)]
recs[1]["name"] = " darrag "
self.assertEqual(self.review(recs)["issues"], [])
def test_the_same_asset_on_two_characters_is_flagged(self):
s = {"physical": "a"}
recs = [{"id": f"c{i}", "name": f"N{i}", "image": _img(9),
"sheet": {**s, **_pair_meta(s), "concept_art_image": _img(10 + i)}} for i in range(2)]
issues = self.review(recs)["issues"]
self.assertEqual({i["id"] for i in issues}, {"c0", "c1"})
self.assertTrue(all("asset_reused_across_characters" in i["issues"] for i in issues))
class PromptHygieneTests(unittest.TestCase):
"""Facts that reach an image prompt must be period-safe, uninjured, and not identical between characters."""
def facts(self, **sheet):
from routes.conversation import _character_visual_facts
return _character_visual_facts("Test", sheet)
def test_modern_wardrobe_words_become_period_garments(self):
text = self.facts(clothing="Offiziersuniform mit Weste, Hut und Sporen. Uniformjacke, Schal.")
for modern in ("uniform", "weste", "hut", "sporen", "jacke", "schal"):
self.assertNotIn(modern, text.lower(), modern)
self.assertIn("surcoat", text)
def test_injury_clauses_are_dropped_but_the_rest_of_the_sentence_stays(self):
text = self.facts(physical="Grüne Augen, blutverschmierte Stirn und schwarzes Haar. Sein Arm ist verbunden.")
self.assertIn("Grüne Augen", text)
self.assertIn("schwarzes Haar", text)
self.assertNotIn("blut", text.lower())
self.assertNotIn("verbunden", text.lower())
def test_a_stored_renaissance_book_profile_is_reworded(self):
from routes.conversation import _book_visual_context
text = _book_visual_context("B", {"era": "Frühren aissance".replace(" ", ""), "setting": "early-Renaissance fantasy world, approximately 1420–1500"})
self.assertNotIn("enaissance", text)
self.assertNotIn("1420", text)
self.assertIn("medieval", text)
def test_a_tricorn_hat_becomes_a_helm(self):
self.assertNotIn("dreispitz", self.facts(clothing="Rüstung mit Dreispitz").lower())
def test_soot_and_dirt_are_not_painted_on(self):
text = self.facts(physical="Wallendes Haar, rußverschmierte Backen.")
self.assertIn("Wallendes Haar", text)
self.assertNotIn("ruß", text.lower())
def test_injured_clothing_is_cleaned_too(self):
text = self.facts(clothing="Graue Robe, an der Seite zerrissen und blutig.")
self.assertIn("Graue Robe", text)
self.assertNotIn("blutig", text.lower())
def test_soldiers_get_different_cuts_and_colours(self):
from routes.conversation import _character_design_variation
sheet = {"clothing": "Rüstung und Schwert"}
designs = {_character_design_variation(n, sheet) for n in ("Alrik", "Baron", "Coran", "Darrag", "Elda", "Fenn", "Garbag", "Himgi")}
self.assertGreaterEqual(len(designs), 5)
self.assertEqual(_character_design_variation("Alrik", sheet), _character_design_variation("Alrik", sheet))
def test_colours_the_sheet_already_names_are_kept(self):
from routes.conversation import _character_design_variation
self.assertIn("colours already described", _character_design_variation("Alrik", {"clothing": "rote Rüstung"}))
class TurnaroundIdentityTests(unittest.TestCase):
def prompts(self, **sheet):
from routes.conversation import _build_compact_character_render_prompts
return _build_compact_character_render_prompts("Test", "Buch", sheet, {}, "cinematic", "", "")
def test_the_three_views_are_generated_as_one_triptych(self):
"""Per-view edit renders returned three front views and copied the pose guide into the costume."""
_portrait, triptych, side, back, *_rest = self.prompts(clothing="Wollrock")
self.assertIn("THREE-PANEL ORTHOGRAPHIC CHARACTER TURNAROUND", triptych)
self.assertIn("LEFT THIRD", triptych)
self.assertIn("CENTER THIRD", triptych)
self.assertIn("RIGHT THIRD", triptych)
for unused in (side, back):
self.assertIn("Unused", unused)
def test_prop_prompts_are_object_only(self):
*_, props = self.prompts(clothing="Rüstung", inventory="Schwert")
self.assertTrue(props)
for prompt in props:
for person_word in ("scalp", "expression", "CLOTHING LOCK", "hairstyle"):
self.assertNotIn(person_word, prompt)
def test_the_book_period_is_high_fantasy_medieval_not_renaissance(self):
portrait = self.prompts(clothing="Wollrock")[0]
self.assertNotIn("Renaissance", portrait)
self.assertIn("The Dark Eye", portrait)
if __name__ == "__main__":
unittest.main()