diff --git a/caption_metadata_routes.py b/caption_metadata_routes.py index b0dc4fb..1c0be0f 100644 --- a/caption_metadata_routes.py +++ b/caption_metadata_routes.py @@ -288,7 +288,7 @@ def configured_cast_from_row_result( if expression: expression, labeled_expression = expression_detail(expression, deps.clean_text) if labeled_expression: - scene_bits.append(f"with expression details showing {expression}") + scene_bits.append(f"showing {expression}") else: scene_bits.append(f"with {expression}") if composition: diff --git a/caption_policy.py b/caption_policy.py index 5ce845e..4a70bbe 100644 --- a/caption_policy.py +++ b/caption_policy.py @@ -145,7 +145,10 @@ def metadata_action_label(row: dict[str, Any], default: str = "sexual pose") -> def normalize_composition(text: str) -> str: - return re.sub(r"^vertical\s+", "", input_policy.clean_text(text), flags=re.IGNORECASE) + text = re.sub(r"^vertical\s+", "", input_policy.clean_text(text), flags=re.IGNORECASE) + text = re.sub(r"\s+composition$", "", text, flags=re.IGNORECASE) + text = re.sub(r"\bcomposition\b", "frame", text, flags=re.IGNORECASE) + return text.strip(" ,") def clean_clothing(text: str) -> str: diff --git a/krea_formatter.py b/krea_formatter.py index 7a645e2..6d8b97c 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -230,6 +230,10 @@ def _composition_phrase( detail_density: str = "balanced", ) -> str: composition = _clean(composition) + if not composition: + return "" + composition = re.sub(r"\s+composition$", "", composition, flags=re.IGNORECASE) + composition = re.sub(r"\bcomposition\b", "frame", composition, flags=re.IGNORECASE).strip(" ,") if not composition: return "" action_lower = _clean(action).lower() diff --git a/krea_normal_formatter.py b/krea_normal_formatter.py index c8abfc3..feee2f6 100644 --- a/krea_normal_formatter.py +++ b/krea_normal_formatter.py @@ -67,6 +67,19 @@ def _couple_subject_phrase(subject: str, ages: str) -> str: return subject +def _framed_composition_phrase(composition: str, prefix: str = "framed as") -> str: + composition = re.sub(r"\s+composition$", "", str(composition or "").strip(), flags=re.IGNORECASE) + composition = re.sub( + r"\bcomposition\b", + "frame", + composition, + flags=re.IGNORECASE, + ).strip(" ,") + if not composition: + return "" + return f"{prefix} {composition}" + + def format_normal_row_result( request: KreaNormalRowRequest, deps: KreaNormalRowDependencies, @@ -95,7 +108,7 @@ def format_normal_row_result( f"with {expression}" if expression else "", f"in {scene}" if scene else "", camera_scene, - f"framed as {composition}" if composition else "", + _framed_composition_phrase(composition), camera, style if detail_level != "concise" else "", ] @@ -118,7 +131,7 @@ def format_normal_row_result( f"The setting is {scene}" if scene else "", camera_scene, f"Facial expressions are {expression}" if expression else "", - f"The image is framed as {composition}" if composition else "", + _framed_composition_phrase(composition, "The image is framed as"), camera, style if detail_level != "concise" else "", ] @@ -131,7 +144,7 @@ def format_normal_row_result( f"in {scene}" if scene else "", camera_scene, f"with {expression}" if expression else "", - f"framed as {composition}" if composition else "", + _framed_composition_phrase(composition), camera, style if detail_level != "concise" else "", ] diff --git a/krea_pair_formatter.py b/krea_pair_formatter.py index a156922..e4b40e4 100644 --- a/krea_pair_formatter.py +++ b/krea_pair_formatter.py @@ -191,7 +191,7 @@ def format_insta_pair_result(request: KreaPairFormatRequest, deps: KreaPairForma deps.expression_phrase(soft_expression), f"in {soft.get('scene_text')}" if soft.get("scene_text") else "", soft_camera_scene, - f"framed as {soft_output_composition}" if soft_output_composition else "", + deps.composition_phrase(soft_output_composition), soft_camera, soft_style if detail_level != "concise" else "", ] diff --git a/tools/prompt_route_simulation.py b/tools/prompt_route_simulation.py index db721a3..2ae1cbf 100644 --- a/tools/prompt_route_simulation.py +++ b/tools/prompt_route_simulation.py @@ -533,6 +533,8 @@ def _caption_expression_grammar_issues(name: str, caption_text: str) -> list[str flags=re.IGNORECASE, ): return [f"{name}.caption: character_expression_has_grammar"] + if re.search(r"\bwith expression details showing\b", caption_text, flags=re.IGNORECASE): + return [f"{name}.caption: expression_detail_connector"] return [] @@ -546,6 +548,18 @@ def _krea_grammar_issues(name: str, krea_prompt: str) -> list[str]: return [] +def _composition_label_issues(name: str, prompts: dict[str, str]) -> list[str]: + issues: list[str] = [] + pattern = re.compile( + r"\b(?:framed as|image is framed as|composition is)\s+[^.;]{0,180}\s+composition\b", + flags=re.IGNORECASE, + ) + for formatter_name in ("krea", "caption"): + if pattern.search(prompts.get(formatter_name, "")): + issues.append(f"{name}.{formatter_name}: framed_composition_label") + return issues + + def _trace_dict(formatter_name: str, payload: dict[str, Any]) -> tuple[dict[str, Any], str]: trace_text = str(payload.get("route_trace_json") or "") if not trace_text: @@ -681,6 +695,7 @@ def _formatter_issues( issues.extend(_caption_cast_descriptor_issues(name, row, caption_text)) issues.extend(_caption_expression_grammar_issues(name, caption_text)) issues.extend(_krea_grammar_issues(name, krea_prompt)) + issues.extend(_composition_label_issues(name, prompts)) for label, value in ( (f"{name}.krea_negative", krea.get("negative_prompt")), diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index c8dcaba..cc6f317 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -4029,6 +4029,10 @@ def smoke_caption_policy() -> None: caption_policy.normalize_composition("vertical centered body frame") == "centered body frame", "Caption composition normalization changed", ) + _expect( + caption_policy.normalize_composition("vertical centered body frame composition") == "centered body frame", + "Caption composition should drop trailing composition label", + ) _expect( caption_policy.clean_clothing("silk dress, fashion editorial styling") == "silk dress", "Caption clothing cleanup changed", @@ -6505,7 +6509,11 @@ def smoke_krea_pov_penetration_route() -> None: _expect("woman" in lower and "thigh" in lower, "POV penetration lost body-position anchors") _expect("camera:" not in prompt, "POV penetration emitted normal third-person camera directive") _expect("role graph:" not in lower and "sexual scene:" not in lower, "POV penetration leaked raw prompt labels") - _expect("composition. explicit" in lower, "POV penetration composition sentence should keep punctuation before style suffix") + style_index = lower.find("explicit consensual") + _expect( + style_index > 0 and lower[:style_index].rstrip().endswith("."), + "POV penetration composition sentence should keep punctuation before style suffix", + ) def smoke_pov_outercourse_position_routes() -> None: