Normalize built-in row appearance metadata

This commit is contained in:
2026-06-27 17:08:01 +02:00
parent 4714e23dc8
commit ed67c9ba7b
3 changed files with 109 additions and 1 deletions
+1 -1
View File
@@ -132,7 +132,7 @@ Core helper ownership:
| `krea_row_fields.py` | Shared Krea normal-row field extraction for item, scene, pose, expression, composition/source-composition, camera, and style used by normal and configured-cast routes. | | `krea_row_fields.py` | Shared Krea normal-row field extraction for item, scene, pose, expression, composition/source-composition, camera, and style used by normal and configured-cast routes. |
| `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. | | `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. |
| `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup, including route-agnostic negative-prompt merge/dedupe. | | `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup, including route-agnostic negative-prompt merge/dedupe. |
| `row_normalization.py` | Final prompt-row and pair metadata normalization: legacy built-in subject/count/scene/item/pose/expression metadata enrichment, trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. | | `row_normalization.py` | Final prompt-row and pair metadata normalization: legacy built-in subject/count/scene/appearance/item/pose/expression metadata enrichment, trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. |
| `formatter_detail.py` | Shared formatter detail-level choices, normalization, and concise/balanced/dense gates used by Krea2 and caption routes. | | `formatter_detail.py` | Shared formatter detail-level choices, normalization, and concise/balanced/dense gates used by Krea2 and caption routes. |
| `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. | | `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. |
| `formatter_target.py` | Shared formatter target choices and normalization for `auto`, `single`, `softcore`, and `hardcore`, including pair-side selection and combined-caption inclusion policy. | | `formatter_target.py` | Shared formatter target choices and normalization for `auto`, `single`, `softcore`, and `hardcore`, including pair-side selection and combined-caption inclusion policy. |
+66
View File
@@ -4,9 +4,11 @@ import re
from typing import Any from typing import Any
try: try:
from . import generate_prompt_batches as prompt_batches
from . import row_location as row_location_policy from . import row_location as row_location_policy
from .prompt_hygiene import combine_negative_text, sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text from .prompt_hygiene import combine_negative_text, sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text
except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`. except ImportError: # Allows local smoke tests with `python tools/prompt_smoke.py`.
import generate_prompt_batches as prompt_batches
import row_location as row_location_policy import row_location as row_location_policy
from prompt_hygiene import combine_negative_text, sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text from prompt_hygiene import combine_negative_text, sanitize_caption_text, sanitize_negative_text, sanitize_prompt_text
@@ -113,6 +115,64 @@ def _clean_legacy_clothing(value: Any) -> str:
return text.strip(" ,") return text.strip(" ,")
def _legacy_body_phrase(row: dict[str, Any]) -> str:
body_phrase = _clean_text(row.get("body_phrase"))
if body_phrase:
return body_phrase
body = _clean_text(row.get("body_type") or row.get("body"))
if not body:
return ""
figure_note = _clean_text(row.get("figure") or row.get("figure_note"))
return _clean_text(prompt_batches.make_body_phrase(body, figure_note))
def _strip_legacy_caption_lead(caption: str) -> str:
pieces = caption.split(", ", 1)
if len(pieces) == 2 and pieces[0].strip().lower() not in ("woman", "man"):
return pieces[1].strip()
return caption
def _legacy_single_caption_front(row: dict[str, Any]) -> dict[str, str]:
caption = _strip_legacy_caption_lead(_clean_text(row.get("caption")))
if not caption:
return {}
subject = _clean_text(row.get("primary_subject") or row.get("subject"))
age = _clean_text(row.get("age_band") or row.get("age"))
body_phrase = _legacy_body_phrase(row)
if subject.lower() in ("woman", "man") and age and body_phrase:
prefix = f"{subject}, {age}, {body_phrase}, "
if caption.lower().startswith(prefix.lower()):
try:
skin, hair, eyes, _rest = caption[len(prefix) :].split(", ", 3)
except ValueError:
return {}
return {
"caption_subject": subject,
"caption_age": age,
"caption_body_phrase": body_phrase,
"caption_skin": skin,
"caption_hair": hair,
"caption_eyes": eyes,
}
pieces = [piece.strip() for piece in caption.split(", ", 6)]
if len(pieces) < 7:
return {}
subject, age, body_phrase, skin, hair, eyes, _rest = pieces
if subject.lower() not in ("woman", "man"):
return {}
return {
"caption_subject": subject,
"caption_age": age,
"caption_body_phrase": body_phrase,
"caption_skin": skin,
"caption_hair": hair,
"caption_eyes": eyes,
}
def enrich_legacy_row_metadata(row: dict[str, Any]) -> dict[str, Any]: def enrich_legacy_row_metadata(row: dict[str, Any]) -> dict[str, Any]:
if row.get("source") != "built_in_generator": if row.get("source") != "built_in_generator":
return row return row
@@ -133,6 +193,12 @@ def enrich_legacy_row_metadata(row: dict[str, Any]) -> dict[str, Any]:
if scene_text: if scene_text:
row["scene_text"] = scene_text row["scene_text"] = scene_text
row.setdefault("scene_entry", {"slug": scene_slug, "prompt": scene_text}) row.setdefault("scene_entry", {"slug": scene_slug, "prompt": scene_text})
if subject_type in ("woman", "man"):
front = _legacy_single_caption_front(row)
_setdefault_nonempty(row, "body_phrase", front.get("caption_body_phrase", ""))
_setdefault_nonempty(row, "skin", front.get("caption_skin", ""))
_setdefault_nonempty(row, "hair", front.get("caption_hair", ""))
_setdefault_nonempty(row, "eyes", front.get("caption_eyes", ""))
pose = _clean_legacy_pose(_legacy_prompt_field(row, "Pose")) pose = _clean_legacy_pose(_legacy_prompt_field(row, "Pose"))
_setdefault_nonempty(row, "pose", pose) _setdefault_nonempty(row, "pose", pose)
expression = _legacy_prompt_field(row, "Facial expression") or _legacy_prompt_field(row, "Facial expressions") expression = _legacy_prompt_field(row, "Facial expression") or _legacy_prompt_field(row, "Facial expressions")
+42
View File
@@ -580,6 +580,10 @@ def smoke_builtin_single() -> None:
_expect(row.get("scene_entry", {}).get("slug") == row.get("scene"), "builtin single row lost scene_entry slug") _expect(row.get("scene_entry", {}).get("slug") == row.get("scene"), "builtin single row lost scene_entry slug")
item = _expect_text("builtin_single_woman.item", row.get("item"), 8) item = _expect_text("builtin_single_woman.item", row.get("item"), 8)
pose = _expect_text("builtin_single_woman.pose", row.get("pose"), 8) pose = _expect_text("builtin_single_woman.pose", row.get("pose"), 8)
body_phrase = _expect_text("builtin_single_woman.body_phrase", row.get("body_phrase"), 8)
skin = _expect_text("builtin_single_woman.skin", row.get("skin"), 8)
hair = _expect_text("builtin_single_woman.hair", row.get("hair"), 5)
eyes = _expect_text("builtin_single_woman.eyes", row.get("eyes"), 4)
_expect(row.get("item_label") == "Clothing", "builtin single row lost item label") _expect(row.get("item_label") == "Clothing", "builtin single row lost item label")
_expect(row.get("clothing") == item, "builtin single row did not mirror clothing into item metadata") _expect(row.get("clothing") == item, "builtin single row did not mirror clothing into item metadata")
_expect("fashion editorial styling" not in item.lower(), "builtin single item kept generic styling suffix") _expect("fashion editorial styling" not in item.lower(), "builtin single item kept generic styling suffix")
@@ -613,13 +617,24 @@ def smoke_builtin_single() -> None:
) )
_expect(item in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost explicit item") _expect(item in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost explicit item")
_expect(pose in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost explicit pose") _expect(pose in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost explicit pose")
_expect(body_phrase in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost body phrase")
_expect(skin in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost skin")
_expect(hair in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost hair")
_expect(eyes in str(krea_metadata.get("krea_prompt", "")), "Krea metadata-only built-in route lost eyes")
item_anchor = " ".join(re.findall(r"[a-z0-9]+", item.lower())[:3]) item_anchor = " ".join(re.findall(r"[a-z0-9]+", item.lower())[:3])
pose_anchor = " ".join(re.findall(r"[a-z0-9]+", pose.lower())[:4]) pose_anchor = " ".join(re.findall(r"[a-z0-9]+", pose.lower())[:4])
sdxl_metadata_prompt = str(sdxl_metadata.get("sdxl_prompt", "")).lower() sdxl_metadata_prompt = str(sdxl_metadata.get("sdxl_prompt", "")).lower()
_expect(item_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit item") _expect(item_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit item")
_expect(pose_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit pose") _expect(pose_anchor in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost explicit pose")
for body_tag in sdxl_tag_policy.split_tag_text(body_phrase):
_expect(body_tag.lower() in sdxl_metadata_prompt, f"SDXL metadata-only built-in route lost body tag: {body_tag}")
_expect(skin.lower() in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost skin")
_expect(hair.lower() in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost hair")
_expect(eyes.lower() in sdxl_metadata_prompt, "SDXL metadata-only built-in route lost eyes")
_expect(caption_metadata_method.endswith("metadata(single)"), "Caption metadata-only built-in route did not use single metadata branch") _expect(caption_metadata_method.endswith("metadata(single)"), "Caption metadata-only built-in route did not use single metadata branch")
_expect(item in caption_metadata and pose in caption_metadata, "Caption metadata-only built-in route lost explicit item or pose") _expect(item in caption_metadata and pose in caption_metadata, "Caption metadata-only built-in route lost explicit item or pose")
_expect(body_phrase in caption_metadata and skin in caption_metadata, "Caption metadata-only built-in route lost appearance")
_expect(hair in caption_metadata and eyes in caption_metadata, "Caption metadata-only built-in route lost hair or eyes")
_expect_formatter_outputs(row, "builtin_single_woman", target="single") _expect_formatter_outputs(row, "builtin_single_woman", target="single")
@@ -2877,6 +2892,33 @@ def smoke_row_normalization_policy() -> None:
_expect("calm smile" in str(legacy_couple.get("expression", "")), "Legacy couple row lost expression metadata") _expect("calm smile" in str(legacy_couple.get("expression", "")), "Legacy couple row lost expression metadata")
_expect("cast_summary" not in legacy_couple, "Legacy couple row should not gain configured-cast summary") _expect("cast_summary" not in legacy_couple, "Legacy couple row should not gain configured-cast summary")
legacy_single = row_normalization.normalize_prompt_row(
{
"source": "built_in_generator",
"primary_subject": "woman",
"scene": "studio",
"age_band": "25-year-old adult",
"body_type": "curvy",
"figure": "soft curves, defined waist",
"caption": (
f"{Trigger}, woman, 25-year-old adult, curvy figure with soft curves, defined waist, "
"warm skin, short blonde hair, blue eyes, pose, expression, clothing, scene, composition"
),
"prompt": (
"A woman. Scene: old studio. Pose: standing calmly. "
"Facial expression: direct look. Clothing: fitted dress, fashion editorial styling. "
"Composition: vertical portrait."
),
"negative_prompt": "bad anatomy",
},
active_trigger=Trigger,
prepend_trigger_to_prompt=False,
)
_expect(legacy_single.get("body_phrase") == "curvy figure with soft curves, defined waist", "Legacy single row lost body phrase")
_expect(legacy_single.get("skin") == "warm skin", "Legacy single row lost skin metadata")
_expect(legacy_single.get("hair") == "short blonde hair", "Legacy single row lost hair metadata")
_expect(legacy_single.get("eyes") == "blue eyes", "Legacy single row lost eye metadata")
legacy_group = row_normalization.normalize_prompt_row( legacy_group = row_normalization.normalize_prompt_row(
{ {
"source": "built_in_generator", "source": "built_in_generator",