From 9b9b0cbb4c51294309933e50bff84a0cce588893 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 22:18:59 +0200 Subject: [PATCH] Extract Insta pair cast context --- docs/prompt-architecture-improvement-plan.md | 12 +- docs/prompt-pool-routing-map.md | 25 +++-- pair_cast.py | 111 +++++++++++++++++++ prompt_builder.py | 100 +++++++---------- tools/prompt_smoke.py | 5 + 5 files changed, 175 insertions(+), 78 deletions(-) create mode 100644 pair_cast.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 529e408..5fe32ef 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -144,18 +144,18 @@ Owner today: `build_insta_of_pair`. Keep here: -- continuity policy; -- softcore cast policy; - -Improve later: - -- split remaining pair cast/descriptor policy out of `build_insta_of_pair`. +- pair route sequencing; +- top-level continuity option handoff between row, camera, clothing, and output + adapters. Already isolated: - soft/hard row creation lives in `pair_rows.py`, including softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, POV row fields, and hardcore row creation. +- pair-level cast/display context lives in `pair_cast.py`, including shared + descriptors, same-cast softcore descriptor text, partner styling, platform and + level labels, softcore cast presence text, and hard cast summary text. - pair-level camera routing lives in `pair_camera.py`, including soft/hard camera config selection, same-as-softcore mode, camera-detail override, same-room hard scene continuity, camera-aware composition mutation, POV camera diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index ed61e36..bb32cfc 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -370,11 +370,16 @@ flowchart TD L[location_config] --> P M[composition_config] --> P H[hardcore_position_config] --> P - P --> A[soft_row via build_prompt] - P --> B[hard_row via build_prompt] - A --> X[pair metadata] - B --> X - X --> K[Krea2/SDXL/Naturalizer] + P --> R[pair_rows.py] + R --> A[soft_row via build_prompt] + R --> B[hard_row via build_prompt] + A --> D[pair_cast.py] + B --> D + D --> X[pair metadata] + B --> Y[pair_camera.py + pair_clothing.py] + Y --> X + X --> Z[pair_output.py] + Z --> K[Krea2/SDXL/Naturalizer] ``` Softcore row: @@ -383,8 +388,8 @@ Softcore row: - Outfit comes from character slot `softcore_outfit` if present, otherwise `INSTA_OF_SOFTCORE_OUTFITS`. - Soft pose comes from `INSTA_OF_SOFTCORE_POSES`. -- Partner styling comes from `_insta_of_partner_styling` when softcore cast is - `same_as_hardcore`. +- Partner styling is resolved through `pair_cast.py` using + `_insta_of_partner_styling` when softcore cast is `same_as_hardcore`. Hardcore row: @@ -455,12 +460,12 @@ plain prompt text. When debugging, inspect these fields before editing pools. | --- | --- | --- | --- | | `mode` | `build_insta_of_pair` | Formatters | `Insta/OF` selects pair formatter branches. | | `options` | `SxCP Insta/OF Options` | Formatters/debug | Soft/hard level, cast mode, continuity, camera modes, expression settings. | -| `shared_descriptor` | Soft row descriptor | Pair formatters | Primary creator descriptor. | -| `shared_cast_descriptors` | Cast descriptor builder | Pair formatters | Full cast descriptor list. | +| `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. | | `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` | `_insta_of_partner_styling` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. | +| `softcore_partner_styling` | `pair_cast.py` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. | | `character_hardcore_clothing` | Character slots | Krea pair branch | Explicit per-character hardcore clothing state. | | `default_man_hardcore_clothing` | Pair fallback | Krea pair branch | Auto clothing for visible men without configured clothing. | | `hardcore_clothing_state` | Pair clothing continuity | Krea/SDXL pair branch | Final hard clothing/body exposure sentence before Krea cleanup. | diff --git a/pair_cast.py b/pair_cast.py new file mode 100644 index 0000000..cb56b10 --- /dev/null +++ b/pair_cast.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +from typing import Any, Callable + + +DescriptorFromRow = Callable[[dict[str, Any]], str] +CastDescriptors = Callable[..., list[str]] +PartnerStyling = Callable[..., dict[str, Any]] +CastPhrase = Callable[[int, int], str] + + +def resolve_insta_pair_cast_context( + *, + soft_row: dict[str, Any], + options: dict[str, Any], + parsed_seed_config: dict[str, int], + seed: int, + row_number: int, + ethnicity: str, + figure: str, + no_plus_women: bool, + no_black: bool, + hard_women_count: int, + hard_men_count: int, + character_slots: list[dict[str, Any]], + character_slot_map: dict[str, dict[str, Any]], + pov_character_labels: list[str], + platform_styles: dict[str, str], + soft_levels: dict[str, str], + hardcore_levels: dict[str, str], + descriptor_from_row: DescriptorFromRow, + build_cast_descriptors: CastDescriptors, + prompt_cast_descriptors: Callable[[str], str], + partner_styling: PartnerStyling, + cast_phrase: CastPhrase, +) -> dict[str, Any]: + descriptor = descriptor_from_row(soft_row) + cast_descriptors = build_cast_descriptors( + descriptor, + parsed_seed_config, + seed, + row_number, + ethnicity, + figure, + no_plus_women, + no_black, + hard_women_count, + hard_men_count, + character_slots, + ) + cast_descriptor_text = prompt_cast_descriptors("; ".join(cast_descriptors)) + same_softcore_cast = options["softcore_cast"] == "same_as_hardcore" + soft_cast_descriptor_text = cast_descriptor_text if same_softcore_cast else f"Woman A: {descriptor}" + soft_partner_styling = partner_styling( + parsed_seed_config, + seed, + row_number, + hard_women_count if same_softcore_cast else 1, + hard_men_count if same_softcore_cast else 0, + pov_character_labels if same_softcore_cast else [], + character_slot_map, + ) + if not same_softcore_cast: + soft_partner_styling = {"outfits": [], "pose": ""} + soft_partner_outfit_text = "; ".join(soft_partner_styling["outfits"]) + + soft_cast = ( + "solo creator setup with Woman A alone" + if options["softcore_cast"] == "solo" + else f"soft creator-teaser setup with {cast_phrase(hard_women_count, hard_men_count)}" + ) + soft_cast_presence = ( + ( + "Frame Woman A from the POV participant's first-person camera in a soft creator-teaser setup; " + "keep the POV participant off-camera as the viewpoint and implied by camera perspective or foreground cues. " + ) + if same_softcore_cast and pov_character_labels + else ( + "Place Woman A and the listed partners together in a soft creator-teaser pose. " + if same_softcore_cast + else "Keep the softcore version focused on Woman A alone. " + ) + ) + soft_cast_styling_sentence = ( + f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. " + if same_softcore_cast and soft_partner_outfit_text + else "" + ) + hard_cast = cast_phrase(hard_women_count, hard_men_count) + soft_descriptor_sentence = ( + f"Cast descriptors: {soft_cast_descriptor_text}. " + if same_softcore_cast + else f"Woman A: {descriptor}. " + ) + + return { + "descriptor": descriptor, + "cast_descriptors": cast_descriptors, + "cast_descriptor_text": cast_descriptor_text, + "soft_cast_descriptor_text": soft_cast_descriptor_text, + "soft_partner_styling": soft_partner_styling, + "soft_partner_outfit_text": soft_partner_outfit_text, + "platform_style": platform_styles[options["platform_style"]], + "soft_level": soft_levels[options["softcore_level"]], + "hard_level": hardcore_levels[options["hardcore_level"]], + "soft_cast": soft_cast, + "soft_cast_presence": soft_cast_presence, + "soft_cast_styling_sentence": soft_cast_styling_sentence, + "hard_cast": hard_cast, + "soft_descriptor_sentence": soft_descriptor_sentence, + } diff --git a/prompt_builder.py b/prompt_builder.py index 7d07f5d..30833e5 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -27,6 +27,7 @@ try: from . import generate_prompt_batches as g from . import pair_clothing from . import pair_camera + from . import pair_cast from . import pair_output from . import pair_rows from . import scene_camera_adapters @@ -60,6 +61,7 @@ except ImportError: # Allows local smoke tests with `python -c`. import generate_prompt_batches as g import pair_clothing import pair_camera + import pair_cast import pair_output import pair_rows import scene_camera_adapters @@ -7037,41 +7039,38 @@ def build_insta_of_pair( hard_row = row_route["hard_row"] hard_content_rng = row_route["hard_content_rng"] - descriptor = _insta_of_descriptor(soft_row) - cast_descriptors = _insta_of_cast_descriptors( - descriptor, - parsed_seed_config, - seed, - row_number, - ethnicity, - figure, - no_plus_women, - no_black, - hard_women_count, - hard_men_count, - character_slots, + cast_context = pair_cast.resolve_insta_pair_cast_context( + soft_row=soft_row, + options=options, + parsed_seed_config=parsed_seed_config, + seed=seed, + row_number=row_number, + ethnicity=ethnicity, + figure=figure, + no_plus_women=no_plus_women, + no_black=no_black, + hard_women_count=hard_women_count, + hard_men_count=hard_men_count, + character_slots=character_slots, + character_slot_map=character_slot_map, + pov_character_labels=pov_character_labels, + platform_styles=INSTA_OF_PLATFORM_STYLES, + soft_levels=INSTA_OF_SOFT_LEVELS, + hardcore_levels=INSTA_OF_HARDCORE_LEVELS, + descriptor_from_row=_insta_of_descriptor, + build_cast_descriptors=_insta_of_cast_descriptors, + prompt_cast_descriptors=_insta_of_prompt_cast_descriptors, + partner_styling=_insta_of_partner_styling, + cast_phrase=_insta_of_cast_phrase, ) - cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors)) - soft_cast_descriptor_text = ( - cast_descriptor_text - if options["softcore_cast"] == "same_as_hardcore" - else f"Woman A: {descriptor}" - ) - soft_partner_styling = _insta_of_partner_styling( - parsed_seed_config, - seed, - row_number, - hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1, - hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0, - pov_character_labels if options["softcore_cast"] == "same_as_hardcore" else [], - character_slot_map, - ) - if options["softcore_cast"] != "same_as_hardcore": - soft_partner_styling = {"outfits": [], "pose": ""} - soft_partner_outfit_text = "; ".join(soft_partner_styling["outfits"]) - platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]] - soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]] - hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]] + descriptor = cast_context["descriptor"] + cast_descriptors = cast_context["cast_descriptors"] + cast_descriptor_text = cast_context["cast_descriptor_text"] + soft_partner_styling = cast_context["soft_partner_styling"] + soft_partner_outfit_text = cast_context["soft_partner_outfit_text"] + platform_style = cast_context["platform_style"] + soft_level = cast_context["soft_level"] + hard_level = cast_context["hard_level"] camera_route = pair_camera.resolve_insta_pair_camera( soft_row=soft_row, hard_row=hard_row, @@ -7104,29 +7103,10 @@ def build_insta_of_pair( hard_camera_scene_sentence = camera_route["hard_camera_scene_sentence"] soft_camera_sentence = camera_route["soft_camera_sentence"] hard_camera_sentence = camera_route["hard_camera_sentence"] - soft_cast = ( - "solo creator setup with Woman A alone" - if options["softcore_cast"] == "solo" - else f"soft creator-teaser setup with {_insta_of_cast_phrase(hard_women_count, hard_men_count)}" - ) - soft_cast_presence = ( - ( - "Frame Woman A from the POV participant's first-person camera in a soft creator-teaser setup; " - "keep the POV participant off-camera as the viewpoint and implied by camera perspective or foreground cues. " - ) - if options["softcore_cast"] == "same_as_hardcore" and pov_character_labels - else ( - "Place Woman A and the listed partners together in a soft creator-teaser pose. " - if options["softcore_cast"] == "same_as_hardcore" - else "Keep the softcore version focused on Woman A alone. " - ) - ) - soft_cast_styling_sentence = ( - f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. " - if options["softcore_cast"] == "same_as_hardcore" and soft_partner_outfit_text - else "" - ) - hard_cast = _insta_of_cast_phrase(hard_women_count, hard_men_count) + soft_cast = cast_context["soft_cast"] + soft_cast_presence = cast_context["soft_cast_presence"] + soft_cast_styling_sentence = cast_context["soft_cast_styling_sentence"] + hard_cast = cast_context["hard_cast"] character_hardcore_clothing_entries = _character_hardcore_clothing_entries( character_slot_map, hard_women_count, @@ -7160,11 +7140,7 @@ def build_insta_of_pair( "dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ", }[hard_detail_density] pov_directive = _pov_prompt_directive(pov_character_labels) - soft_descriptor_sentence = ( - f"Cast descriptors: {soft_cast_descriptor_text}. " - if options["softcore_cast"] == "same_as_hardcore" - else f"Woman A: {descriptor}. " - ) + soft_descriptor_sentence = cast_context["soft_descriptor_sentence"] return pair_output.assemble_insta_pair_metadata( active_trigger=active_trigger, diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 2642351..c2e7162 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -630,6 +630,8 @@ def _expect_pair(pair: dict[str, Any], name: str) -> None: _expect(pair.get("mode") == "Insta/OF", f"{name}.mode should be Insta/OF") _expect_row_base(pair.get("softcore_row") or {}, f"{name}.softcore_row") _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") _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) @@ -662,6 +664,9 @@ def smoke_insta_pair() -> None: clothing_state = _clean_key(pair.get("hardcore_clothing_state")) _expect("body is fully exposed" in clothing_state, "explicit nude pair should keep body exposure state") _expect("teaser outfit detail" not in clothing_state, "explicit nude pair should not repeat softcore outfit detail") + partner_styling = pair.get("softcore_partner_styling") or {} + _expect(partner_styling.get("outfits"), "same-cast pair should keep partner softcore outfit styling") + _expect_text("insta_pair_same_cast.partner_pose", partner_styling.get("pose"), 12) def smoke_krea_pair_clothing_state() -> None: