Extract Insta pair cast context

This commit is contained in:
2026-06-26 22:18:59 +02:00
parent b7939a4748
commit 9b9b0cbb4c
5 changed files with 175 additions and 78 deletions
+6 -6
View File
@@ -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
+15 -10
View File
@@ -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. |
+111
View File
@@ -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,
}
+38 -62
View File
@@ -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,
+5
View File
@@ -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: