diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 3766fb6..baf13aa 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -273,7 +273,7 @@ Already isolated: caption-part joining, embedded soft/hard row output synchronization, and row sanitation before metadata leaves generation. It also copies side-specific pair metadata, such as soft partner styling and hardcore clothing/detail - state, onto the embedded soft/hard rows. + state, plus shared cast descriptors, onto the embedded soft/hard rows. - final custom-row assembly now lives in `row_assembly.py` behind `CustomRowAssemblyRequest`, covering render context population, prompt/caption rendering delegation, row-base indexing, row metadata copying, @@ -326,8 +326,9 @@ Already isolated: Embedded soft/hard rows are synchronized to the final pair prompt, caption, and negative outputs during normalization so serialized pair metadata does not carry stale standalone row text. Side-specific structured fields are - synchronized there too, including soft partner styling and hardcore clothing - continuity metadata. + synchronized there too, including soft partner styling, hardcore clothing + continuity metadata, and shared cast descriptors for same-cast caption and + formatter routes. ### Krea2 Formatter Path diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 2a6790c..28dde4a 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -536,7 +536,7 @@ plain prompt text. When debugging, inspect these fields before editing pools. | `options` | `SxCP Insta/OF Options` | Formatters/debug | Soft/hard level, cast mode, continuity, camera modes, expression settings. | | `shared_descriptor` | `pair_cast.py` | Pair formatters | Primary creator descriptor. | | `shared_cast_descriptors` | `pair_cast.py` | Pair formatters | Full cast descriptor list. | -| `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side; their prompt, caption, negative, and side-specific metadata fields are synchronized to the final pair outputs/root fields during pair normalization. | +| `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side; their prompt, caption, negative, shared cast descriptors, and side-specific metadata fields are synchronized to the final pair outputs/root fields during pair normalization. | | `softcore_prompt`, `hardcore_prompt` | `pair_output.py` | Direct output/fallback | Raw pair prompts before formatter rewrite. | | `softcore_negative_prompt`, `hardcore_negative_prompt` | `pair_output.py` | Formatter negatives | Separate negatives for each side. | | `softcore_partner_styling` | `pair_cast.py` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. | diff --git a/row_normalization.py b/row_normalization.py index 18bd6b3..48caba4 100644 --- a/row_normalization.py +++ b/row_normalization.py @@ -142,11 +142,36 @@ def synchronize_pair_side_metadata(pair: dict[str, Any]) -> dict[str, Any]: return pair +def synchronize_pair_cast_metadata(pair: dict[str, Any]) -> dict[str, Any]: + descriptors = pair.get("shared_cast_descriptors") + if isinstance(descriptors, list): + descriptor_list = [str(item).strip() for item in descriptors if str(item or "").strip()] + descriptor_text = "; ".join(descriptor_list) + else: + descriptor_text = str(descriptors or "").strip() + descriptor_list = [descriptor_text] if descriptor_text else [] + if not descriptor_text: + return pair + + options = pair.get("options") if isinstance(pair.get("options"), dict) else {} + row_keys = ["hardcore_row"] + if options.get("softcore_cast") == "same_as_hardcore": + row_keys.append("softcore_row") + for row_key in row_keys: + row = pair.get(row_key) + if not isinstance(row, dict): + continue + row["cast_descriptor_text"] = descriptor_text + row["cast_descriptors"] = list(descriptor_list) + return pair + + def normalize_pair_metadata(pair: dict[str, Any], *, active_trigger: str = "") -> dict[str, Any]: trigger = str(active_trigger or "").strip() triggers = _trigger_tuple(trigger) synchronize_pair_row_outputs(pair) synchronize_pair_side_metadata(pair) + synchronize_pair_cast_metadata(pair) for key in ("softcore_prompt", "hardcore_prompt"): if key in pair: pair[key] = sanitize_prompt_text(pair.get(key, ""), triggers=triggers) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index b3c38ec..638b509 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -3976,6 +3976,16 @@ def _expect_pair(pair: dict[str, Any], name: str) -> None: _expect_custom_row(pair.get("hardcore_row") or {}, f"{name}.hardcore_row") _expect_text(f"{name}.shared_descriptor", pair.get("shared_descriptor"), 12) _expect(pair.get("shared_cast_descriptors"), f"{name}.shared_cast_descriptors should not be empty") + shared_cast_text = "; ".join(str(item).strip() for item in pair.get("shared_cast_descriptors") or [] if str(item).strip()) + _expect( + pair["hardcore_row"].get("cast_descriptor_text") == shared_cast_text, + f"{name}.hardcore_row cast descriptors drifted from pair root", + ) + if pair.get("options", {}).get("softcore_cast") == "same_as_hardcore": + _expect( + pair["softcore_row"].get("cast_descriptor_text") == shared_cast_text, + f"{name}.softcore_row cast descriptors drifted from same-cast pair root", + ) _expect_text(f"{name}.softcore_prompt", pair.get("softcore_prompt"), 20) _expect_text(f"{name}.hardcore_prompt", pair.get("hardcore_prompt"), 20) _expect_trigger_once(f"{name}.softcore_prompt", pair.get("softcore_prompt"), Trigger)