"""Distinct, deterministic face profiles (core/face_profile.py) and their place in the prompts. The fixture roster is shaped like the real book-02 sheets: German text, most characters with no facial trait at all, a few with hair or eye colour, hedged species notes, a raven and a narrator. No image is generated and no LLM is called. """ from __future__ import annotations import os import tempfile import unittest from unittest import mock os.environ.setdefault("CONFIG_DIR", tempfile.mkdtemp(prefix="ttvc-face-test-config-")) import routes.conversation as conv from core import face_profile as fp BOOK = "Testbuch 02" def _s(gender="", age="", species="Mensch", physical="", rel="", **extra): return {"gender": gender, "age_estimate": age, "race_species": species, "physical": physical, "relationships": rel, **extra} ROSTER = [ ("Cindira", _s("female", "Junge Erwachsene", physical="Weiblich, Tränen in den Augen, zerrüttet.")), ("Marcian", _s("male", "Jung", physical="Jung, schlank genug, um sich auf einem Schemel unbehaglich zu fühlen.")), ("Marrad", _s("male", "Jung (Fast acht Sommer)", "Mensch (oder Zwerg, je nach Kontext)", "Klein, dünn, schwach.", "Vater: Darrag (Schmied)")), ("Lysandra", _s("female", "jung bis mittelalterlich", "Mensch (Amazone)", "Schwarzhaarig.")), ("Garbaz", _s("male", "mittleres Alter", "Zwerg (Amboßzwerg)", "Breit gebaut.")), ("Tschubax", _s("male", "Mittleres Alter", "Erzzwerg", "Trägt einen Bart, wirkt wie ein Gelehrter.")), ("Himgi", _s("male", "mittleres Alter", "Mensch", "Zwergengröße, sträubende Haare bei Anspannung.")), ("Nyrilla", _s("female", "unbestimmt", "Elfe", "Schlank.")), ("Eolan", _s("male", "alt", "Auelfe (Mensch-Elfen-Mischling)", "")), ("Kolon", _s("male", "Mittleres Alter", "Mensch (oder Ork-Verbündeter)", "Kleiner Mann mit einem weißen Bart.")), ("Darrag", _s("male", "Erwachsener Mann", "Mensch", "Groß, stark; gerötete Augen, unrasiertes Gesicht.", "Vater von Marrad (tot)")), ("Prinz", _s("male", "junges Erwachsenenalter", "Mensch", "Jung, schulterlanges dunkelblondes Haar, braune Augen, trägt ein Amulett.")), ("Ludara", _s("female", "junges Erwachsenenalter", "Mensch", "Blonde Haare, blaue Augen.")), ("Gernot", _s("male", "junges Erwachsenenalter", "Mensch", "Junges Aussehen, schulterlanges dunkelblondes Haar, braune Augen.")), ("Marina", _s("female", "Jungfrau (junges Mädchen)", "Mensch (wird Vampir)", "Schön. Langes, blondes Haar.")), ("Zerwas", _s("male", "alter Mann (im Aussehen)", "Vampir", "Groß und muskulös.")), ("Promos", _s("male", "Alter Mann (beugt sich, grauhaarig)", "Mensch", "Alter Mann, taumelt benommen.")), ("Graf", _s("male", "alt", "Mensch", "Alt, boshafte Gesichtszüge.")), ("Ordbert", _s("male", "mittleres Alter", "Mensch", "Hat gezwirbelte Schnauzbartspitzen.")), ("Leonardo", _s("male", "Mittleres Alter", "Mensch", "Bart, unsicherer Blick.")), ("Rialla", _s("female", "jung bis mittelalterlich", "Mensch", "groß, blonde Haare, Kriegerin.")), ("Olda", _s("female", "", "Mensch", "Schwarzhaarige")), ("Sartassa", _s("female", "jung", "Mensch", "")), ("junges Mädchen", _s("female", "Junges Mädchen", "Mensch", "Schlank, junges Mädchen.")), ("Orkschamane", _s("", "", "", "")), ("Orgrim", _s("male", "mittleres Alter", "Ork", "")), ("Bote", _s("male", "jung", "Mensch", "")), ("Wächter", _s("", "", "Mensch", "")), ("Kroah", _s("male", "unbekannt", "Rabe (magisch)", "Rabe, fliegt enge Kreise.")), ("Narrator", _s("", "", "", "")), ] def _inputs(roster=ROSTER, cast=None): names = [n for n, _ in roster] return [fp.face_input_from_sheet(n, sh, conv._character_age_category(sh), names, conv._character_species_text(sh, 100)) for n, sh in roster] def _profiles(roster=ROSTER): return fp.assign_profiles(BOOK, _inputs(roster)) class DeterminismTests(unittest.TestCase): def test_same_roster_gives_same_profiles(self): a, b = _profiles(), _profiles() self.assertEqual({n: p.values for n, p in a.items()}, {n: p.values for n, p in b.items()}) def test_other_book_gives_other_choices(self): other = fp.assign_profiles("Anderes Buch", _inputs()) mine = _profiles() self.assertNotEqual({n: p.values for n, p in mine.items()}, {n: p.values for n, p in other.items()}) def test_existing_characters_keep_their_profile_when_the_cast_grows(self): before = _profiles() extra = [("Neu Eins", _s("female", "jung", "Mensch", "")), ("Neu Zwei", _s("male", "alt", "Zwerg", ""))] after = _profiles(ROSTER + extra) for name, profile in before.items(): self.assertEqual(profile.values, after[name].values, name) self.assertIn("Neu Eins", after) def test_non_humanoids_and_the_narrator_get_no_profile(self): profiles = _profiles() self.assertNotIn("Kroah", profiles) self.assertNotIn("Narrator", profiles) self.assertIn("Orkschamane", profiles) # species taken from the name when the sheet has none self.assertEqual(profiles["Orkschamane"].species, "o") class DistinctnessTests(unittest.TestCase): def test_distinctness_beats_the_old_baseline(self): inputs = _inputs() profiles = fp.assign_profiles(BOOK, inputs) report = fp.similarity_report(inputs, profiles) self.assertLess(report["mean_distance_before"], 0.25) # the model default is shared self.assertGreater(report["mean_distance_after"], 0.8) self.assertGreater(report["mean_distance_after"], report["mean_distance_before"] + 0.55) self.assertGreater(report["characters_without_any_face_trait"], 10) def test_no_two_unrelated_characters_look_alike(self): profiles = _profiles() names = list(profiles) kin = {frozenset(("Marrad", "Darrag")), frozenset(("Himgi", "Darrag"))} worst = 0.0 for i, a in enumerate(names): for b in names[i + 1:]: if frozenset((a, b)) in kin: continue worst = max(worst, 1 - fp.distance(profiles[a].values, profiles[b].values)) self.assertLess(worst, 0.55, f"most similar unrelated pair has similarity {worst:.2f}") def test_greedy_beats_picking_by_hash_alone(self): inputs = _inputs() greedy = fp.mean_pairwise([p.values for p in fp.assign_profiles(BOOK, inputs).values()]) solo = fp.mean_pairwise([fp.assign_profiles(BOOK, [i]).get(i.name).values for i in inputs if fp.assign_profiles(BOOK, [i])]) self.assertGreater(greedy, solo) class ConstraintTests(unittest.TestCase): def setUp(self): self.profiles = _profiles() def test_no_facial_hair_for_women_children_or_unknown_gender(self): for name in ("Cindira", "Lysandra", "Nyrilla", "Ludara", "Marina", "Rialla", "Olda", "Sartassa", "junges Mädchen", "Marrad", "Orkschamane", "Wächter"): self.assertNotIn("facial_hair", self.profiles[name].values, name) self.assertNotIn("beard", self.profiles[name].clause(), name) def test_no_beard_for_a_young_woman_even_when_a_dwarf(self): sheet = _s("female", "jung", "Zwerg", "") prof = fp.assign_profiles(BOOK, [fp.face_input_from_sheet("Dwarfa", sheet, "adult", [], "Zwerg")])["Dwarfa"] self.assertNotIn("facial_hair", prof.values) def test_elves_have_no_beard_and_no_wrinkles_and_no_orc_jaw(self): for name in ("Nyrilla", "Eolan"): prof = self.profiles[name] self.assertNotIn("facial_hair", prof.values) self.assertNotIn(prof.values.get("age_signs", "elf_ageless")[:3], ("old", "mid")) self.assertNotIn("tusk_jaw", prof.values.values()) def test_human_clauses_never_mention_elf_or_orc_traits(self): for name, profile in self.profiles.items(): if profile.species == "h": text = profile.clause().lower() for word in ("pointed ear", "elf", "tusk", "protruding lower jaw", "ageless"): self.assertNotIn(word, text, f"{word!r} in {name}") def test_age_appropriate_hair_and_skin(self): for name in ("Promos", "Graf", "Zerwas"): self.assertIn(self.profiles[name].values["hair_colour"], ("grey", "white", "grey_streaked"), name) self.assertIn(self.profiles[name].values["age_signs"], ("old_wrinkled", "old_sagging", "old_lines"), name) for name in ("Marrad", "junges Mädchen"): self.assertTrue(self.profiles[name].values["age_signs"].startswith(("child", "teen")), name) self.assertNotIn(self.profiles[name].values["hair_colour"], ("grey", "white", "grey_streaked")) for name in ("Ludara", "Prinz", "Gernot"): self.assertNotIn(self.profiles[name].values["hair_colour"], ("grey", "white", "grey_streaked")) def test_white_beard_in_the_sheet_means_light_hair(self): # Kolon: "weißen Bart" (middle-aged): hair follows the beard, the beard itself is kept self.assertEqual(self.profiles["Kolon"].values["facial_hair"], "sheet_beard:white") self.assertIn(self.profiles["Kolon"].values["hair_colour"], ("grey", "white", "grey_streaked")) self.assertIn("a white beard", self.profiles["Kolon"].clause()) def test_vampire_skin_stays_pale(self): self.assertIn(self.profiles["Zerwas"].values["skin"], ("pale", "fair", "sallow", "olive")) self.assertNotIn("freckles", self.profiles["Zerwas"].values.get("mark", "")) def test_orc_and_dwarf_pools(self): self.assertIn(self.profiles["Orgrim"].values["skin"], ("olive", "dark_olive", "ash_brown", "warm_brown")) self.assertIn(self.profiles["Garbaz"].values["facial_hair"], ("stubble", "moustache", "full_beard", "long_beard", "braided_beard", "forked_beard", "square_beard")) def test_culture_named_in_the_sheet_limits_skin(self): north = _s("male", "jung", "Mensch", "", nationality_background="Thorwaler") inp = fp.face_input_from_sheet("Thor", north, "adult", []) self.assertEqual(inp.culture, "N") self.assertTrue(all(o.id not in ("warm_brown", "deep_brown") for o in fp.candidates("skin", inp))) south = _s("male", "jung", "Mensch", "", nationality_background="Tulamide aus Mhanadistan") inp = fp.face_input_from_sheet("Tula", south, "adult", []) self.assertTrue(all(o.id not in ("pale", "fair_freckled", "ruddy") for o in fp.candidates("skin", inp))) def test_alternatives_in_the_species_note_do_not_change_the_species(self): self.assertEqual(fp.species_code("Mensch (oder Zwerg, je nach Kontext)"), "h") self.assertEqual(fp.species_code("Mensch oder Ork"), "h") self.assertEqual(fp.species_code("Halbelf"), "he") self.assertEqual(fp.species_code("Rabe (magisch)"), "") self.assertEqual(fp.species_code("", "Elfriede"), "h") def test_every_species_age_gender_combination_gets_every_applicable_slot(self): for species in ("h", "e", "d", "o", "v", "he"): for stage in "ctymo": for gender in ("m", "f", ""): inp = fp.FaceInput("X", species, stage, gender, "", "") prof = fp.build_profile(inp, "b", []) missing = [s for s in fp.SLOTS if s not in prof.values and fp._slot_applicable(s, inp)] self.assertEqual(missing, [], (species, stage, gender)) if species != "e" or stage in "ct": continue self.assertNotIn(prof.values.get("age_signs", ""), ("old_wrinkled", "mid_lines")) def test_alter_the_noun_is_not_old(self): self.assertEqual(fp.age_stage("Alter unklar, erscheint als junger Mann"), "y") self.assertEqual(fp.age_stage("Alter Mann (beugt sich)"), "o") class SheetTraitTests(unittest.TestCase): def _one(self, physical, **kw): sheet = _s(kw.pop("gender", "male"), kw.pop("age", "jung"), kw.pop("species", "Mensch"), physical) return fp.assign_profiles(BOOK, [fp.face_input_from_sheet("X", sheet, "adult", [])])["X"] def test_sheet_traits_are_kept_and_marked(self): prof = self._one("Schwarzhaarig, blaue Augen, Sommersprossen, hohe Wangenknochen, Hakennase.") self.assertEqual((prof.values["hair_colour"], prof.values["eye_colour"], prof.values["mark"], prof.values["cheeks"], prof.values["nose"]), ("black", "blue", "freckles", "high", "aquiline")) for slot in ("hair_colour", "eye_colour", "mark", "cheeks", "nose"): self.assertEqual(prof.sources[slot], "sheet") self.assertIn("black hair", prof.clause()) def test_english_sheets_work_too(self): prof = self._one("Short red hair, green eyes, a broad nose and a square jaw.") self.assertEqual((prof.values["hair_colour"], prof.values["eye_colour"], prof.values["nose"], prof.values["jaw"]), ("red", "green", "broad", "square_jaw")) self.assertEqual(prof.values["hair_style"], "s:short") def test_sheet_never_contradicted(self): cases = [("Kurzes dunkles Haar, grüne Augen", {"hair_colour": "dark_brown", "eye_colour": "green"}), ("Bartlos, rundes Gesicht", {"facial_hair": "clean", "face_shape": "round"}), ("Dunkelhäutig, volle Lippen", {"skin": "deep_brown", "mouth": "full"}), ("Hair is white, eyes are grey", {"hair_colour": "white", "eye_colour": "grey"}), ("dark eyes and brown hair", {"hair_colour": "brown", "eye_colour": "dark_brown"})] for text, expected in cases: prof = self._one(text) for slot, oid in expected.items(): self.assertEqual(prof.values[slot], oid, f"{text!r}: {slot}") def test_bald_sheet_gets_no_hair_colour_or_style(self): prof = self._one("Kahlköpfig.") self.assertEqual(prof.values["hair_style"], "bald") self.assertNotIn("hair_colour", prof.values) self.assertIn("a bald head", prof.clause()) def test_scars_are_not_added_to_the_prompt_text(self): prof = self._one("Eine lange Narbe über der Wange.") self.assertEqual(prof.values["mark"], "sheet_mark") self.assertNotIn("scar", prof.clause().lower()) def test_unspecified_unrasiert_is_stubble_not_clean(self): self.assertEqual(self._one("Unrasiert.").values["facial_hair"], "stubble") def test_owner_override_replaces_the_clause_and_still_counts(self): sheet = _s("male", "jung", "Mensch", "Mager.", visual_face="narrow face, hooked nose, one blind white eye, black hair") inp = fp.face_input_from_sheet("X", sheet, "adult", []) prof = fp.assign_profiles(BOOK, [inp])["X"] self.assertEqual(prof.clause(), "face: narrow face, hooked nose, one blind white eye, black hair") self.assertEqual(prof.values["hair_colour"], "black") # parsed, so later characters avoid it self.assertEqual(prof.sources["hair_colour"], "override") class KinTests(unittest.TestCase): def test_kin_detection(self): names = ["Marrad", "Darrag", "Himgi"] self.assertEqual(fp.find_kin("Marrad", "Vater: Darrag (Schmied, trinkt viel); Himgi (Freund)", names), ("Darrag",)) self.assertEqual(fp.find_kin("Darrag", "Vater von Marrad (tot)", names), ("Marrad",)) self.assertEqual(fp.find_kin("Himgi", "Arbeitskollege von Darrag", names), ()) def test_relatives_share_family_traits_strangers_do_not(self): shared_kin = shared_other = 0 total = 12 for i in range(total): roster = [(f"Anna{i}", _s("female", "jung", "Mensch", "")), (f"Bert{i}", _s("male", "jung", "Mensch", "", rel=f"Schwester: Anna{i}")), (f"Cord{i}", _s("male", "jung", "Mensch", ""))] profiles = fp.assign_profiles(BOOK, _inputs(roster)) fam = lambda a, b: sum(profiles[a].values.get(s) == profiles[b].values.get(s) for s in fp.FAMILY_SLOTS) shared_kin += fam(f"Anna{i}", f"Bert{i}") shared_other += fam(f"Anna{i}", f"Cord{i}") self.assertGreater(shared_kin, shared_other) self.assertGreaterEqual(shared_kin / total, 1.0) # at least one inherited slot on average def test_relatives_still_differ_in_the_other_slots(self): roster = [("Anna", _s("female", "jung", "Mensch", "")), ("Bert", _s("male", "jung", "Mensch", "", rel="Bruder: Anna"))] profiles = fp.assign_profiles(BOOK, _inputs(roster)) self.assertGreater(fp.distance(profiles["Anna"].values, profiles["Bert"].values), 0.3) class PromptTests(unittest.TestCase): SHEET = _s("female", "Junge Erwachsene", "Mensch", "Blonde Haare, blaue Augen.", clothing="Ein einfaches Leinenkleid.") def _attached(self, name="Ludara", sheet=None): sheet = sheet or self.SHEET roster = [{"name": n, "sheet": sh} for n, sh in ROSTER] with mock.patch("core.database.char_face_roster", return_value=roster): return conv._attach_face_profile(name, BOOK, sheet, dict(sheet)) def test_face_clause_is_in_every_prompt_and_survives_cleaning(self): sheet = self._attached() clause = sheet["_face_clause"] self.assertTrue(clause.startswith("face: ")) self.assertIn("blonde hair", clause) portrait = conv._build_character_portrait_prompt("Ludara", BOOK, sheet, {}, "cinematic") turnaround = conv._build_character_sheet_prompt("Ludara", BOOK, sheet, {}, "cinematic") self.assertIn(clause.removeprefix("face: "), portrait) self.assertIn(clause.removeprefix("face: "), turnaround) profile, front, _, _, scene, _ = conv._build_compact_character_render_prompts( "Ludara", BOOK, sheet, {}, "cinematic", portrait, turnaround) for text in (profile, front, scene): self.assertIn(clause, text) # the injury/negation filters leave it untouched def test_hand_edited_subject_still_gets_the_face(self): sheet = self._attached() edited = "Task: x. Subject: a weathered shepherd girl in a brown dress. Context: y." profile, front, *_ = conv._build_compact_character_render_prompts( "Ludara", BOOK, sheet, {}, "cinematic", edited, edited) self.assertIn(sheet["_face_clause"], profile) self.assertIn(sheet["_face_clause"], front) def test_homogenising_boilerplate_is_gone(self): sheet = self._attached() text = " ".join(( conv._build_character_portrait_prompt("Ludara", BOOK, sheet, {}, "concept"), conv._build_character_sheet_prompt("Ludara", BOOK, sheet, {}, "concept"), *conv._build_compact_character_render_prompts("Ludara", BOOK, sheet, {}, "concept", "", "")[:2], )).lower() self.assertNotIn("consistent facial construction", text) self.assertNotIn("reaching at least to the ears or collar", text) self.assertNotIn("past the shoulders", text) def test_dwarf_beard_comes_from_the_face_clause_only(self): dwarf = _s("male", "mittleres Alter", "Zwerg", "Breit gebaut.", clothing="Kettenhemd") sheet = self._attached("Garbaz", dwarf) self.assertIn("beard", sheet["_face_clause"]) if sheet["_face_beard"] else None lock = conv._age_body_lock(dwarf)[2] self.assertNotIn("beard", lock) woman = self._attached("Dwarfa", _s("female", "jung", "Zwerg", "", clothing="Kleid")) text = " ".join(conv._build_compact_character_render_prompts("Dwarfa", BOOK, woman, {}, "cinematic", "", "")[:2]).lower() self.assertNotIn("beard", text) def test_german_to_english_path_still_feeds_the_profile(self): sheet = {**self.SHEET, "physical": "Schwarzhaarig, blaue Augen."} def chat(messages, max_tokens): import json payload = json.loads(messages[-1]["content"]) if messages[-1]["content"].lstrip().startswith("{") else {} return json.dumps({k: str(v).replace("Schwarzhaarig", "Black-haired") for k, v in payload.items()}) with mock.patch.object(conv, "_load_settings", return_value={}): en, _, _, info = conv._english_prompt_inputs("Ludara", sheet, {}, None, {}, chat=chat, cache={}) roster = [{"name": n, "sheet": sh} for n, sh in ROSTER] with mock.patch("core.database.char_face_roster", return_value=roster): prompt_sheet = conv._attach_face_profile("Ludara", BOOK, sheet, en) self.assertIn("black hair", prompt_sheet["_face_clause"]) self.assertIn("blue eyes", prompt_sheet["_face_clause"]) self.assertTrue(conv._character_visual_facts("Ludara", prompt_sheet).count("face: ") == 1) def test_cast_unreadable_falls_back_to_the_character_alone(self): with mock.patch("core.database.char_face_roster", side_effect=RuntimeError("db")): sheet = conv._attach_face_profile("Ludara", BOOK, self.SHEET, dict(self.SHEET)) self.assertTrue(sheet["_face_clause"].startswith("face: ")) def test_facts_without_cast_context_are_still_deterministic(self): a = conv._character_visual_facts("Ludara", self.SHEET) b = conv._character_visual_facts("Ludara", dict(self.SHEET)) self.assertEqual(a, b) self.assertIn("face: ", a) def test_seeds_stay_per_prompt(self): # the image seed is derived from the prompt text / generation key, never shared by the cast import inspect source = inspect.getsource(conv._comfyui_run) self.assertIn("seed_key or prompt", source) class PreviewTests(unittest.TestCase): def test_preview_lists_profiles_and_a_before_after_score(self): roster = [{"name": n, "sheet": sh} for n, sh in ROSTER] result = conv._face_preview_for_roster(BOOK, roster) self.assertEqual(result["report"]["characters"], len(result["characters"])) self.assertGreater(result["report"]["mean_distance_after"], result["report"]["mean_distance_before"]) self.assertEqual(len(result["similarity_matrix_after"]), len(result["order"])) self.assertIn("Kroah", result["skipped_without_humanoid_face"]) first = result["characters"][0] self.assertIn("slots", first) self.assertIn("source", next(iter(first["slots"].values()))) if __name__ == "__main__": unittest.main()