479 lines
25 KiB
Python
479 lines
25 KiB
Python
"""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)
|
||
first = _character_design_variation("Alrik", sheet)
|
||
second = _character_design_variation("Alrik", sheet)
|
||
self.assertEqual(first, second)
|
||
|
||
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 full-body views of the same one", triptych)
|
||
self.assertIn("left a front view, middle a side profile, right a back view", triptych)
|
||
for unused in (side, back):
|
||
self.assertIn("Unused", unused)
|
||
|
||
def test_the_turnaround_prompt_is_short_and_framing_first(self):
|
||
"""Measured: framing rules buried in a ~3 000-character prompt were ignored (feet cut off 5/5,
|
||
scenery backdrop); the short framing-first prompt passed 10/10."""
|
||
triptych = self.prompts(clothing="Wollrock", physical="Kurzes Haar.")[1]
|
||
self.assertLess(len(triptych), 1500)
|
||
self.assertTrue(triptych.startswith("Character turnaround reference sheet"))
|
||
framing = triptych.index("top of the head to the soles of the shoes")
|
||
self.assertLess(framing, triptych.index("The adult person:"))
|
||
self.assertIn("Plain empty white seamless studio background", triptych)
|
||
self.assertNotIn("..", triptych)
|
||
|
||
def test_a_long_saved_subject_is_trimmed_to_keep_the_framing_on_top(self):
|
||
triptych = self.prompts(physical="Sehr lange Beschreibung. " * 80)[1]
|
||
self.assertLess(len(triptych), 1500)
|
||
|
||
def test_the_portrait_look_is_carried_into_the_turnaround_and_the_scene(self):
|
||
from routes.conversation import _build_compact_character_render_prompts
|
||
out = _build_compact_character_render_prompts("Darrag", "Buch", {"physical": "Kräftig."}, {}, "cinematic", "", "",
|
||
"hair: shoulder-length brown hair")
|
||
self.assertIn("exactly as in the portrait: hair: shoulder-length brown hair", out[1])
|
||
self.assertIn("exactly as in the portrait: hair: shoulder-length brown hair", out[4])
|
||
self.assertLess(out[1].index("head to the soles"), out[1].index("exactly as in the portrait"))
|
||
|
||
def test_turnaround_keeps_the_rules_that_matter(self):
|
||
leather = self.prompts(clothing="Abgenutzte Lederrüstung.")[1]
|
||
self.assertIn("Leather and cloth armour only", leather)
|
||
elf = self.prompts(race_species="Elfe")[1]
|
||
self.assertIn("Pointed elf-shaped ears", elf)
|
||
child = self.prompts(physical="Ein Kind von acht Jahren.")[1]
|
||
self.assertIn("same one child", child)
|
||
|
||
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_accessory_prompts_are_isolated_props_without_body_parts(self):
|
||
*_, props = self.prompts(clothing="Rüstung", inventory="Armbrust, Schlüssel, Lederhandschuhe")
|
||
self.assertTrue(props)
|
||
for prompt in props:
|
||
self.assertIn("concept art prop sheet featuring isolated character accessories:", prompt)
|
||
self.assertIn("plain, neutral studio background", prompt)
|
||
self.assertIn("Strictly inanimate objects only", prompt)
|
||
self.assertIn("no people, no hands, no faces, and no body parts of any kind", prompt)
|
||
self.assertNotIn("oak table", prompt)
|
||
joined = " ".join(props).lower()
|
||
self.assertIn("armbrust", joined)
|
||
self.assertIn("schlüssel", joined)
|
||
|
||
def test_item_negative_prompt_reaches_the_item_node_only(self):
|
||
from routes.conversation import _SIGNATURE_ITEM_PANEL_NEGATIVE, _LOWER_PANEL_NEGATIVE
|
||
self.assertIn("hands", _SIGNATURE_ITEM_PANEL_NEGATIVE)
|
||
self.assertNotIn("boots", _SIGNATURE_ITEM_PANEL_NEGATIVE)
|
||
self.assertNotIn(_SIGNATURE_ITEM_PANEL_NEGATIVE, _LOWER_PANEL_NEGATIVE)
|
||
|
||
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)
|
||
|
||
|
||
class RenderDefectTests(unittest.TestCase):
|
||
"""Each test pins one defect seen in a real render."""
|
||
|
||
KARYLA = {"gender": "female",
|
||
"race_species": "Mensch (oder Halbelfe, je nach Kontext, hier nicht spezifiziert)",
|
||
"physical": "Kurzes blondes Haar; drahtige Statur einer Kriegerin.",
|
||
"clothing": "Robuste, abgenutzte Lederrüstung mit Waffengürtel und Schwert."}
|
||
|
||
def prompts(self, sheet, name="Test"):
|
||
from routes.conversation import _build_compact_character_render_prompts
|
||
return _build_compact_character_render_prompts(name, "Buch", sheet, {}, "cinematic", "", "")
|
||
|
||
def test_a_hedged_species_does_not_give_a_human_elf_ears(self):
|
||
from routes.conversation import _character_ear_shape, _character_visual_facts
|
||
self.assertEqual(_character_ear_shape(self.KARYLA), "ordinary rounded human ears")
|
||
self.assertNotIn("Halbelfe", _character_visual_facts("Karyla", self.KARYLA))
|
||
|
||
def test_a_real_elf_keeps_pointed_ears(self):
|
||
from routes.conversation import _character_ear_shape
|
||
self.assertEqual(_character_ear_shape({"race_species": "Elfe"}), "pointed elf-shaped ears")
|
||
|
||
def test_the_scene_panel_restates_gender_and_species(self):
|
||
scene = self.prompts(self.KARYLA, "Karyla")[4]
|
||
self.assertIn("one adult woman named Karyla", scene)
|
||
|
||
def test_a_child_is_not_described_as_an_adult_in_the_scene(self):
|
||
scene = self.prompts({"gender": "male", "physical": "Ein Kind von acht Jahren."}, "Jorin")[4]
|
||
self.assertIn("boy named Jorin", scene)
|
||
|
||
def test_scars_are_left_out_of_image_prompts(self):
|
||
"""Any scar wording -- even "fully healed, pale" -- rendered as fresh red gashes (Eolan)."""
|
||
prompts = self.prompts({"physical": "Weißhaarig. Eine lange Narbe zieht sich über die Wange.", "marks": "scar"})
|
||
for prompt in (prompts[0], prompts[1]):
|
||
self.assertIn("Weißhaarig", prompt)
|
||
self.assertNotIn("Narbe", prompt)
|
||
self.assertNotIn("scar", prompt.lower())
|
||
|
||
def test_a_gesture_of_the_moment_is_not_drawn(self):
|
||
""""Zunge streicht über die Lippen" rendered as a tongue sticking out (Eolan)."""
|
||
triptych = self.prompts({"physical": "Asketisches Gesicht. Zunge streicht über die Lippen."})[1]
|
||
self.assertIn("Asketisches Gesicht", triptych)
|
||
self.assertNotIn("Zunge", triptych)
|
||
|
||
def test_an_armed_character_is_told_how_to_hold_the_weapon(self):
|
||
scene = self.prompts(self.KARYLA, "Karyla")[4]
|
||
self.assertIn("gripped low by its hilt", scene)
|
||
self.assertIn("both hands empty", self.prompts(self.KARYLA, "Karyla")[0])
|
||
|
||
def test_an_unarmed_character_gets_no_weapon_wording(self):
|
||
self.assertNotIn("hilt", self.prompts({"clothing": "Schlichtes Leinenkleid."})[4])
|
||
|
||
|
||
class LowerPanelPromptTests(unittest.TestCase):
|
||
"""The story moment and the signature items under the turnaround.
|
||
|
||
Marcian (a knight in plate and a red cloak, Das Jahr des Greifen) got a modern man with
|
||
headphones and a camera in a garden, captioned with pseudo-text, and a stock crossbow on a
|
||
white sweep: the scene prompt was ~5 400 characters with the period rules at the end, opened
|
||
with "ears ... clearly visible" and asked for studio photography by a photographer.
|
||
"""
|
||
|
||
BOOK = "Hohlbein & Hennen - DSA - Das Jahr des Greifen 01 - Der Sturm"
|
||
MARCIAN = {
|
||
"gender": "male", "race_species": "Mensch", "title": "Ritter",
|
||
"profession": "Inquisitor, Agent der KGIA",
|
||
"visual_appearance": "Ein markanter Mann mittleren Alters mit dunklem, leicht ergrautem Haar.",
|
||
"visual_costume": "Prächtige Plattenrüstung mit einem flammend roten Umhang.",
|
||
"visual_scene": "Marcian gibt den Verteidigern von Greifenfurt einen präzisen Befehl.",
|
||
"visual_props": ["Armbrust"],
|
||
}
|
||
|
||
def prompts(self, style="cinematic", look="", book_profile=None):
|
||
from routes.conversation import _build_compact_character_render_prompts
|
||
return _build_compact_character_render_prompts("Marcian", self.BOOK, self.MARCIAN, book_profile or {},
|
||
style, "", "", look)
|
||
|
||
def test_the_scene_is_the_same_character_in_the_books_world(self):
|
||
scene = self.prompts(look="hair: shoulder-length dark brown hair")[4]
|
||
self.assertIn("medieval high-fantasy world of The Dark Eye", scene)
|
||
self.assertIn("900-1300 AD", scene)
|
||
self.assertIn("one adult man named Marcian", scene)
|
||
self.assertIn("exactly the character of the turnaround", scene)
|
||
self.assertIn("dunklem, leicht ergrautem Haar", scene)
|
||
self.assertIn("Plattenrüstung mit einem flammend roten Umhang", scene)
|
||
self.assertIn("exactly as in the portrait: hair: shoulder-length dark brown hair", scene)
|
||
self.assertIn("Greifenfurt", scene)
|
||
|
||
def test_the_scene_opens_with_the_world_and_stays_short(self):
|
||
scene = self.prompts()[4]
|
||
self.assertTrue(scene.startswith("A cinematic film still from a medieval high-fantasy epic"))
|
||
self.assertLess(scene.index("The Dark Eye"), scene.index("Marcian"))
|
||
self.assertLess(len(scene), 3200)
|
||
long_sheet = dict(self.MARCIAN, visual_scene="Er spricht sehr lange. " * 80)
|
||
from routes.conversation import _build_compact_character_render_prompts
|
||
self.assertLess(len(_build_compact_character_render_prompts(
|
||
"Marcian", self.BOOK, long_sheet, {}, "cinematic", "", "")[4]), 3200)
|
||
|
||
def test_the_lower_panels_state_a_hand_made_unmarked_world(self):
|
||
_p, _t, _s, _b, scene, props = self.prompts()
|
||
for prompt in [scene] + props:
|
||
self.assertIn("hand-made and pre-industrial", prompt)
|
||
self.assertIn("All surfaces are plain and unmarked", prompt)
|
||
self.assertIn("one single seamless picture", prompt)
|
||
|
||
def test_the_lower_panels_carry_no_modern_or_text_cue(self):
|
||
"""The encoder draws what it reads, even after "no": nothing to avoid is named positively."""
|
||
_p, _t, _s, _b, scene, props = self.prompts()
|
||
for prompt in [scene] + props:
|
||
# the accessory template itself asks for a "neutral studio background"
|
||
lower = prompt.lower().replace("neutral studio background", "")
|
||
for cue in ("photograph", "studio", "actor", "camera", "headphone", "label", "caption",
|
||
"lettered", "overlay", "output", "museum", "catalog", "head continuity",
|
||
"evidence", "lock:", "der sturm"):
|
||
self.assertNotIn(cue, lower, f"{cue!r} in {prompt[:80]}…")
|
||
|
||
def test_the_concept_style_is_painted(self):
|
||
_p, _t, _s, _b, scene, props = self.prompts(style="concept")
|
||
self.assertTrue(scene.startswith("A semi-realistic hand-painted medieval high-fantasy illustration"))
|
||
for prompt in props:
|
||
self.assertIn("A semi-realistic hand-painted medieval high-fantasy illustration", prompt)
|
||
|
||
def test_the_book_profile_setting_reaches_the_world(self):
|
||
scene = self.prompts(book_profile={"setting": "Greifenfurt, eine belagerte Stadt", "era": "Frührenaissance"})[4]
|
||
self.assertIn("Greifenfurt, eine belagerte Stadt", scene)
|
||
self.assertNotIn("renaissance", scene.lower())
|
||
|
||
def test_the_prop_is_the_characters_own_item_in_context(self):
|
||
props = self.prompts()[5]
|
||
self.assertEqual(len(props), 1)
|
||
prop = props[0]
|
||
self.assertIn("one complete medieval crossbow", prop)
|
||
self.assertIn("personal, well-used possession of Marcian", prop)
|
||
self.assertIn("Plattenrüstung mit einem flammend roten Umhang", prop)
|
||
self.assertIn("plain, neutral studio background", prop)
|
||
self.assertIn("The Dark Eye", prop)
|
||
|
||
def test_a_still_life_item_rests_on_the_table_not_the_sweep(self):
|
||
from routes.conversation import _build_compact_character_render_prompts
|
||
sheet = dict(self.MARCIAN, visual_props=["Amulett"])
|
||
prop = _build_compact_character_render_prompts("Marcian", self.BOOK, sheet, {}, "cinematic", "", "")[5][0]
|
||
self.assertIn("coiled flat on a plain surface", prop)
|
||
self.assertNotIn("sweep", prop)
|
||
|
||
def test_the_negative_prompt_names_text_and_modern_objects(self):
|
||
from routes.conversation import _LOWER_PANEL_NEGATIVE
|
||
for word in ("text", "caption", "label", "watermark", "logo", "headphones", "camera",
|
||
"modern clothing", "modern building", "suburban garden"):
|
||
self.assertIn(word, _LOWER_PANEL_NEGATIVE)
|
||
|
||
def test_the_scene_and_prop_negatives_reach_the_workflow(self):
|
||
import requests
|
||
from fastapi import HTTPException
|
||
import routes.conversation as conversation
|
||
sent = {}
|
||
|
||
def fake_post(url, json=None, **_kw):
|
||
sent["graph"] = json["prompt"]
|
||
raise requests.exceptions.ConnectionError("stop here")
|
||
|
||
with mock.patch.object(conversation, "_comfyui_upload_data_uri", return_value="x.png"), \
|
||
mock.patch.object(conversation.requests, "post", side_effect=fake_post):
|
||
with self.assertRaises(HTTPException):
|
||
conversation._comfyui_run_consistent_images(
|
||
"http://comfy", conversation._bundled_consistent_comfyui_workflow(), "data:image/png;base64,",
|
||
"p", "front", "side", "back", "scene", ["prop"], face_transfer=False)
|
||
graph = sent["graph"]
|
||
for node in ("71", "24"):
|
||
text = graph[node]["inputs"]["text"]
|
||
self.assertTrue(text.startswith("low quality")) # the workflow's own list is kept
|
||
self.assertIn("headphones", text)
|
||
self.assertIn("caption", text)
|
||
self.assertEqual(graph["70"]["inputs"]["text"], "scene")
|
||
self.assertEqual(graph["23"]["inputs"]["text"], "prop")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|