diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 9b8a5f5..d6d153d 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -257,8 +257,8 @@ Improve later: Near-term: - Add final row hygiene already done through `prompt_hygiene.py`. -- Add a metadata smoke checker for representative rows through - `tools/prompt_smoke.py`. +- Add a metadata smoke checker for representative generated rows and static + formatter fixtures through `tools/prompt_smoke.py`. - Normalize every row with one function before JSON serialization. Medium-term: @@ -359,5 +359,5 @@ Medium-term: checks. 2. Extract hardcore role graph generation from `prompt_builder.py` into a dedicated `hardcore_role_graphs.py` module. -3. Add route-level smoke fixtures for more representative Krea/SDXL/caption metadata - rows. +3. Add more route-level smoke fixtures for generated edge cases that are not + covered by the current static Krea/SDXL/caption metadata fixtures. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index a13183a..6db8963 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -722,6 +722,9 @@ pair metadata through the core Python APIs, then verifies: normal camera text, and preserve composition punctuation before the style suffix; - expression-disabled rows do not fall back to generated expression text. +- static formatter metadata fixtures keep source-provided action families + stable across Krea2 prose, SDXL tags, and natural captions even when raw item + text contains distracting wording. ## Editing Cheatsheet diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index dd781e1..515044c 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -273,6 +273,64 @@ def _prompt_row( return row +def _fixture_hardcore_row(**overrides: Any) -> dict[str, Any]: + row: dict[str, Any] = { + "source": "json_category", + "prompt": "Fixture explicit adult prompt for metadata route.", + "caption": "fixture caption", + "negative_prompt": "low quality, bad anatomy", + "main_category": "Hardcore sexual poses", + "subcategory": "Penetrative sex", + "category_slug": "hardcore_sexual_poses", + "subcategory_slug": "penetrative_sex", + "subject_type": "configured_cast", + "subject_phrase": "1 adult woman and 1 adult man", + "cast_summary": "1 woman, 1 man", + "cast_descriptor_text": ( + "Woman A: 25-year-old adult woman, slim figure, fair skin, blonde hair, blue eyes; " + "Man A: 40-year-old adult man, average figure, tan skin, dark hair" + ), + "cast_descriptors": [ + "Woman A: 25-year-old adult woman, slim figure, fair skin, blonde hair, blue eyes", + "Man A: 40-year-old adult man, average figure, tan skin, dark hair", + ], + "women_count": 1, + "men_count": 1, + "person_count": 2, + "item": ( + "missionary position while full-body penetrative sex, hands gripping the ass, " + "mouth close to the ear, and explicit genital contact visible" + ), + "custom_item": "Penetrative sex", + "item_label": "Sexual pose", + "item_axis_values": { + "position": "missionary position", + "penetration_act": "full-body penetrative sex", + "mouth_detail": "mouth close to the ear", + }, + "scene_text": "private studio room with warm light", + "scene_kind": "explicit adult sex scene", + "pose": "configured explicit pose", + "composition": "front-facing full-body frame", + "source_composition": "front-facing full-body frame", + "role_graph": ( + "Woman A lies on her back with legs open around Man A's hips while Man A is above her between her thighs; " + "Man A's hips press close and Man A's penis thrusts into her pussy." + ), + "source_role_graph": ( + "Woman A lies on her back with legs open around Man A's hips while Man A is above her between her thighs; " + "Man A's hips press close and Man A's penis thrusts into her pussy." + ), + "expression": "focused adult expression", + "action_family": "penetration", + "position_family": "penetrative", + "position_key": "missionary", + "position_keys": ["missionary"], + } + row.update(overrides) + return row + + def smoke_builtin_single() -> None: row = _prompt_row(name="builtin_single_woman", category="woman", subcategory="random", seed=1001, men_count=0) _expect(row.get("source") == "built_in_generator", "builtin row should come from built-in generator") @@ -681,6 +739,99 @@ def smoke_no_expression_fallback() -> None: _expect_formatter_outputs(row, "hardcore_expression_disabled", target="single") +def smoke_formatter_metadata_fixtures() -> None: + cases = [ + { + "name": "fixture_penetration_text_noise", + "row": _fixture_hardcore_row(), + "krea_terms": ("penis thrusts",), + "sdxl_terms": ("penetrative sex", "missionary"), + "caption_terms": ("penetrative action",), + }, + { + "name": "fixture_manual_source_family", + "row": _fixture_hardcore_row( + subcategory="Manual stimulation", + subcategory_slug="manual_stimulation", + item="wet fingers moving between the thighs, one hand braced on the hip, wet shine on fingers and inner thighs", + custom_item="Manual stimulation", + item_axis_values={ + "position": "kneeling hand-between-thighs position", + "manual_act": "wet fingers moving between the thighs", + }, + composition="close crop on hands and face", + source_composition="close crop on hands and face", + role_graph=( + "Woman A reclines with thighs open while Man A's hand is between her legs, " + "fingers visibly stimulating her pussy." + ), + source_role_graph=( + "Woman A reclines with thighs open while Man A's hand is between her legs, " + "fingers visibly stimulating her pussy." + ), + action_family="foreplay", + position_family="manual", + position_key="fingering", + position_keys=["fingering", "open_thighs"], + ), + "krea_terms": ("fingers visibly stimulating",), + "sdxl_terms": ("manual stimulation", "fingering"), + "caption_terms": ("manual action",), + }, + { + "name": "fixture_climax_family", + "row": _fixture_hardcore_row( + subcategory="Cumshot and climax", + subcategory_slug="cumshot_climax", + item="external cumshot with cum on lower back and ass, explicit semen aftermath visible", + custom_item="Cumshot and climax", + item_axis_values={"position": "on all fours with hips raised"}, + composition="low-angle post-orgasm frame", + source_composition="low-angle post-orgasm frame", + role_graph=( + "Woman A is on all fours with hips raised while Man A is positioned behind her " + "and ejaculates semen across her ass, thighs, and lower back." + ), + source_role_graph=( + "Woman A is on all fours with hips raised while Man A is positioned behind her " + "and ejaculates semen across her ass, thighs, and lower back." + ), + action_family="climax", + position_family="climax", + position_key="doggy", + position_keys=["doggy"], + ), + "krea_terms": ("ejaculates semen",), + "sdxl_terms": ("climax", "semen"), + "caption_terms": ("climax action",), + }, + ] + for case in cases: + name = case["name"] + row = case["row"] + _expect_custom_row(row, name) + metadata = _json(row) + + krea = krea_formatter.format_krea2_prompt("", metadata_json=metadata, target="single") + krea_prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 40).lower() + _expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata") + _expect("role graph:" not in krea_prompt and "sexual pose:" not in krea_prompt, f"{name}.krea leaked raw labels") + for term in case["krea_terms"]: + _expect(term in krea_prompt, f"{name}.krea missing {term!r}") + + sdxl = sdxl_formatter.format_sdxl_prompt("", metadata_json=metadata, target="single", trigger=SdxlTrigger, prepend_trigger=True) + sdxl_prompt = _expect_text(f"{name}.sdxl_prompt", sdxl.get("sdxl_prompt"), 40).lower() + _expect("metadata" in sdxl.get("method", ""), f"{name}.sdxl did not use metadata") + for term in case["sdxl_terms"]: + _expect(term in sdxl_prompt, f"{name}.sdxl missing {term!r}") + + caption, method = caption_naturalizer.naturalize_caption("", metadata_json=metadata, trigger=Trigger, include_trigger=True) + caption_text = _expect_text(f"{name}.caption", caption, 40).lower() + _expect("metadata" in method, f"{name}.caption did not use metadata") + for term in case["caption_terms"]: + _expect(term in caption_text, f"{name}.caption missing {term!r}") + + SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("builtin_single_woman", smoke_builtin_single), ("camera_scene_single", smoke_camera_scene_single), @@ -694,6 +845,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("pov_camera_scene", smoke_pov_camera_scene), ("krea_pov_penetration_route", smoke_krea_pov_penetration_route), ("expression_disabled", smoke_no_expression_fallback), + ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ]