Extract Insta option policy

This commit is contained in:
2026-06-26 23:43:14 +02:00
parent 30b5280da1
commit bc5ec35ef7
5 changed files with 521 additions and 322 deletions
@@ -151,6 +151,10 @@ Keep here:
Already isolated:
- Insta/OF option normalization, softcore category/outfit/pose pools, partner
outfit pools, clothing-continuity labels, negatives, and hardcore cast count
policy live in `pair_options.py`; `prompt_builder.py` keeps public delegate
wrappers for existing nodes and tests.
- 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.
+7 -5
View File
@@ -68,6 +68,7 @@ Core helper ownership:
| Python module | What it owns |
| --- | --- |
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, and hardcore cast count policy. |
| `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_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 hardcore clothing continuity, action-aware body-access flags, conflicting outfit-piece cleanup, default visible-men clothing, and final root clothing-state assembly. |
@@ -112,7 +113,7 @@ These recipes identify the intended road before editing prompt text.
| 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 |
| 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`, `INSTA_OF_SOFTCORE_OUTFITS`, category item templates |
| 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`, `_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`, `_apply_composition_config_to_legacy_row`, Krea composition phrase |
| Use Qwen/orbit camera geometry | Qwen/orbit node -> camera_config -> builder/pair | For pair, use `softcore_camera_config` and/or `hardcore_camera_config`; set mode from config in options | `_camera_config_with_mode`, `_camera_directive`, `_camera_scene_directive_for_context` |
@@ -233,6 +234,7 @@ This table is the first stop when the selected content is wrong.
| `sexual_poses.json` foreplay/interaction/manual/oral/outercourse/penetration/etc. | Hardcore action and porn-scene interaction templates, role graphs, axis values, hardcore pool references | `pose` for pose-content route, also `role`; sometimes `content` aliases matter | High because Krea2 rewrites action and POV position text |
| `location_pools.json` | Reusable scene pools and legacy scene extensions | `scene` | Medium when a camera-aware adapter changes scene/composition wording |
| `expression_composition_pools.json` | Reusable expressions and framing/composition pools | `expression`, `composition` | Medium because formatter may label or suppress expressions |
| `pair_options.py` | Insta/OF option defaults, softcore level-to-category mapping, creator outfit/pose pools, partner outfit pools, negatives, and hard cast count policy | Options node plus `content`/`pose` axes inside pair route | Medium because pair route pools must remain consistent with Krea/SDXL pair formatting |
| `generate_prompt_batches.py` legacy pools | Built-in generator clothing, pose, expression, scene, composition lists | Main row seed plus axis config through legacy adapter | Medium because legacy prompt format is field-label heavy |
When adding a new pool, choose JSON when the change is pure selectable wording.
@@ -387,10 +389,10 @@ flowchart TD
Softcore row:
- Category comes from `INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL`.
- Category comes from `pair_options.INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL`.
- Outfit comes from character slot `softcore_outfit` if present, otherwise
`INSTA_OF_SOFTCORE_OUTFITS`.
- Soft pose comes from `INSTA_OF_SOFTCORE_POSES`.
`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`.
@@ -789,7 +791,7 @@ pair metadata through the core Python APIs, then verifies:
| Symptom | First file/function to inspect |
| --- | --- |
| Wrong main category/subcategory frequency | Category node config, `category_library.load_category_library`, category JSON weights. |
| Wrong outfit/clothing item | Relevant category JSON, `INSTA_OF_SOFTCORE_OUTFITS`, `SxCP Character Clothing`. |
| 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`. |
| 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. |
+409
View File
@@ -0,0 +1,409 @@
from __future__ import annotations
import json
import re
from typing import Any
INSTA_OF_SOFT_LEVELS = {
"social_tease": "Instagram-style thirst-trap post, suggestive polished social feed energy",
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate",
"implied_nude": "implied nude creator set, strategically covered body and intimate teaser framing",
"explicit_tease": "stronger adult teaser set with bolder nude-adjacent styling and solo-tease framing",
"explicit_nude": "explicit nude creator set with fully nude solo-tease framing",
}
INSTA_OF_HARDCORE_LEVELS = {
"explicit": "explicit adult creator content with clear sexual contact and adult-only framing",
"hardcore": "hardcore adult creator content with anatomically clear sexual contact and intense body language",
}
INSTA_OF_PLATFORM_STYLES = {
"hybrid": "hybrid Instagram-to-OF creator shoot, polished social-media framing with intimate subscriber-content energy",
"instagram": "Instagram-inspired creator shoot, polished mirror-selfie and feed-post aesthetics",
"onlyfans": "OnlyFans-inspired creator shoot, intimate subscriber-view camera and candid premium-content framing",
}
INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = {
"none": "",
"same_outfit": "Woman A keeps her teaser outfit on with the body contact readable",
"partially_removed": "Woman A's teaser outfit is pushed aside and partly removed where needed, leaving body contact unobstructed",
"implied_nude": "Woman A's body is partly exposed, with fabric slipping off or covering only part of the body",
"explicit_nude": "Woman A's body is fully exposed, bare skin unobstructed",
}
INSTA_OF_NEGATIVE = (
"minors, childlike appearance, teen, underage, schoolgirl, non-consensual, coercion, rape, "
"violence, injury, blood, gore, incest, bestiality, watermark, logo, readable username, social media UI"
)
INSTA_OF_SOFT_NEGATIVE = (
INSTA_OF_NEGATIVE
+ ", explicit intercourse, penetration, oral sex, cumshot, genital contact, group sex, "
"shirtless partner, bare-chested partner, partner nudity"
)
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL = {
"social_tease": "Casual clothes / Smart casual",
"lingerie_tease": "Provocative erotic clothes / Provocative lingerie",
"implied_nude": "Provocative erotic clothes / Provocative lingerie",
"explicit_tease": "Provocative erotic clothes / Sheer exposed",
"explicit_nude": "Provocative erotic clothes / Nude accessories",
}
INSTA_OF_SOFTCORE_OUTFITS = {
"social_tease": [
"cropped fitted tee, low-rise jeans, delicate jewelry, and polished feed-post styling",
"oversized off-shoulder sweater with fitted shorts and soft lounge socks",
"ribbed tank top, mini skirt, hoop earrings, and casual creator styling",
"silky camisole tucked into relaxed trousers with a subtle waist chain",
"sporty crop top, bike shorts, clean sneakers, and glossy social-feed styling",
"button-down shirt tied at the waist over a fitted bralette and denim shorts",
"body-hugging knit dress with bare shoulders and simple heels",
"relaxed hoodie half-zipped over a crop top with high-cut shorts",
],
"lingerie_tease": [
"black lace lingerie set with opaque cups, high-waisted briefs, garter straps, and sheer robe",
"satin bralette and matching high-waisted panties under an oversized shirt",
"lace bodysuit with opaque cups, soft stockings, and delicate garter details",
"silk slip dress with thin straps, thigh slit, and subtle lace trim",
"matching balconette bra and brief set under a loosely draped satin robe",
"velvet lingerie set with covered cups, garter belt, sheer stockings, and small gold accents",
"mesh robe over a covered lace teddy, styled as a premium creator teaser",
"structured corset top with opaque panels, matching briefs, and sheer stockings",
],
"implied_nude": [
"oversized white shirt slipping off one shoulder, body mostly covered, bare legs, and soft creator-shot styling",
"towel wrap held across the chest and hips, implied nude but fully covered",
"satin sheet wrapped around the body with shoulders and legs visible but intimate areas covered",
"open robe held closed by hand, implied nude beneath without explicit exposure",
"bath towel and damp hair after a shower, covered chest and hips, intimate creator styling",
"soft blanket wrapped around the body, bare shoulders visible, sensual but covered",
],
"explicit_tease": [
"sheer robe over matching lingerie with intimate areas obscured by lace pattern and pose",
"wet-look bodysuit with opaque panels, high-cut legs, and glossy club-light styling",
"transparent mesh dress over covered lingerie, posed as an adult creator teaser",
"lace teddy with strategic opaque embroidery, garter straps, and sheer stockings",
"bare-shoulder robe opened around covered lingerie, bold solo adult tease",
"strappy lingerie set with covered cups and high-waisted bottoms, styled as a stronger solo teaser",
],
"explicit_nude": [
"body fully exposed with jewelry accents and direct adult selfie confidence",
"mirror-selfie body exposure with jewelry accents and bold creator-shot framing",
"body fully exposed with direct eye contact and soft creator-shot styling",
"vanity-mirror body exposure with necklace detail and premium creator-shot styling",
"shower-afterglow body exposure with wet hair, skin highlights, and phone-shot framing",
"indoor body exposure with one hand holding the phone and direct camera awareness",
],
}
INSTA_OF_SOFTCORE_POSES = {
"social_tease": [
"taking a mirror selfie with one hip angled and relaxed social-feed confidence",
"leaning against a doorway with one hand holding the phone and a casual teasing smile",
"sitting casually for a polished outfit-check selfie",
"standing by the window with shoulders relaxed and body angled toward the phone",
"posing in a clean feed-post stance with one hand at the waist",
"stretching one arm above the head in a casual morning selfie pose",
],
"lingerie_tease": [
"taking a mirror lingerie selfie with one hip angled and the outfit clearly visible",
"kneeling in a covered lingerie teaser pose with hands resting on fabric",
"leaning with the robe draped around covered lingerie",
"standing in a three-quarter lingerie outfit-check pose with legs softly crossed",
"sitting with stockings and garter details visible in a controlled teaser pose",
"turning slightly over one shoulder to show the lingerie silhouette",
],
"implied_nude": [
"holding the towel or sheet securely in place while posing for an implied nude selfie",
"sitting with soft fabric wrapped securely around the body and shoulders visible",
"standing by a mirror with a towel wrapped around the body",
"reclining under satin fabric with intimate areas fully obscured",
"holding an open robe closed in a covered implied nude teaser pose",
"looking into the phone camera while wrapped in a blanket with bare shoulders visible",
],
"explicit_tease": [
"posing in a stronger adult teaser stance with covered lingerie and direct camera awareness",
"kneeling with a sheer robe arranged around covered lingerie",
"standing close to the mirror with the outfit framed boldly",
"leaning forward slightly with hands on the robe and intimate areas obscured",
"sitting in a bolder covered lingerie pose with direct eye contact",
"arching subtly in a solo adult tease while the styling keeps explicit anatomy obscured",
],
"explicit_nude": [
"taking a bold mirror selfie with direct eye contact and the body clearly framed",
"posing with body fully exposed and jewelry accents as styling",
"standing with body fully exposed in a premium creator-shot pose",
"reclining with body fully exposed and the phone held close",
"turning slightly in a mirror pose with the body framed head-to-thigh",
"kneeling in a controlled adult teaser pose with body fully exposed and direct phone-camera awareness",
],
}
INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS = [
"satin slip dress under an oversized shirt",
"soft cardigan over a camisole with relaxed trousers",
"fitted crop top with high-waisted jeans",
"silky robe over a covered bralette and lounge shorts",
"bodycon mini dress with simple heels",
"ribbed tank top with joggers and delicate jewelry",
"oversized tee with fitted shorts and lounge socks",
"button-down shirt with a fitted skirt",
]
INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS = [
"fitted black tee with dark jeans",
"buttoned linen shirt with chinos",
"hoodie and joggers",
"open overshirt over a fitted tank with relaxed trousers",
"gym tee with track pants and a towel over one shoulder",
"casual knit shirt with tailored trousers",
"dark crewneck sweater with jeans",
"short-sleeve button-up shirt with relaxed shorts",
]
SOFTCORE_CAST_POSES = [
"standing together for a mirror selfie with relaxed close body language",
"posing shoulder-to-shoulder in a creator-shot group teaser",
"leaning together in a polished subscriber preview",
"sitting close together with relaxed hands and styled outfit visibility",
"arranged around Woman A in a flirtatious creator-teaser pose",
"posing together as a coordinated adult creator set",
"standing near the phone tripod with relaxed teasing body language",
"framed together in a softcore cast reveal",
]
def _is_false(value: Any) -> bool:
if isinstance(value, bool):
return not value
text = str(value).strip().lower()
return text in {"false", "0", "no", "off", "disabled"}
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
try:
number = float(value)
except (TypeError, ValueError):
number = default
return max(min_value, min(max_value, number))
def _normalize_free_text_values(values: Any) -> list[str]:
if isinstance(values, str):
raw_values = [part.strip() for part in re.split(r"[\n;]+", values) if part.strip()]
elif isinstance(values, (list, tuple, set)):
raw_values = list(values)
else:
raw_values = []
normalized: list[str] = []
for raw_value in raw_values:
value = str(raw_value or "").strip()
if value and value not in normalized:
normalized.append(value)
return normalized
def character_softcore_outfit_values(source: str, custom_outfits: str = "") -> list[str]:
source = str(source or "no_change").strip()
if source in INSTA_OF_SOFTCORE_OUTFITS:
return list(INSTA_OF_SOFTCORE_OUTFITS[source])
if source == "partner_woman":
return list(INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)
if source == "partner_man":
return list(INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)
if source == "custom":
return _normalize_free_text_values(custom_outfits)
return []
def character_hardcore_clothing_values(state: str, custom_clothing: str = "") -> list[str]:
state = str(state or "no_change").strip()
if state == "fully_nude":
return ["fully nude"]
if state == "partly_exposed":
return ["partly nude, body exposed"]
if state == "same_outfit":
return ["keeps the teaser outfit on with the body contact readable"]
if state == "partially_removed":
return ["teaser outfit is pushed aside and partly removed where needed, leaving body contact unobstructed"]
if state == "custom":
return _normalize_free_text_values(custom_clothing)
return []
def build_insta_of_options_json(
softcore_cast: str = "solo",
hardcore_cast: str = "use_counts",
hardcore_women_count: int = 1,
hardcore_men_count: int = 1,
softcore_level: str = "lingerie_tease",
hardcore_level: str = "hardcore",
platform_style: str = "hybrid",
continuity: str = "same_creator_same_room",
hardcore_clothing_continuity: str = "partially_removed",
softcore_camera_mode: str = "handheld_selfie",
hardcore_camera_mode: str = "from_camera_config",
camera_detail: str = "from_camera_config",
softcore_expression_intensity: float = 0.45,
hardcore_expression_intensity: float = 0.85,
softcore_expression_enabled: bool = True,
hardcore_expression_enabled: bool = True,
hardcore_detail_density: str = "balanced",
hardcore_detail_density_choices: list[str] | tuple[str, ...] = ("compact", "balanced", "dense"),
) -> str:
hardcore_detail_density = (
hardcore_detail_density if hardcore_detail_density in hardcore_detail_density_choices else "balanced"
)
return json.dumps(
{
"softcore_cast": softcore_cast,
"hardcore_cast": hardcore_cast,
"hardcore_women_count": int(hardcore_women_count),
"hardcore_men_count": int(hardcore_men_count),
"softcore_level": softcore_level,
"hardcore_level": hardcore_level,
"platform_style": platform_style,
"continuity": continuity,
"hardcore_clothing_continuity": hardcore_clothing_continuity,
"softcore_camera_mode": softcore_camera_mode,
"hardcore_camera_mode": hardcore_camera_mode,
"camera_detail": camera_detail,
"softcore_expression_enabled": not _is_false(softcore_expression_enabled),
"hardcore_expression_enabled": not _is_false(hardcore_expression_enabled),
"softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45),
"hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85),
"hardcore_detail_density": hardcore_detail_density,
},
ensure_ascii=True,
sort_keys=True,
)
def parse_insta_of_options(
options_json: str | dict[str, Any] | None,
*,
camera_mode_choices: dict[str, str] | list[str] | tuple[str, ...],
camera_detail_choices: list[str] | tuple[str, ...],
hardcore_detail_density_choices: list[str] | tuple[str, ...],
) -> dict[str, Any]:
defaults = {
"softcore_cast": "solo",
"hardcore_cast": "use_counts",
"hardcore_women_count": 1,
"hardcore_men_count": 1,
"softcore_level": "lingerie_tease",
"hardcore_level": "hardcore",
"platform_style": "hybrid",
"continuity": "same_creator_same_room",
"hardcore_clothing_continuity": "partially_removed",
"softcore_camera_mode": "handheld_selfie",
"hardcore_camera_mode": "from_camera_config",
"camera_detail": "from_camera_config",
"softcore_expression_enabled": True,
"hardcore_expression_enabled": True,
"softcore_expression_intensity": 0.45,
"hardcore_expression_intensity": 0.85,
"hardcore_detail_density": "balanced",
}
if not options_json:
return defaults
if isinstance(options_json, dict):
raw = options_json
else:
try:
raw = json.loads(str(options_json))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid Insta/OF options JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("Insta/OF options must be a JSON object")
valid_camera_modes = set(camera_mode_choices) if isinstance(camera_mode_choices, dict) else set(camera_mode_choices)
parsed = {**defaults, **raw}
parsed["softcore_cast"] = parsed["softcore_cast"] if parsed["softcore_cast"] in ("solo", "same_as_hardcore") else defaults["softcore_cast"]
parsed["hardcore_cast"] = parsed["hardcore_cast"] if parsed["hardcore_cast"] in ("use_counts", "couple", "threesome", "group") else defaults["hardcore_cast"]
parsed["softcore_level"] = parsed["softcore_level"] if parsed["softcore_level"] in INSTA_OF_SOFT_LEVELS else defaults["softcore_level"]
parsed["hardcore_level"] = parsed["hardcore_level"] if parsed["hardcore_level"] in INSTA_OF_HARDCORE_LEVELS else defaults["hardcore_level"]
parsed["platform_style"] = parsed["platform_style"] if parsed["platform_style"] in INSTA_OF_PLATFORM_STYLES else defaults["platform_style"]
parsed["continuity"] = parsed["continuity"] if parsed["continuity"] in ("same_creator_same_room", "same_creator_new_scene") else defaults["continuity"]
parsed["hardcore_clothing_continuity"] = (
parsed["hardcore_clothing_continuity"]
if parsed["hardcore_clothing_continuity"] in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
else defaults["hardcore_clothing_continuity"]
)
parsed["softcore_camera_mode"] = (
parsed["softcore_camera_mode"]
if parsed["softcore_camera_mode"] in valid_camera_modes or parsed["softcore_camera_mode"] == "from_camera_config"
else defaults["softcore_camera_mode"]
)
if (
parsed["hardcore_camera_mode"] not in valid_camera_modes
and parsed["hardcore_camera_mode"] not in ("from_camera_config", "same_as_softcore")
):
parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"]
parsed["camera_detail"] = (
parsed["camera_detail"]
if parsed["camera_detail"] in camera_detail_choices or parsed["camera_detail"] == "from_camera_config"
else defaults["camera_detail"]
)
parsed["softcore_expression_enabled"] = not _is_false(parsed.get("softcore_expression_enabled", True))
parsed["hardcore_expression_enabled"] = not _is_false(parsed.get("hardcore_expression_enabled", True))
parsed["softcore_expression_intensity"] = _clamped_float(
parsed.get("softcore_expression_intensity"),
defaults["softcore_expression_intensity"],
)
parsed["hardcore_expression_intensity"] = _clamped_float(
parsed.get("hardcore_expression_intensity"),
defaults["hardcore_expression_intensity"],
)
parsed["hardcore_detail_density"] = (
parsed["hardcore_detail_density"]
if parsed.get("hardcore_detail_density") in hardcore_detail_density_choices
else defaults["hardcore_detail_density"]
)
for key in ("hardcore_women_count", "hardcore_men_count"):
try:
parsed[key] = max(0, min(12, int(parsed[key])))
except (TypeError, ValueError):
parsed[key] = defaults[key]
return parsed
def hardcore_counts(options: dict[str, Any]) -> tuple[int, int]:
policy = str(options.get("hardcore_cast", "use_counts"))
if policy == "couple":
women_count, men_count = 1, 1
elif policy == "threesome":
women_count, men_count = 2, 1
elif policy == "group":
women_count, men_count = 3, 2
else:
women_count = int(options.get("hardcore_women_count") or 0)
men_count = int(options.get("hardcore_men_count") or 0)
women_count = max(1, min(12, women_count))
men_count = max(0, min(12, men_count))
if women_count + men_count < 2:
men_count = 1
return women_count, men_count
def softcore_category(level: str) -> tuple[str, str]:
subcategory = INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL.get(
level,
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL["lingerie_tease"],
)
category, _subcategory = subcategory.split(" / ", 1)
return category, subcategory
def softcore_outfit_pool(level: str) -> list[str]:
return list(INSTA_OF_SOFTCORE_OUTFITS.get(level, INSTA_OF_SOFTCORE_OUTFITS["lingerie_tease"]))
def softcore_pose_pool(level: str) -> list[str]:
return list(INSTA_OF_SOFTCORE_POSES.get(level, INSTA_OF_SOFTCORE_POSES["lingerie_tease"]))
def softcore_item_prompt_label(level: str) -> str:
return "Body exposure" if level == "explicit_nude" else "Outfit"
+45 -317
View File
@@ -30,6 +30,7 @@ try:
from . import pair_cast
from . import pair_output
from . import pair_rows
from . import pair_options
from . import scene_camera_adapters
from .hardcore_text_cleanup import (
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
@@ -64,6 +65,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
import pair_cast
import pair_output
import pair_rows
import pair_options
import scene_camera_adapters
from hardcore_text_cleanup import (
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
@@ -6492,191 +6494,25 @@ def build_prompt_from_configs(
)
INSTA_OF_SOFT_LEVELS = {
"social_tease": "Instagram-style thirst-trap post, suggestive polished social feed energy",
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate",
"implied_nude": "implied nude creator set, strategically covered body and intimate teaser framing",
"explicit_tease": "stronger adult teaser set with bolder nude-adjacent styling and solo-tease framing",
"explicit_nude": "explicit nude creator set with fully nude solo-tease framing",
}
INSTA_OF_HARDCORE_LEVELS = {
"explicit": "explicit adult creator content with clear sexual contact and adult-only framing",
"hardcore": "hardcore adult creator content with anatomically clear sexual contact and intense body language",
}
INSTA_OF_PLATFORM_STYLES = {
"hybrid": "hybrid Instagram-to-OF creator shoot, polished social-media framing with intimate subscriber-content energy",
"instagram": "Instagram-inspired creator shoot, polished mirror-selfie and feed-post aesthetics",
"onlyfans": "OnlyFans-inspired creator shoot, intimate subscriber-view camera and candid premium-content framing",
}
INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = {
"none": "",
"same_outfit": "Woman A keeps her teaser outfit on with the body contact readable",
"partially_removed": "Woman A's teaser outfit is pushed aside and partly removed where needed, leaving body contact unobstructed",
"implied_nude": "Woman A's body is partly exposed, with fabric slipping off or covering only part of the body",
"explicit_nude": "Woman A's body is fully exposed, bare skin unobstructed",
}
INSTA_OF_NEGATIVE = (
"minors, childlike appearance, teen, underage, schoolgirl, non-consensual, coercion, rape, "
"violence, injury, blood, gore, incest, bestiality, watermark, logo, readable username, social media UI"
)
INSTA_OF_SOFT_NEGATIVE = (
INSTA_OF_NEGATIVE
+ ", explicit intercourse, penetration, oral sex, cumshot, genital contact, group sex, "
"shirtless partner, bare-chested partner, partner nudity"
)
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL = {
"social_tease": "Casual clothes / Smart casual",
"lingerie_tease": "Provocative erotic clothes / Provocative lingerie",
"implied_nude": "Provocative erotic clothes / Provocative lingerie",
"explicit_tease": "Provocative erotic clothes / Sheer exposed",
"explicit_nude": "Provocative erotic clothes / Nude accessories",
}
INSTA_OF_SOFTCORE_OUTFITS = {
"social_tease": [
"cropped fitted tee, low-rise jeans, delicate jewelry, and polished feed-post styling",
"oversized off-shoulder sweater with fitted shorts and soft lounge socks",
"ribbed tank top, mini skirt, hoop earrings, and casual creator styling",
"silky camisole tucked into relaxed trousers with a subtle waist chain",
"sporty crop top, bike shorts, clean sneakers, and glossy social-feed styling",
"button-down shirt tied at the waist over a fitted bralette and denim shorts",
"body-hugging knit dress with bare shoulders and simple heels",
"relaxed hoodie half-zipped over a crop top with high-cut shorts",
],
"lingerie_tease": [
"black lace lingerie set with opaque cups, high-waisted briefs, garter straps, and sheer robe",
"satin bralette and matching high-waisted panties under an oversized shirt",
"lace bodysuit with opaque cups, soft stockings, and delicate garter details",
"silk slip dress with thin straps, thigh slit, and subtle lace trim",
"matching balconette bra and brief set under a loosely draped satin robe",
"velvet lingerie set with covered cups, garter belt, sheer stockings, and small gold accents",
"mesh robe over a covered lace teddy, styled as a premium creator teaser",
"structured corset top with opaque panels, matching briefs, and sheer stockings",
],
"implied_nude": [
"oversized white shirt slipping off one shoulder, body mostly covered, bare legs, and soft creator-shot styling",
"towel wrap held across the chest and hips, implied nude but fully covered",
"satin sheet wrapped around the body with shoulders and legs visible but intimate areas covered",
"open robe held closed by hand, implied nude beneath without explicit exposure",
"bath towel and damp hair after a shower, covered chest and hips, intimate creator styling",
"soft blanket wrapped around the body, bare shoulders visible, sensual but covered",
],
"explicit_tease": [
"sheer robe over matching lingerie with intimate areas obscured by lace pattern and pose",
"wet-look bodysuit with opaque panels, high-cut legs, and glossy club-light styling",
"transparent mesh dress over covered lingerie, posed as an adult creator teaser",
"lace teddy with strategic opaque embroidery, garter straps, and sheer stockings",
"bare-shoulder robe opened around covered lingerie, bold solo adult tease",
"strappy lingerie set with covered cups and high-waisted bottoms, styled as a stronger solo teaser",
],
"explicit_nude": [
"body fully exposed with jewelry accents and direct adult selfie confidence",
"mirror-selfie body exposure with jewelry accents and bold creator-shot framing",
"body fully exposed with direct eye contact and soft creator-shot styling",
"vanity-mirror body exposure with necklace detail and premium creator-shot styling",
"shower-afterglow body exposure with wet hair, skin highlights, and phone-shot framing",
"indoor body exposure with one hand holding the phone and direct camera awareness",
],
}
INSTA_OF_SOFTCORE_POSES = {
"social_tease": [
"taking a mirror selfie with one hip angled and relaxed social-feed confidence",
"leaning against a doorway with one hand holding the phone and a casual teasing smile",
"sitting casually for a polished outfit-check selfie",
"standing by the window with shoulders relaxed and body angled toward the phone",
"posing in a clean feed-post stance with one hand at the waist",
"stretching one arm above the head in a casual morning selfie pose",
],
"lingerie_tease": [
"taking a mirror lingerie selfie with one hip angled and the outfit clearly visible",
"kneeling in a covered lingerie teaser pose with hands resting on fabric",
"leaning with the robe draped around covered lingerie",
"standing in a three-quarter lingerie outfit-check pose with legs softly crossed",
"sitting with stockings and garter details visible in a controlled teaser pose",
"turning slightly over one shoulder to show the lingerie silhouette",
],
"implied_nude": [
"holding the towel or sheet securely in place while posing for an implied nude selfie",
"sitting with soft fabric wrapped securely around the body and shoulders visible",
"standing by a mirror with a towel wrapped around the body",
"reclining under satin fabric with intimate areas fully obscured",
"holding an open robe closed in a covered implied nude teaser pose",
"looking into the phone camera while wrapped in a blanket with bare shoulders visible",
],
"explicit_tease": [
"posing in a stronger adult teaser stance with covered lingerie and direct camera awareness",
"kneeling with a sheer robe arranged around covered lingerie",
"standing close to the mirror with the outfit framed boldly",
"leaning forward slightly with hands on the robe and intimate areas obscured",
"sitting in a bolder covered lingerie pose with direct eye contact",
"arching subtly in a solo adult tease while the styling keeps explicit anatomy obscured",
],
"explicit_nude": [
"taking a bold mirror selfie with direct eye contact and the body clearly framed",
"posing with body fully exposed and jewelry accents as styling",
"standing with body fully exposed in a premium creator-shot pose",
"reclining with body fully exposed and the phone held close",
"turning slightly in a mirror pose with the body framed head-to-thigh",
"kneeling in a controlled adult teaser pose with body fully exposed and direct phone-camera awareness",
],
}
INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS = [
"satin slip dress under an oversized shirt",
"soft cardigan over a camisole with relaxed trousers",
"fitted crop top with high-waisted jeans",
"silky robe over a covered bralette and lounge shorts",
"bodycon mini dress with simple heels",
"ribbed tank top with joggers and delicate jewelry",
"oversized tee with fitted shorts and lounge socks",
"button-down shirt with a fitted skirt",
]
INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS = [
"fitted black tee with dark jeans",
"buttoned linen shirt with chinos",
"hoodie and joggers",
"open overshirt over a fitted tank with relaxed trousers",
"gym tee with track pants and a towel over one shoulder",
"casual knit shirt with tailored trousers",
"dark crewneck sweater with jeans",
"short-sleeve button-up shirt with relaxed shorts",
]
INSTA_OF_SOFT_LEVELS = pair_options.INSTA_OF_SOFT_LEVELS
INSTA_OF_HARDCORE_LEVELS = pair_options.INSTA_OF_HARDCORE_LEVELS
INSTA_OF_PLATFORM_STYLES = pair_options.INSTA_OF_PLATFORM_STYLES
INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = pair_options.INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
INSTA_OF_NEGATIVE = pair_options.INSTA_OF_NEGATIVE
INSTA_OF_SOFT_NEGATIVE = pair_options.INSTA_OF_SOFT_NEGATIVE
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL = pair_options.INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL
INSTA_OF_SOFTCORE_OUTFITS = pair_options.INSTA_OF_SOFTCORE_OUTFITS
INSTA_OF_SOFTCORE_POSES = pair_options.INSTA_OF_SOFTCORE_POSES
INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS = pair_options.INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS
INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS = pair_options.INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS
def character_softcore_outfit_values(source: str, custom_outfits: str = "") -> list[str]:
source = str(source or "no_change").strip()
if source in INSTA_OF_SOFTCORE_OUTFITS:
return list(INSTA_OF_SOFTCORE_OUTFITS[source])
if source == "partner_woman":
return list(INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)
if source == "partner_man":
return list(INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)
if source == "custom":
return _normalize_characteristic_values(custom_outfits, None, allow_free_text=True)
return []
return pair_options.character_softcore_outfit_values(source, custom_outfits)
def character_hardcore_clothing_values(state: str, custom_clothing: str = "") -> list[str]:
state = str(state or "no_change").strip()
if state == "fully_nude":
return ["fully nude"]
if state == "partly_exposed":
return ["partly nude, body exposed"]
if state == "same_outfit":
return ["keeps the teaser outfit on with the body contact readable"]
if state == "partially_removed":
return ["teaser outfit is pushed aside and partly removed where needed, leaving body contact unobstructed"]
if state == "custom":
return _normalize_characteristic_values(custom_clothing, None, allow_free_text=True)
return []
return pair_options.character_hardcore_clothing_values(state, custom_clothing)
def build_insta_of_options_json(
@@ -6698,131 +6534,39 @@ def build_insta_of_options_json(
hardcore_expression_enabled: bool = True,
hardcore_detail_density: str = "balanced",
) -> str:
hardcore_detail_density = (
hardcore_detail_density if hardcore_detail_density in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced"
)
return json.dumps(
{
"softcore_cast": softcore_cast,
"hardcore_cast": hardcore_cast,
"hardcore_women_count": int(hardcore_women_count),
"hardcore_men_count": int(hardcore_men_count),
"softcore_level": softcore_level,
"hardcore_level": hardcore_level,
"platform_style": platform_style,
"continuity": continuity,
"hardcore_clothing_continuity": hardcore_clothing_continuity,
"softcore_camera_mode": softcore_camera_mode,
"hardcore_camera_mode": hardcore_camera_mode,
"camera_detail": camera_detail,
"softcore_expression_enabled": not _is_false(softcore_expression_enabled),
"hardcore_expression_enabled": not _is_false(hardcore_expression_enabled),
"softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45),
"hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85),
"hardcore_detail_density": hardcore_detail_density,
},
ensure_ascii=True,
sort_keys=True,
return pair_options.build_insta_of_options_json(
softcore_cast=softcore_cast,
hardcore_cast=hardcore_cast,
hardcore_women_count=hardcore_women_count,
hardcore_men_count=hardcore_men_count,
softcore_level=softcore_level,
hardcore_level=hardcore_level,
platform_style=platform_style,
continuity=continuity,
hardcore_clothing_continuity=hardcore_clothing_continuity,
softcore_camera_mode=softcore_camera_mode,
hardcore_camera_mode=hardcore_camera_mode,
camera_detail=camera_detail,
softcore_expression_intensity=softcore_expression_intensity,
hardcore_expression_intensity=hardcore_expression_intensity,
softcore_expression_enabled=softcore_expression_enabled,
hardcore_expression_enabled=hardcore_expression_enabled,
hardcore_detail_density=hardcore_detail_density,
hardcore_detail_density_choices=HARDCORE_DETAIL_DENSITY_CHOICES,
)
def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[str, Any]:
defaults = {
"softcore_cast": "solo",
"hardcore_cast": "use_counts",
"hardcore_women_count": 1,
"hardcore_men_count": 1,
"softcore_level": "lingerie_tease",
"hardcore_level": "hardcore",
"platform_style": "hybrid",
"continuity": "same_creator_same_room",
"hardcore_clothing_continuity": "partially_removed",
"softcore_camera_mode": "handheld_selfie",
"hardcore_camera_mode": "from_camera_config",
"camera_detail": "from_camera_config",
"softcore_expression_enabled": True,
"hardcore_expression_enabled": True,
"softcore_expression_intensity": 0.45,
"hardcore_expression_intensity": 0.85,
"hardcore_detail_density": "balanced",
}
if not options_json:
return defaults
if isinstance(options_json, dict):
raw = options_json
else:
try:
raw = json.loads(str(options_json))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid Insta/OF options JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("Insta/OF options must be a JSON object")
parsed = {**defaults, **raw}
parsed["softcore_cast"] = parsed["softcore_cast"] if parsed["softcore_cast"] in ("solo", "same_as_hardcore") else defaults["softcore_cast"]
parsed["hardcore_cast"] = parsed["hardcore_cast"] if parsed["hardcore_cast"] in ("use_counts", "couple", "threesome", "group") else defaults["hardcore_cast"]
parsed["softcore_level"] = parsed["softcore_level"] if parsed["softcore_level"] in INSTA_OF_SOFT_LEVELS else defaults["softcore_level"]
parsed["hardcore_level"] = parsed["hardcore_level"] if parsed["hardcore_level"] in INSTA_OF_HARDCORE_LEVELS else defaults["hardcore_level"]
parsed["platform_style"] = parsed["platform_style"] if parsed["platform_style"] in INSTA_OF_PLATFORM_STYLES else defaults["platform_style"]
parsed["continuity"] = parsed["continuity"] if parsed["continuity"] in ("same_creator_same_room", "same_creator_new_scene") else defaults["continuity"]
parsed["hardcore_clothing_continuity"] = (
parsed["hardcore_clothing_continuity"]
if parsed["hardcore_clothing_continuity"] in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
else defaults["hardcore_clothing_continuity"]
return pair_options.parse_insta_of_options(
options_json,
camera_mode_choices=CAMERA_MODE_PROMPTS,
camera_detail_choices=CAMERA_DETAIL_CHOICES,
hardcore_detail_density_choices=HARDCORE_DETAIL_DENSITY_CHOICES,
)
parsed["softcore_camera_mode"] = (
parsed["softcore_camera_mode"]
if parsed["softcore_camera_mode"] in CAMERA_MODE_PROMPTS or parsed["softcore_camera_mode"] == "from_camera_config"
else defaults["softcore_camera_mode"]
)
if (
parsed["hardcore_camera_mode"] not in CAMERA_MODE_PROMPTS
and parsed["hardcore_camera_mode"] not in ("from_camera_config", "same_as_softcore")
):
parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"]
parsed["camera_detail"] = (
parsed["camera_detail"]
if parsed["camera_detail"] in CAMERA_DETAIL_CHOICES or parsed["camera_detail"] == "from_camera_config"
else defaults["camera_detail"]
)
parsed["softcore_expression_enabled"] = not _is_false(parsed.get("softcore_expression_enabled", True))
parsed["hardcore_expression_enabled"] = not _is_false(parsed.get("hardcore_expression_enabled", True))
parsed["softcore_expression_intensity"] = _clamped_float(
parsed.get("softcore_expression_intensity"),
defaults["softcore_expression_intensity"],
)
parsed["hardcore_expression_intensity"] = _clamped_float(
parsed.get("hardcore_expression_intensity"),
defaults["hardcore_expression_intensity"],
)
parsed["hardcore_detail_density"] = (
parsed["hardcore_detail_density"]
if parsed.get("hardcore_detail_density") in HARDCORE_DETAIL_DENSITY_CHOICES
else defaults["hardcore_detail_density"]
)
for key in ("hardcore_women_count", "hardcore_men_count"):
try:
parsed[key] = max(0, min(12, int(parsed[key])))
except (TypeError, ValueError):
parsed[key] = defaults[key]
return parsed
def _insta_of_hardcore_counts(options: dict[str, Any]) -> tuple[int, int]:
policy = str(options.get("hardcore_cast", "use_counts"))
if policy == "couple":
women_count, men_count = 1, 1
elif policy == "threesome":
women_count, men_count = 2, 1
elif policy == "group":
women_count, men_count = 3, 2
else:
women_count = int(options.get("hardcore_women_count") or 0)
men_count = int(options.get("hardcore_men_count") or 0)
women_count = max(1, min(12, women_count))
men_count = max(0, min(12, men_count))
if women_count + men_count < 2:
men_count = 1
return women_count, men_count
return pair_options.hardcore_counts(options)
def _insta_of_descriptor(row: dict[str, Any]) -> str:
@@ -6888,39 +6632,23 @@ def _insta_of_prompt_cast_descriptors(text: str) -> str:
return str(text or "").replace("Woman A / primary creator:", "Woman A:")
SOFTCORE_CAST_POSES = [
"standing together for a mirror selfie with relaxed close body language",
"posing shoulder-to-shoulder in a creator-shot group teaser",
"leaning together in a polished subscriber preview",
"sitting close together with relaxed hands and styled outfit visibility",
"arranged around Woman A in a flirtatious creator-teaser pose",
"posing together as a coordinated adult creator set",
"standing near the phone tripod with relaxed teasing body language",
"framed together in a softcore cast reveal",
]
SOFTCORE_CAST_POSES = pair_options.SOFTCORE_CAST_POSES
def _insta_of_softcore_category(level: str) -> tuple[str, str]:
subcategory = INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL.get(
level,
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL["lingerie_tease"],
)
category, _subcategory = subcategory.split(" / ", 1)
return category, subcategory
return pair_options.softcore_category(level)
def _insta_of_softcore_outfit(rng: random.Random, level: str) -> str:
pool = INSTA_OF_SOFTCORE_OUTFITS.get(level, INSTA_OF_SOFTCORE_OUTFITS["lingerie_tease"])
return g.choose(rng, pool)
return g.choose(rng, pair_options.softcore_outfit_pool(level))
def _insta_of_softcore_item_prompt_label(level: str) -> str:
return "Body exposure" if level == "explicit_nude" else "Outfit"
return pair_options.softcore_item_prompt_label(level)
def _insta_of_softcore_pose(rng: random.Random, level: str) -> str:
pool = INSTA_OF_SOFTCORE_POSES.get(level, INSTA_OF_SOFTCORE_POSES["lingerie_tease"])
return g.choose(rng, pool)
return g.choose(rng, pair_options.softcore_pose_pool(level))
def _insta_of_partner_styling(
+56
View File
@@ -627,6 +627,61 @@ def _insta_options(**overrides: Any) -> str:
return _json(data)
def smoke_pair_options_policy() -> None:
_expect(
pb.INSTA_OF_SOFTCORE_OUTFITS is pb.pair_options.INSTA_OF_SOFTCORE_OUTFITS,
"prompt_builder should delegate Insta/OF softcore outfit policy to pair_options",
)
options = json.loads(
pb.build_insta_of_options_json(
softcore_expression_enabled="false",
hardcore_expression_enabled="0",
softcore_expression_intensity=1.4,
hardcore_expression_intensity=-0.4,
hardcore_detail_density="invalid",
)
)
_expect(options["softcore_expression_enabled"] is False, "softcore expression enabled should normalize false strings")
_expect(options["hardcore_expression_enabled"] is False, "hardcore expression enabled should normalize false strings")
_expect(options["softcore_expression_intensity"] == 1.0, "softcore expression intensity should clamp high values")
_expect(options["hardcore_expression_intensity"] == 0.0, "hardcore expression intensity should clamp low values")
_expect(options["hardcore_detail_density"] == "balanced", "invalid hardcore detail density should fallback")
parsed = pb._parse_insta_of_options(
{
"softcore_cast": "bad",
"hardcore_cast": "bad",
"softcore_camera_mode": "bad",
"hardcore_camera_mode": "bad",
"camera_detail": "bad",
"hardcore_detail_density": "bad",
"hardcore_women_count": "20",
"hardcore_men_count": "-3",
}
)
_expect(parsed["softcore_cast"] == "solo", "invalid softcore cast should fallback")
_expect(parsed["hardcore_cast"] == "use_counts", "invalid hardcore cast should fallback")
_expect(parsed["softcore_camera_mode"] == "handheld_selfie", "invalid softcore camera should fallback")
_expect(parsed["hardcore_camera_mode"] == "from_camera_config", "invalid hardcore camera should fallback")
_expect(parsed["camera_detail"] == "from_camera_config", "invalid camera detail should fallback")
_expect(parsed["hardcore_detail_density"] == "balanced", "invalid hardcore density should fallback on parse")
_expect(parsed["hardcore_women_count"] == 12, "women count should clamp to max")
_expect(parsed["hardcore_men_count"] == 0, "men count should clamp to min")
_expect(pb.character_softcore_outfit_values("partner_man"), "partner man softcore outfit pool should not be empty")
_expect(
pb.character_softcore_outfit_values("custom", "one; two\nthree") == ["one", "two", "three"],
"custom softcore outfits should split stable free-text lists",
)
_expect("fully nude" in pb.character_hardcore_clothing_values("fully_nude"), "fully nude clothing state should be exposed")
_expect(
pb.character_hardcore_clothing_values("custom", "bare; outfit pushed aside") == ["bare", "outfit pushed aside"],
"custom hardcore clothing should split stable free-text lists",
)
_expect(pb._insta_of_hardcore_counts({"hardcore_cast": "threesome"}) == (2, 1), "threesome count policy changed")
_expect(pb._insta_of_softcore_category("social_tease") == ("Casual clothes", "Casual clothes / Smart casual"), "softcore category mapping changed")
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")
@@ -2314,6 +2369,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("category_library_route", smoke_category_library_route),
("hardcore_category_routes", smoke_hardcore_category_routes),
("krea_close_foreplay_route", smoke_krea_close_foreplay_route),
("pair_options_policy", smoke_pair_options_policy),
("insta_pair_same_cast", smoke_insta_pair),
("krea_pair_clothing_state", smoke_krea_pair_clothing_state),
("insta_pair_pov_man", smoke_insta_pair_pov),