Move pair cast styling policy
This commit is contained in:
@@ -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. |
|
||||
|
||||
+82
-14
@@ -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
|
||||
|
||||
+3
-45
@@ -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"]
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user