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: Keep here:
- continuity policy; - pair route sequencing;
- softcore cast policy; - top-level continuity option handoff between row, camera, clothing, and output
adapters.
Improve later:
- split remaining pair cast/descriptor policy out of `build_insta_of_pair`.
Already isolated: Already isolated:
- soft/hard row creation lives in `pair_rows.py`, including softcore expression - soft/hard row creation lives in `pair_rows.py`, including softcore expression
override resolution, Woman A slot context application, soft outfit/pose override resolution, Woman A slot context application, soft outfit/pose
overrides, POV row fields, and hardcore row creation. 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 - pair-level camera routing lives in `pair_camera.py`, including soft/hard
camera config selection, same-as-softcore mode, camera-detail override, camera config selection, same-as-softcore mode, camera-detail override,
same-room hard scene continuity, camera-aware composition mutation, POV camera 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 L[location_config] --> P
M[composition_config] --> P M[composition_config] --> P
H[hardcore_position_config] --> P H[hardcore_position_config] --> P
P --> A[soft_row via build_prompt] P --> R[pair_rows.py]
P --> B[hard_row via build_prompt] R --> A[soft_row via build_prompt]
A --> X[pair metadata] R --> B[hard_row via build_prompt]
B --> X A --> D[pair_cast.py]
X --> K[Krea2/SDXL/Naturalizer] 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: Softcore row:
@@ -383,8 +388,8 @@ Softcore row:
- Outfit comes from character slot `softcore_outfit` if present, otherwise - Outfit comes from character slot `softcore_outfit` if present, otherwise
`INSTA_OF_SOFTCORE_OUTFITS`. `INSTA_OF_SOFTCORE_OUTFITS`.
- Soft pose comes from `INSTA_OF_SOFTCORE_POSES`. - Soft pose comes from `INSTA_OF_SOFTCORE_POSES`.
- Partner styling comes from `_insta_of_partner_styling` when softcore cast is - Partner styling is resolved through `pair_cast.py` using
`same_as_hardcore`. `_insta_of_partner_styling` when softcore cast is `same_as_hardcore`.
Hardcore row: 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. | | `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. | | `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_descriptor` | `pair_cast.py` | Pair formatters | Primary creator descriptor. |
| `shared_cast_descriptors` | Cast descriptor builder | Pair formatters | Full cast descriptor list. | | `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_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_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_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. | | `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. | | `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. | | `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 generate_prompt_batches as g
from . import pair_clothing from . import pair_clothing
from . import pair_camera from . import pair_camera
from . import pair_cast
from . import pair_output from . import pair_output
from . import pair_rows from . import pair_rows
from . import scene_camera_adapters 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 generate_prompt_batches as g
import pair_clothing import pair_clothing
import pair_camera import pair_camera
import pair_cast
import pair_output import pair_output
import pair_rows import pair_rows
import scene_camera_adapters import scene_camera_adapters
@@ -7037,41 +7039,38 @@ def build_insta_of_pair(
hard_row = row_route["hard_row"] hard_row = row_route["hard_row"]
hard_content_rng = row_route["hard_content_rng"] hard_content_rng = row_route["hard_content_rng"]
descriptor = _insta_of_descriptor(soft_row) cast_context = pair_cast.resolve_insta_pair_cast_context(
cast_descriptors = _insta_of_cast_descriptors( soft_row=soft_row,
descriptor, options=options,
parsed_seed_config, parsed_seed_config=parsed_seed_config,
seed, seed=seed,
row_number, row_number=row_number,
ethnicity, ethnicity=ethnicity,
figure, figure=figure,
no_plus_women, no_plus_women=no_plus_women,
no_black, no_black=no_black,
hard_women_count, hard_women_count=hard_women_count,
hard_men_count, hard_men_count=hard_men_count,
character_slots, 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)) descriptor = cast_context["descriptor"]
soft_cast_descriptor_text = ( cast_descriptors = cast_context["cast_descriptors"]
cast_descriptor_text cast_descriptor_text = cast_context["cast_descriptor_text"]
if options["softcore_cast"] == "same_as_hardcore" soft_partner_styling = cast_context["soft_partner_styling"]
else f"Woman A: {descriptor}" soft_partner_outfit_text = cast_context["soft_partner_outfit_text"]
) platform_style = cast_context["platform_style"]
soft_partner_styling = _insta_of_partner_styling( soft_level = cast_context["soft_level"]
parsed_seed_config, hard_level = cast_context["hard_level"]
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"]]
camera_route = pair_camera.resolve_insta_pair_camera( camera_route = pair_camera.resolve_insta_pair_camera(
soft_row=soft_row, soft_row=soft_row,
hard_row=hard_row, hard_row=hard_row,
@@ -7104,29 +7103,10 @@ def build_insta_of_pair(
hard_camera_scene_sentence = camera_route["hard_camera_scene_sentence"] hard_camera_scene_sentence = camera_route["hard_camera_scene_sentence"]
soft_camera_sentence = camera_route["soft_camera_sentence"] soft_camera_sentence = camera_route["soft_camera_sentence"]
hard_camera_sentence = camera_route["hard_camera_sentence"] hard_camera_sentence = camera_route["hard_camera_sentence"]
soft_cast = ( soft_cast = cast_context["soft_cast"]
"solo creator setup with Woman A alone" soft_cast_presence = cast_context["soft_cast_presence"]
if options["softcore_cast"] == "solo" soft_cast_styling_sentence = cast_context["soft_cast_styling_sentence"]
else f"soft creator-teaser setup with {_insta_of_cast_phrase(hard_women_count, hard_men_count)}" hard_cast = cast_context["hard_cast"]
)
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)
character_hardcore_clothing_entries = _character_hardcore_clothing_entries( character_hardcore_clothing_entries = _character_hardcore_clothing_entries(
character_slot_map, character_slot_map,
hard_women_count, 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. ", "dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ",
}[hard_detail_density] }[hard_detail_density]
pov_directive = _pov_prompt_directive(pov_character_labels) pov_directive = _pov_prompt_directive(pov_character_labels)
soft_descriptor_sentence = ( soft_descriptor_sentence = cast_context["soft_descriptor_sentence"]
f"Cast descriptors: {soft_cast_descriptor_text}. "
if options["softcore_cast"] == "same_as_hardcore"
else f"Woman A: {descriptor}. "
)
return pair_output.assemble_insta_pair_metadata( return pair_output.assemble_insta_pair_metadata(
active_trigger=active_trigger, 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(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_row_base(pair.get("softcore_row") or {}, f"{name}.softcore_row")
_expect_custom_row(pair.get("hardcore_row") or {}, f"{name}.hardcore_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}.softcore_prompt", pair.get("softcore_prompt"), 20)
_expect_text(f"{name}.hardcore_prompt", pair.get("hardcore_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) _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")) 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("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") _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: def smoke_krea_pair_clothing_state() -> None: