diff --git a/krea_pair_formatter.py b/krea_pair_formatter.py index 11547f9..f68afde 100644 --- a/krea_pair_formatter.py +++ b/krea_pair_formatter.py @@ -1,6 +1,7 @@ from __future__ import annotations from dataclasses import dataclass +import re from typing import Any, Callable try: @@ -129,12 +130,32 @@ def _side_profile_hidden_lower_clothing_clause(clause: str) -> bool: ) and any(term in lower for term in ("pulled aside", "removed", "lowered", "visible")) +def _side_profile_visible_clothing_clause(clause: str) -> str: + text = clause.strip(" .") + if not text: + return "" + lower = text.lower() + if lower.startswith("woman a's "): + text = text[len("Woman A's "):] + elif lower.startswith("the woman's "): + text = text[len("the woman's "):] + elif lower.startswith("her "): + text = text[len("her "):] + text = re.sub(r"\s+(?:remain|remains)\s+visible\s+from\s+the\s+same\s+outfit$", " from the same outfit", text, flags=re.IGNORECASE) + text = re.sub(r"\s+(?:remain|remains)\s+visible$", "", text, flags=re.IGNORECASE) + text = re.sub(r",\s+((?:a|an|the)\s+)", r" and \1", text, count=1, flags=re.IGNORECASE) + if not text.lower().startswith(("a ", "an ", "the ")): + text = f"the {text}" + return f"the woman wears {text}" + + def _krea2_atlas_clothing_text(row: dict[str, Any], text: Any) -> str: clothing = str(text or "").strip() if not clothing or not _has_krea2_atlas_variant(row): return clothing side_profile_oral = _has_krea2_variant(row, SIDE_PROFILE_ORAL_VARIANT) kept: list[str] = [] + side_profile_removed_hidden_lower = False for clause in clothing.split(";"): clause = clause.strip(" .") if not clause: @@ -143,8 +164,13 @@ def _krea2_atlas_clothing_text(row: dict[str, Any], text: Any) -> str: if lower.startswith(("pov foreground clothing cue:", "pov foreground body cue:")): continue if side_profile_oral and _side_profile_hidden_lower_clothing_clause(clause): + side_profile_removed_hidden_lower = True continue + if side_profile_oral: + clause = _side_profile_visible_clothing_clause(clause) kept.append(clause) + if side_profile_oral and side_profile_removed_hidden_lower: + kept.insert(0, "The woman's lower garments are pulled aside out of frame") return "; ".join(kept) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 32833b0..6722c95 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -8889,6 +8889,14 @@ def smoke_pov_oral_position_routes() -> None: "button-down shirt tied at the waist" in side_body_clothing_prompt and "fitted bralette" in side_body_clothing_prompt, f"Side-profile oral clothing restore should preserve visible upper outfit continuity: {side_body_clothing_prompt}", ) + _expect( + "the woman's lower garments are pulled aside out of frame" in side_body_clothing_prompt, + f"Side-profile oral clothing restore should keep partial-removal state without exposing hidden garments: {side_body_clothing_prompt}", + ) + _expect( + "the woman wears the button-down shirt tied at the waist" in side_body_clothing_prompt, + f"Side-profile oral clothing restore should assign visible clothing to the woman: {side_body_clothing_prompt}", + ) sitting_pair = pb.build_insta_of_pair( row_number=1,