From 972c8f14b61b26843db093210e13ad01cae332e4 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 03:37:33 +0200 Subject: [PATCH] Move pair cast styling policy --- docs/prompt-pool-routing-map.md | 9 ++-- pair_cast.py | 96 ++++++++++++++++++++++++++++----- prompt_builder.py | 48 ++--------------- tools/prompt_smoke.py | 22 ++++++++ 4 files changed, 112 insertions(+), 63 deletions(-) diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 835d1c8..325ba58 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -81,6 +81,7 @@ Core helper ownership: | `hardcore_position_config.py` | Hardcore position/action-filter choices, selected-position normalization, config JSON builders/parsers, focus-policy toggles, subcategory allow-list policy, position-key detection, and category/template/axis filtering. | | `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, hardcore cast count policy, and hardcore detail-density directives. | | `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. | +| `pair_cast.py` | Insta/OF shared descriptors, same-cast softcore descriptor text, partner styling selection, cast-summary wording, platform/level labels, softcore cast presence text, and hard cast summary text. | | `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. | | `pair_clothing.py` | Insta/OF clothing sentence formatting, body-exposure scene cleanup, hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, configured/default visible-person clothing, and final root clothing-state assembly. | | `pair_output.py` | Insta/OF final pair prompts, trigger preservation, negative prompts, captions, and root pair metadata assembly. | @@ -132,7 +133,7 @@ These recipes identify the intended road before editing prompt text. | Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `krea_pov_actions.py`, `krea_pov.py`, `krea_cast.cast_prose` omit-label handling | | Generate porn-scene interaction beats | `Hardcore Position Pool` -> `Hardcore Action Filter` -> pair/builder | Use `focus=interaction_only` for kissing/body worship/transitions/guidance/camera/watching/aftercare, or `focus=manual_only` for fingering/clit/manual stimulation; constrain keys such as `camera_showing`, `wrist_pinning`, `fingering`, `aftercare` | `sexual_poses.json` interaction/manual subcategories, `_role_graph`, `krea_action_context.is_foreplay_text` / `krea_actions.hardcore_action_sentence` | | Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields | -| Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `_insta_of_partner_styling`, character slot clothing, pair Krea branch | +| Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `pair_cast.softcore_partner_styling`, character slot clothing, pair Krea branch | | Change only outfit/clothing | Character clothing or category content route | Keep `person_seed`, `scene_seed`, `pose_seed`; change `content_seed`; slot `softcore_outfit` overrides Insta/OF outfit | `SxCP Character Clothing`, `pair_options.py`, category item templates | | Force a custom location | `SxCP Location Pool` or `SxCP Location Theme` -> builder/pair | `combine_mode=replace` to force; `add` to mix with category scenes | `_scene_pool`, `row_location.apply_location_config_to_legacy_row`, camera scene adapter | | Force a custom frame/composition | `SxCP Composition Pool` or `SxCP Location Theme` -> builder/pair | `combine_mode=replace` to force; `add` to mix | `_composition_pool`, `row_location.apply_composition_config_to_legacy_row`, Krea composition phrase | @@ -423,8 +424,8 @@ Softcore row: - Outfit comes from character slot `softcore_outfit` if present, otherwise `pair_options.INSTA_OF_SOFTCORE_OUTFITS`. - Soft pose comes from `pair_options.INSTA_OF_SOFTCORE_POSES`. -- Partner styling is resolved through `pair_cast.py` using - `_insta_of_partner_styling` when softcore cast is `same_as_hardcore`. +- Partner styling is resolved through `pair_cast.softcore_partner_styling` when + softcore cast is `same_as_hardcore`. Hardcore row: @@ -829,7 +830,7 @@ pair metadata through the core Python APIs, then verifies: | --- | --- | | Wrong main category/subcategory frequency | Category node config, `category_library.load_category_library`, category JSON weights. | | Wrong outfit/clothing item | Relevant category JSON, `pair_options.py`, `SxCP Character Clothing`. | -| Nude/clothing state confusing Krea2 | `build_insta_of_pair` clothing state helpers, then `krea_clothing.natural_clothing_state`. | +| Nude/clothing state confusing Krea2 | `pair_clothing.py`, then `krea_clothing.natural_clothing_state`. | | Wrong location | `categories/location_pools.json`, category `scene_pool`, `_scene_pool`. | | Location good but camera/location layout wrong | `_camera_scene_directive_for_context`, coworking adapter functions. | | Repeated desk/anchor in POV foreground | Coworking direction/distance/elevation helpers. | diff --git a/pair_cast.py b/pair_cast.py index cb56b10..862d4b2 100644 --- a/pair_cast.py +++ b/pair_cast.py @@ -2,11 +2,75 @@ from __future__ import annotations from typing import Any, Callable +try: + from . import pair_clothing + from . import pair_options +except ImportError: # Allows local smoke tests with top-level imports. + import pair_clothing + import pair_options + DescriptorFromRow = Callable[[dict[str, Any]], str] CastDescriptors = Callable[..., list[str]] -PartnerStyling = Callable[..., dict[str, Any]] -CastPhrase = Callable[[int, int], str] +AxisRng = Callable[[dict[str, int], str, int, int], Any] +Choose = Callable[[Any, list[str]], str] +SlotSoftcoreOutfit = Callable[[dict[str, Any] | None, Any], str] + + +def cast_summary_phrase(women_count: int, men_count: int) -> str: + women_count = max(0, int(women_count)) + men_count = max(0, int(men_count)) + if women_count + men_count == 0: + women_count = 1 + person_count = women_count + men_count + women_label = "woman" if women_count == 1 else "women" + men_label = "man" if men_count == 1 else "men" + return f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults" + + +def softcore_partner_styling( + *, + seed_config: dict[str, int], + seed: int, + row_number: int, + women_count: int, + men_count: int, + pov_labels: list[str] | None, + label_map: dict[str, dict[str, Any]] | None, + axis_rng: AxisRng, + choose: Choose, + slot_softcore_outfit: SlotSoftcoreOutfit, +) -> dict[str, Any]: + content_rng = axis_rng(seed_config, "content", seed, row_number + 421) + pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421) + pov_set = set(pov_labels or []) + outfits: list[str] = [] + for index in range(max(0, women_count - 1)): + label = chr(ord("B") + index) + full_label = f"Woman {label}" + outfit = slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or choose( + content_rng, + pair_options.INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS, + ) + sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit) + if sentence: + outfits.append(sentence) + for index in range(max(0, men_count)): + label = chr(ord("A") + index) + full_label = f"Man {label}" + if full_label in pov_set: + continue + outfit = slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or choose( + content_rng, + pair_options.INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS, + ) + sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit) + if sentence: + outfits.append(sentence) + return { + "outfits": outfits, + "pose": choose(pose_rng, pair_options.SOFTCORE_CAST_POSES), + } def resolve_insta_pair_cast_context( @@ -31,8 +95,9 @@ def resolve_insta_pair_cast_context( descriptor_from_row: DescriptorFromRow, build_cast_descriptors: CastDescriptors, prompt_cast_descriptors: Callable[[str], str], - partner_styling: PartnerStyling, - cast_phrase: CastPhrase, + axis_rng: AxisRng, + choose: Choose, + slot_softcore_outfit: SlotSoftcoreOutfit, ) -> dict[str, Any]: descriptor = descriptor_from_row(soft_row) cast_descriptors = build_cast_descriptors( @@ -51,14 +116,17 @@ def resolve_insta_pair_cast_context( 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, + soft_partner_styling = softcore_partner_styling( + seed_config=parsed_seed_config, + seed=seed, + row_number=row_number, + women_count=hard_women_count if same_softcore_cast else 1, + men_count=hard_men_count if same_softcore_cast else 0, + pov_labels=pov_character_labels if same_softcore_cast else [], + label_map=character_slot_map, + axis_rng=axis_rng, + choose=choose, + slot_softcore_outfit=slot_softcore_outfit, ) if not same_softcore_cast: soft_partner_styling = {"outfits": [], "pose": ""} @@ -67,7 +135,7 @@ def resolve_insta_pair_cast_context( 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)}" + else f"soft creator-teaser setup with {cast_summary_phrase(hard_women_count, hard_men_count)}" ) soft_cast_presence = ( ( @@ -86,7 +154,7 @@ def resolve_insta_pair_cast_context( if same_softcore_cast and soft_partner_outfit_text else "" ) - hard_cast = cast_phrase(hard_women_count, hard_men_count) + hard_cast = cast_summary_phrase(hard_women_count, hard_men_count) soft_descriptor_sentence = ( f"Cast descriptors: {soft_cast_descriptor_text}. " if same_softcore_cast diff --git a/prompt_builder.py b/prompt_builder.py index 7479c10..6ef0925 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -3934,18 +3934,10 @@ def _insta_of_cast_descriptors( return descriptors -def _insta_of_cast_phrase(women_count: int, men_count: int) -> str: - context = _configured_cast_context(women_count, men_count) - return context["cast_summary"] - - def _insta_of_prompt_cast_descriptors(text: str) -> str: return str(text or "").replace("Woman A / primary creator:", "Woman A:") -SOFTCORE_CAST_POSES = pair_options.SOFTCORE_CAST_POSES - - def _insta_of_softcore_category(level: str) -> tuple[str, str]: return pair_options.softcore_category(level) @@ -3962,41 +3954,6 @@ def _insta_of_softcore_pose(rng: random.Random, level: str) -> str: return g.choose(rng, pair_options.softcore_pose_pool(level)) -def _insta_of_partner_styling( - seed_config: dict[str, int], - seed: int, - row_number: int, - women_count: int, - men_count: int, - pov_labels: list[str] | None = None, - label_map: dict[str, dict[str, Any]] | None = None, -) -> dict[str, Any]: - content_rng = _axis_rng(seed_config, "content", seed, row_number + 421) - pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421) - pov_set = set(pov_labels or []) - outfits: list[str] = [] - for index in range(max(0, women_count - 1)): - label = chr(ord("B") + index) - full_label = f"Woman {label}" - outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS) - sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit) - if sentence: - outfits.append(sentence) - for index in range(max(0, men_count)): - label = chr(ord("A") + index) - full_label = f"Man {label}" - if full_label in pov_set: - continue - outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS) - sentence = pair_clothing.softcore_outfit_sentence(full_label, outfit) - if sentence: - outfits.append(sentence) - return { - "outfits": outfits, - "pose": g.choose(pose_rng, SOFTCORE_CAST_POSES), - } - - def build_insta_of_pair( row_number: int, start_index: int, @@ -4098,8 +4055,9 @@ def build_insta_of_pair( 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, + axis_rng=_axis_rng, + choose=g.choose, + slot_softcore_outfit=_slot_softcore_outfit, ) descriptor = cast_context["descriptor"] cast_descriptors = cast_context["cast_descriptors"] diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 565a691..67fa265 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -41,6 +41,7 @@ import krea_cast # noqa: E402 import krea_formatter # noqa: E402 import location_config # noqa: E402 import loop_nodes # noqa: E402 +import pair_cast # noqa: E402 import pair_clothing # noqa: E402 import prompt_builder as pb # noqa: E402 import pov_policy # noqa: E402 @@ -1681,6 +1682,27 @@ def smoke_pair_options_policy() -> None: == ["Woman A's body is fully exposed, bare skin unobstructed"], "Pair clothing character entries should skip POV labels", ) + _expect( + pair_cast.cast_summary_phrase(2, 1) == "2 women, 1 man, 3 total adults", + "Pair cast summary phrase should live in pair_cast", + ) + partner_styling = pair_cast.softcore_partner_styling( + seed_config={}, + seed=1, + row_number=1, + women_count=2, + men_count=1, + pov_labels=["Man A"], + label_map={"Woman B": {"softcore_outfit": "custom satin dress"}, "Man A": {"softcore_outfit": "hidden"}}, + axis_rng=lambda _config, _axis, seed_value, row_value: random.Random(seed_value + row_value), + choose=lambda _rng, pool: pool[0], + slot_softcore_outfit=lambda slot, _rng: str((slot or {}).get("softcore_outfit") or ""), + ) + _expect( + partner_styling["outfits"] == ["Woman B wears custom satin dress"], + "Pair cast partner styling should use configured partner outfit and skip POV men", + ) + _expect_text("pair_cast.partner_pose", partner_styling.get("pose"), 12) options = json.loads( pb.build_insta_of_options_json( softcore_expression_enabled="false",