Extract Insta option policy
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
@@ -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(
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user