"""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)) if __name__ == "__main__": unittest.main()