"""Unit tests for layered Anima prompt assembly (no LLM).""" import os import sys from unittest.mock import patch import pytest # Ensure project root on path sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) from services import sd_prompt as sp @pytest.fixture def anima(): with patch.object(sp, "_is_anima", return_value=True), patch.object(sp, "_is_pony", return_value=False): yield PERSONA_WOLF = { "appearance_tags": "wolfgirl, white_hair, golden_eyes, wolf_ears, tail, big_breast", "appearance_prose": "", "lora_name": "", } PERSONA_CARRIE = { "appearance_tags": "short_hair, brown_hair, blue_eyes, skinny", "appearance_prose": "", "lora_name": "", } def test_walking_scene_includes_action_tags_and_contextual_pov(anima): scene = sp._sanitize_scene_fields({ "shot_type": "first_person_pov", "pov_cue": "walking_together", "viewer_body_visible": False, "action_tags": "holding_hands, walking, smiling, looking_at_each_other", "environment_tags": "outdoors, sunlight, golden_hour", "scene_description": "She walks beside you, laughter in the warm afternoon light.", }) hybrid = sp.build_positive_prompt_hybrid(scene, PERSONA_WOLF, "") assert "walking" in hybrid assert "smiling" in hybrid assert "holding_hands" not in hybrid assert "looking_at_each_other" not in hybrid assert "outdoors" in hybrid assert "threshold" not in hybrid.lower() assert "POV: walking beside you" in hybrid assert "someone" not in hybrid.lower() assert "both " not in hybrid.lower() def test_hybrid_differs_from_tags_only_when_prose_present(anima): scene = { "shot_type": "first_person_pov", "pov_cue": "walking_together", "viewer_body_visible": False, "action_tags": "holding_hands, walking", "environment_tags": "outdoors, sunlight", "scene_description": "Shared laughter drifts through the golden afternoon.", } tags_only = sp.build_positive_prompt_tags_only(scene, PERSONA_WOLF, "") hybrid = sp.build_positive_prompt_hybrid(scene, PERSONA_WOLF, "") assert tags_only != hybrid assert "Shared laughter" in hybrid assert "Shared laughter" not in tags_only def test_carrie_doorway_scene(anima): scene = { "shot_type": "first_person_pov", "pov_cue": "doorway_invite", "viewer_body_visible": False, "action_tags": "arms_out, inviting_hug, smirk, looking_at_viewer", "environment_tags": "doorway, apartment, night, indoors", "scene_description": "She waits in the doorway with playful hunger in half-lidded eyes.", } outfit = "crop_top, ripped_jeans, black_jeans" hybrid = sp.build_positive_prompt_hybrid(scene, PERSONA_CARRIE, outfit) assert "arms_out" in hybrid assert "doorway" in hybrid assert "crop_top" in hybrid assert "threshold" not in hybrid.lower() assert "POV: she blocks the doorway" in hybrid def test_pov_inferred_from_action_when_cue_missing(anima): scene = { "shot_type": "first_person_pov", "action_tags": "holding_hands, walking, smiling", "environment_tags": "outdoors, park", "scene_description": "", } tags = sp.build_positive_prompt_tags_only(scene, PERSONA_WOLF, "") assert "POV: walking beside you" in tags def test_negative_includes_interaction_block_for_pov_contact(anima): scene = { "shot_type": "first_person_pov", "viewer_body_visible": False, "action_tags": "arms_out, hug, inviting_hug", "environment_tags": "doorway", } neg = sp._negative_for_scene(scene) assert "duplicate" in neg assert "extra_person" in neg assert "third person" in neg def test_scene_should_generate_false(): assert sp._scene_should_generate({"should_generate": False}) is False assert sp._scene_should_generate({"should_generate": True}) is True assert sp._scene_should_generate({}) is True def test_format_builder_user_block_illustrate_vs_context(anima): messages = [ {"role": "assistant", "content": "Long old first_mes " + ("x" * 900)}, {"role": "user", "content": "Hi"}, {"role": "assistant", "content": "*walks holding your hand*"}, ] block = sp._format_builder_user_block(PERSONA_WOLF, messages, "[]") assert "=== ILLUSTRATE" in block assert "=== Context" in block assert "*walks holding your hand*" in block assert "Long old first_mes" in block assert len(block.split("Long old first_mes")[1].split("assistant:")[0]) < 900 def test_bundle_from_scene_anima_uses_hybrid_as_tag_full(anima): scene = { "should_generate": True, "shot_type": "first_person_pov", "pov_cue": "face_to_face", "action_tags": "smiling", "environment_tags": "indoors", "scene_description": "A warm smile greets you.", } with patch.object(sp, "anima_dual_enabled", return_value=False): bundle = sp._bundle_from_scene(scene, PERSONA_WOLF, "") assert "A warm smile" in bundle.tag_full assert bundle.desc_full is None def test_user_example_walking_llm_output_cleaned(anima): """Regression: LLM prose/sentence leakage and second-person refs.""" scene = sp._sanitize_scene_fields({ "shot_type": "first_person_pov", "pov_cue": "walking_together", "action_tags": ( "holding_hands, walking, smiling, looking_at_each_other, " "A wolfgirl walks hand in hand with someone, both smiling and chatting" ), "environment_tags": "outdoor, daylight, path", "scene_description": ( "A wolfgirl walks hand in hand with someone, both smiling and chatting under the daylight." ), }) persona = {**PERSONA_WOLF, "appearance_tags": PERSONA_WOLF["appearance_tags"] + ", pumped_up"} tags_only = sp.build_positive_prompt_tags_only(scene, persona, "") hybrid = sp.build_positive_prompt_hybrid(scene, persona, "") assert "pumped_up" not in tags_only assert "someone" not in hybrid.lower() assert "both " not in hybrid.lower() assert ". A wolfgirl walks" not in tags_only assert tags_only != hybrid or not scene.get("scene_description") def test_user_example_carrie_env_reconciled(anima): scene = sp._sanitize_scene_fields({ "shot_type": "first_person_pov", "pov_cue": "doorway_invite", "action_tags": "arms_out, inviting_hug, smirk, half-lidded_eyes", "environment_tags": "doorway, nighttime, outdoor", "scene_description": ( "Carrie stands in her doorway at night, arms outstretched toward you with a mischievous smirk." ), }) hybrid = sp.build_positive_prompt_hybrid( scene, PERSONA_CARRIE, "crop_top, ripped_jeans, black_jeans, jeans" ) assert "outdoor" not in hybrid.lower() or "doorway" in hybrid assert ", jeans," not in f", {hybrid}," assert "someone" not in hybrid.lower() def test_long_first_mes_uses_final_beat(anima): carrie_tail = ( "About an hour later...\n\n" "Carrie stood at her front door, arms out, smirking. " '"Come on, hug me. Now." It\'s getting cold out.' ) long = ("She shops for clothes.\n\n" * 5) + carrie_tail excerpt = sp._extract_illustrate_content(long) assert "front door" in excerpt or "hug me" in excerpt assert "shops for clothes" not in excerpt def test_hybrid_gets_fallback_when_no_scene_description(anima): scene = sp._sanitize_scene_fields({ "shot_type": "first_person_pov", "pov_cue": "walking_together", "action_tags": "walking, smiling", "environment_tags": "outdoor, daylight", "scene_description": "", }) tags_only = sp.build_positive_prompt_tags_only(scene, PERSONA_WOLF, "") hybrid = sp.build_positive_prompt_hybrid(scene, PERSONA_WOLF, "") assert hybrid != tags_only assert "afternoon" in hybrid.lower() or "laughter" in hybrid.lower() def test_yuki_pov_drops_lifting_and_nose_rub(anima): scene = sp._sanitize_scene_fields({ "shot_type": "first_person_pov", "pov_cue": "face_to_face", "action_tags": "arms_out, lifting, nose_rub, smiling", "environment_tags": "indoors, warm_lighting", "scene_description": "Her golden eyes soften with warmth toward the camera.", }) hybrid = sp.build_positive_prompt_hybrid(scene, {**PERSONA_WOLF, "appearance_tags": "fox_girl, golden_eyes"}, "pink_sweater") assert "lifting" not in hybrid assert "nose_rub" not in hybrid assert "golden" in hybrid.lower() def test_bundle_tags_only_alt_when_dual_compare(anima): scene = { "shot_type": "first_person_pov", "pov_cue": "dialogue_close", "action_tags": "smiling", "environment_tags": "indoors", "scene_description": "Soft light on her face.", } with patch.object(sp, "anima_dual_enabled", return_value=True): bundle = sp._bundle_from_scene(scene, PERSONA_WOLF, "") assert bundle.desc_full is not None assert bundle.desc_full != bundle.tag_full assert "Soft light" in bundle.tag_full assert "Soft light" not in bundle.desc_full.split(sp.NEGATIVE_PROMPT_SEPARATOR)[0]