diff --git a/scene_camera_adapters.py b/scene_camera_adapters.py index fdbd56e..d10664c 100644 --- a/scene_camera_adapters.py +++ b/scene_camera_adapters.py @@ -1042,10 +1042,11 @@ def scene_camera_directive( geometry = pov_camera_geometry_phrase(parsed, compact_labels) if pov_labels else camera_geometry_phrase(parsed, compact_labels) geometry_clause = f" ({geometry})" if geometry else "" if pov_labels: + subject, _pronoun = scene_subject_terms(subject_kind, pov_labels) return ( - f"{profile['layout_label']} from POV{geometry_clause}: {direction_detail}. " - f"{distance_detail}; {elevation_detail}; lower foreground is reserved for POV body or hand cues; " - f"use the multiangle camera only as first-person spatial geometry." + f"{profile['layout_label']} from POV{geometry_clause}: keep {subject} and the action primary; " + f"{profile['place']} context stays beside or behind the bodies, not in the lower foreground; " + f"lower foreground is reserved for POV body or hand cues; use the multiangle camera only as first-person spatial geometry." ) return ( f"{profile['layout_label']}{geometry_clause}: {direction_detail}; " @@ -1098,6 +1099,35 @@ def composition_subject_text(text: str, subject_kind: str) -> str: return text +ACTION_COMPOSITION_TERMS = ( + "action geography", + "anal", + "ass", + "body contact", + "cowgirl", + "doggy", + "explicit", + "first-person", + "foreground hands", + "genital", + "hips", + "kneeling", + "mouth", + "oral", + "penetration", + "pov", + "rear-entry", + "rear entry", + "rear-view", + "sexual contact", +) + + +def is_action_specific_composition(text: Any) -> bool: + lower = str(text or "").lower() + return any(term in lower for term in ACTION_COMPOSITION_TERMS) + + def contextual_composition_prompt( scene_text: Any, composition: Any, @@ -1110,6 +1140,8 @@ def contextual_composition_prompt( text = str(composition or "").strip() if not text: return text + if is_action_specific_composition(text): + return text text = composition_subject_text(text, subject_kind) profile = scene_camera_profile(scene_text, scene_entry=scene_entry, theme=theme, profile_key=profile_key) if not profile: diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index cd18b0e..9555e82 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -881,6 +881,36 @@ def smoke_row_camera_policy() -> None: "coworking lounge frame with the couple near a desk edge" in updated.get("composition", ""), "row camera policy did not adapt coworking composition for couple rows", ) + pov_action_row = dict(row) + pov_action_row["composition"] = ( + "first-person rear-view frame looking down at the woman's raised ass, " + "with foreground hands and rear-entry contact readable" + ) + pov_action_row["prompt"] = ( + "A generated adult prompt. Framed as first-person rear-view frame looking down at the woman's raised ass, " + "with foreground hands and rear-entry contact readable. Avoid: low quality." + ) + updated_pov_action = row_camera.apply_camera_config( + pov_action_row, + _orbit_camera(horizontal_angle=180, vertical_angle=-30, zoom=7.5), + compact_labels=pb.CAMERA_COMPACT_LABELS, + ) + _expect( + "first-person rear-view frame" in updated_pov_action.get("composition", ""), + "row camera policy replaced explicit POV action composition with generic location composition", + ) + _expect( + "couple's raised ass" not in updated_pov_action.get("composition", ""), + "row camera policy rewrote explicit female action anatomy as couple anatomy", + ) + _expect( + "coworking lounge frame with the couple near a desk edge" not in updated_pov_action.get("composition", ""), + "row camera policy leaked generic coworking composition into explicit POV action frame", + ) + _expect( + str(updated_pov_action.get("camera_scene_directive", "")).count(";") <= 3, + "POV camera scene directive became too noisy", + ) already_matching_row = dict(row) already_matching_row["pov_character_labels"] = [] already_matching_row["composition"] = "coworking lounge frame with the subjects near a desk edge and tall-window depth behind them"