Files

8886 lines
357 KiB
Python

from __future__ import annotations
import json
import math
import random
import re
from pathlib import Path
from string import Formatter
from typing import Any, Callable
try:
from . import generate_prompt_batches as g
from .prompt_hygiene import (
sanitize_caption_text,
sanitize_negative_text,
sanitize_prompt_text,
)
except ImportError: # Allows local smoke tests with `python -c`.
import generate_prompt_batches as g
from prompt_hygiene import (
sanitize_caption_text,
sanitize_negative_text,
sanitize_prompt_text,
)
ROOT_DIR = Path(__file__).resolve().parent
CATEGORY_DIR = ROOT_DIR / "categories"
PROFILE_DIR = ROOT_DIR / "profiles"
BUILTIN_CATEGORIES = [
"auto_weighted",
"auto_full",
"woman",
"man",
"couple",
"group_or_layout",
"custom_random",
]
RANDOM_SUBCATEGORY = "random"
SEED_AXIS_SALTS = {
"category": 31,
"subcategory": 37,
"content": 41,
"person": 43,
"scene": 47,
"pose": 53,
"role": 57,
"expression": 59,
"composition": 61,
}
SEED_AXIS_ALIASES = {
"category": ("category_seed", "category"),
"subcategory": ("subcategory_seed", "subcategory"),
"content": ("content_seed", "item_seed", "outfit_seed", "sexual_pose_seed", "content"),
"person": ("person_seed", "appearance_seed", "cast_seed", "person"),
"scene": ("scene_seed", "scene"),
"pose": ("pose_seed", "sexual_pose_seed", "pose"),
"role": ("role_seed", "role", "pose_seed", "sexual_pose_seed"),
"expression": ("expression_seed", "face_seed", "expression"),
"composition": ("composition_seed", "camera_seed", "composition"),
}
SEED_LOCK_AXES = (
"category",
"subcategory",
"content",
"person",
"scene",
"pose",
"role",
"expression",
"composition",
)
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
ETHNICITY_FILTER_CHOICES = [
"any",
"european",
"mediterranean_mena",
"latina",
"east_asian",
"southeast_asian",
"south_asian",
"black_african",
"indigenous",
"mixed",
"asian",
"white_asian",
"western_european",
"french_european",
"germanic_european",
"nordic_european",
"celtic_european",
"slavic_european",
"baltic_european",
"alpine_european",
"balkan_european",
"greek_mediterranean",
"italian_mediterranean",
"iberian_mediterranean",
]
ETHNICITY_LIST_KEYS = tuple(choice for choice in ETHNICITY_FILTER_CHOICES if choice != "any")
ETHNICITY_BASE_LIST_KEYS = (
"european",
"mediterranean_mena",
"latina",
"east_asian",
"southeast_asian",
"south_asian",
"black_african",
"indigenous",
"mixed",
)
EUROPEAN_REGIONAL_LIST_KEYS = (
"western_european",
"french_european",
"germanic_european",
"nordic_european",
"celtic_european",
"slavic_european",
"baltic_european",
"alpine_european",
"balkan_european",
)
MEDITERRANEAN_REGIONAL_LIST_KEYS = (
"greek_mediterranean",
"italian_mediterranean",
"iberian_mediterranean",
)
CHARACTER_LABEL_CHOICES = [
"auto_chain",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
]
CHARACTER_AGE_CHOICES = (
["random", "manual"]
+ [f"{age}-year-old adult" for age in range(21, 86)]
+ [
"late 20s adult",
"early 30s adult",
"mid 30s adult",
"late 30s adult",
"early 40s adult",
"mid 40s adult",
"late 40s adult",
"early 50s adult",
"mid 50s adult",
"late 50s adult",
"early 60s adult",
"mid 60s adult",
"late 60s adult",
"early 70s adult",
"mid 70s adult",
"late 70s adult",
"early 80s adult",
]
)
CHARACTER_BODY_CHOICES = [
"random",
"manual",
"slim",
"petite adult",
"toned",
"athletic",
"average",
"curvy",
"soft curvy",
"curvy athletic",
"hourglass",
"slim busty",
"busty",
"busty curvy",
"voluptuous",
"plus-size",
"heavyset",
"fat",
"stocky",
"broad",
"muscular",
]
CHARACTER_WOMAN_BODY_CHOICES = [
"random",
"manual",
"slim",
"petite adult",
"toned",
"athletic",
"average",
"curvy",
"soft curvy",
"curvy athletic",
"hourglass",
"slim busty",
"busty",
"busty curvy",
"voluptuous",
"plus-size",
"heavyset",
"fat",
]
CHARACTER_MAN_BODY_CHOICES = [
"random",
"manual",
"slim",
"lean",
"lean athletic",
"toned",
"average",
"athletic",
"muscular",
"broad",
"broad-shouldered",
"stocky",
"heavyset",
"fat",
]
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
CHARACTER_HAIR_COLOR_CHOICES = [
"random",
"black",
"brown",
"dark_brown",
"chestnut",
"auburn",
"copper",
"red",
"blonde",
"platinum_blonde",
"ash_blonde",
"honey_blonde",
"strawberry_blonde",
"dark_blonde",
"silver_gray",
"white",
]
CHARACTER_HAIR_LENGTH_CHOICES = [
"random",
"very_short",
"short",
"bob_lob",
"shoulder_length",
"medium",
"long",
"very_long",
"updo",
]
CHARACTER_HAIR_STYLE_CHOICES = [
"random",
"straight",
"waves",
"loose_waves",
"curls",
"tight_curls",
"pixie_cut",
"bob",
"lob",
"shag",
"ponytail",
"braid",
"braids",
"bun",
"messy_bun",
"locs",
"twists",
"afro",
"natural_curls",
"wet_hair",
"slicked_back",
]
CHARACTER_EYE_COLOR_CHOICES = [
"random",
"blue",
"pale_blue",
"ice_blue",
"blue_gray",
"green",
"emerald_green",
"hazel",
"light_hazel",
"green_hazel",
"amber",
"amber_brown",
"honey_brown",
"brown",
"deep_brown",
"dark_brown",
"dark",
"gray",
"gray_brown",
]
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
HARDCORE_POSITION_FAMILY_CHOICES = [
"any",
"penetrative",
"foreplay",
"interaction",
"manual",
"oral",
"outercourse",
"anal",
"climax",
"threesome",
"group",
]
HARDCORE_POSITION_FOCUS_CHOICES = [
"keep_pool",
"penetration_only",
"foreplay_only",
"interaction_only",
"manual_only",
"oral_only",
"outercourse_only",
"anal_only",
"climax_only",
"threesome_only",
"group_only",
]
HARDCORE_POSITION_KEY_CHOICES = [
"missionary",
"cowgirl",
"reverse_cowgirl",
"doggy",
"bent_over",
"face_down_ass_up",
"standing",
"side_lying",
"edge_supported",
"kneeling",
"lotus_lap",
"face_sitting",
"sixty_nine",
"reclining_oral",
"straddled_oral",
"spread_leg_oral",
"chair_oral",
"kissing",
"caressing",
"breast_touch",
"face_touch",
"undressing",
"body_worship",
"nipple_play",
"ass_grab",
"thigh_kissing",
"hair_holding",
"wrist_pinning",
"dirty_talk",
"position_transition",
"guided_positioning",
"camera_showing",
"watching",
"aftercare",
"cleanup",
"fingering",
"clit_rubbing",
"mutual_masturbation",
"boobjob",
"testicle_sucking",
"penis_licking",
"handjob",
"footjob",
"open_thighs",
"front_back",
]
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"any": [
"penetrative_sex",
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"manual_stimulation",
"oral_sex",
"outercourse_sex",
"anal_double_penetration",
"threesomes",
"group_coordination",
"group_sex_orgy",
"cumshot_climax",
"aftercare_cleanup",
],
"penetrative": ["penetrative_sex"],
"foreplay": ["foreplay_teasing"],
"interaction": [
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"group_coordination",
"aftercare_cleanup",
],
"manual": ["manual_stimulation"],
"oral": ["oral_sex"],
"outercourse": ["outercourse_sex", "manual_stimulation"],
"anal": ["anal_double_penetration"],
"climax": ["cumshot_climax"],
"threesome": ["threesomes"],
"group": ["group_sex_orgy"],
}
HARDCORE_POSITION_KEY_MATCHES = {
"missionary": ("missionary", "above her", "under her"),
"cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
"reverse_cowgirl": ("reverse cowgirl", "facing away"),
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
"bent_over": ("bent-over", "bent over", "hips raised"),
"face_down_ass_up": ("face-down", "ass-up"),
"standing": ("standing", "stands", "braced standing"),
"side_lying": ("side-lying", "side lying", "spooning", "on the side", "on her side"),
"edge_supported": ("edge-of-bed", "edge of bed", "bed edge", "raised edge", "edge-supported"),
"kneeling": ("kneeling", "kneels", "kneeling center"),
"lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
"face_sitting": ("face-sitting", "face sitting"),
"sixty_nine": ("sixty-nine", "69"),
"reclining_oral": ("reclining cunnilingus",),
"straddled_oral": ("straddled oral",),
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
"chair_oral": ("chair oral",),
"kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
"caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
"body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
"nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
"ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
"thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
"hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
"wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
"dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
"position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
"guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
"camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
"watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
"aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
"cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
"fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
"clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
"mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
"handjob": ("handjob", "hand job", "stroking the penis", "hand stroking", "manual stimulation"),
"footjob": ("footjob", "soles", "toes curled", "feet stroking"),
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
}
HARDCORE_POSITION_AXIS_KEYS = {
"position",
"body_position",
"body_arrangement",
"arrangement",
"tease_act",
"touch_detail",
"manual_act",
"manual_detail",
"worship_act",
"transition_act",
"control_act",
"performance_act",
"coordination_act",
"aftercare_act",
"cleanup_detail",
}
CAMERA_ORBIT_FRAMING_CHOICES = [
"from_zoom",
"wide",
"medium",
"full_body",
"three_quarter",
"close_up",
"extreme_close_up",
]
CAMERA_ORBIT_FOCUS_CHOICES = [
"auto",
"face",
"torso",
"hips",
"full_body",
"action",
"contact_points",
"environment",
]
GENERIC_POSITIVE_SUFFIX = (
"Use crisp clean comic linework, detailed hatching, soft blended shading, "
"pastel skin tones, muted blues and pinks, warm sensual lighting, and tactile textured paper."
)
SINGLE_TEMPLATE = (
"A {subject}: {style}, {age}, {body_phrase}, {skin}, {hair}, {eyes}. "
"{item_label}: {item}. Scene: {scene}. Pose: {pose}. Facial expression: {expression}. "
"Composition: {composition_prompt}. {positive_suffix} Avoid: {negative_prompt}."
)
COUPLE_TEMPLATE = (
"{subject_phrase}: {style}. Ages: {age}. Body types: {body}. {item_label}: {item}. "
"Scene: {scene}. Pose: {pose}. Facial expressions: {expression}. "
"Composition: {composition_prompt}. {positive_suffix} Avoid: {negative_prompt}."
)
GROUP_TEMPLATE = (
"{subject_phrase}: {style}, ages {age}, diverse adult body types. {item_label}: {item}. "
"Scene: {scene}. Facial expressions: {expression}. Composition: {composition_prompt}. "
"{positive_suffix} Avoid: {negative_prompt}."
)
LAYOUT_TEMPLATE = (
"{item}: {style}, adults only, clean designed composition. Scene: {scene}. "
"Facial expression: {expression}. Composition: {composition}. {positive_suffix} "
"Avoid: {negative_prompt}. Use no readable text unless the layout naturally needs small decorative placeholder marks."
)
CAMERA_MODE_PROMPTS = {
"disabled": "",
"standard": "",
"handheld_selfie": (
"Camera mode: handheld smartphone selfie, close arm-length framing, visible creator-shot perspective, "
"slight wide-angle intimacy, direct eye contact, natural phone-camera composition."
),
"mirror_selfie": (
"Camera mode: mirror selfie with the phone visible in one hand, reflective framing, creator looking at the screen, "
"body and environment visible through the mirror."
),
"phone_tripod": (
"Camera mode: phone on tripod or ring-light stand, creator-facing social-video framing, stable vertical composition, "
"hands-free self-recorded setup."
),
"creator_pov": (
"Camera mode: creator-held POV, intimate subscriber-view angle, the creator controls the camera, close foreground body framing."
),
"bed_selfie": (
"Camera mode: bed selfie shot from a phone held above or beside the body, intimate close framing, sheets visible around the subject."
),
"bathroom_mirror": (
"Camera mode: bathroom mirror selfie, phone visible, tiled private room, close vertical framing, candid creator-shot energy."
),
"phone_flash": (
"Camera mode: direct phone-flash selfie, crisp flash highlights, candid night-post feeling, hard-edged smartphone shadows."
),
"action_cam": (
"Camera mode: body-mounted or handheld action-camera intimacy, very close wide-angle perspective, dynamic creator-shot framing."
),
}
CAMERA_COMPACT_LABELS = {
"disabled": "",
"standard": "",
"handheld_selfie": "handheld smartphone selfie",
"mirror_selfie": "mirror selfie",
"phone_tripod": "phone tripod / ring-light setup",
"creator_pov": "creator-held POV",
"bed_selfie": "bed selfie",
"bathroom_mirror": "bathroom mirror selfie",
"phone_flash": "phone-flash selfie",
"action_cam": "handheld action-camera view",
"full_body": "full body",
"three_quarter": "three-quarter body",
"waist_up": "waist-up",
"close_up": "close-up",
"extreme_close_up": "extreme close-up",
"eye_level": "eye-level",
"high_angle": "high-angle",
"low_angle": "low-angle",
"overhead": "overhead",
"side_profile": "side-profile",
"rear_view": "rear-view",
"mirror_reflection": "mirror reflection",
"smartphone_wide": "smartphone wide-angle",
"ultra_wide": "ultra-wide",
"portrait_lens": "phone portrait lens",
"telephoto": "telephoto-style",
"macro_detail": "macro detail",
"arm_length": "arm-length",
"near_body": "near-body",
"bedside": "bedside phone",
"room_corner": "room-corner phone",
"vertical_story": "vertical 9:16",
"square_feed": "square feed",
"horizontal": "horizontal",
"phone_visible": "phone visible",
"phone_hidden": "phone hidden",
"screen_reflection": "screen reflection",
"ring_light_visible": "ring light visible",
}
CAMERA_SHOT_PROMPTS = {
"auto": "",
"full_body": "Shot size: full body visible, head-to-toe framing, no important body parts cropped out.",
"three_quarter": "Shot size: three-quarter body framing, face, torso, hips, and thighs clearly visible.",
"waist_up": "Shot size: waist-up creator framing with face and upper body as the focus.",
"close_up": "Shot size: close-up framing with face, expression, hands, and body contact emphasized.",
"extreme_close_up": "Shot size: extreme close-up detail shot, tightly framed and intimate.",
}
CAMERA_ANGLE_PROMPTS = {
"auto": "",
"eye_level": "Angle: eye-level camera angle with direct creator eye contact.",
"high_angle": "Angle: high-angle selfie looking down toward the body.",
"low_angle": "Angle: low-angle phone camera looking upward from near the body.",
"overhead": "Angle: overhead phone shot looking down at the full pose.",
"side_profile": "Angle: side-profile camera view emphasizing body silhouette and contact points.",
"rear_view": "Angle: rear-view camera framing with the body turned away from the lens.",
"mirror_reflection": "Angle: mirror-reflection composition with the phone and reflected body placement readable.",
}
CAMERA_LENS_PROMPTS = {
"auto": "",
"smartphone_wide": "Lens: smartphone wide-angle lens with slight edge distortion and close personal scale.",
"ultra_wide": "Lens: ultra-wide phone lens, exaggerated near-camera perspective, environmental context visible.",
"portrait_lens": "Lens: phone portrait mode, shallow depth of field, crisp subject separation.",
"telephoto": "Lens: compressed telephoto-style framing, flatter proportions, less distortion.",
"macro_detail": "Lens: macro-detail phone shot focused on texture, skin, fabric, and contact detail.",
}
CAMERA_DISTANCE_PROMPTS = {
"auto": "",
"arm_length": "Camera distance: arm-length selfie distance, close enough to feel handheld.",
"near_body": "Camera distance: near-body camera placement with intimate foreground framing.",
"bedside": "Camera distance: phone placed beside the body on the bed or floor.",
"room_corner": "Camera distance: phone set across the room, self-recorded but wider and more observational.",
}
CAMERA_ORIENTATION_PROMPTS = {
"auto": "",
"vertical_story": "Orientation: vertical 9:16 story/reel framing.",
"square_feed": "Orientation: square social-feed crop.",
"horizontal": "Orientation: horizontal phone-video crop.",
}
CAMERA_PHONE_PROMPTS = {
"auto": "",
"phone_visible": "Phone visibility: phone visible in hand or mirror, clearly creator-shot.",
"phone_hidden": "Phone visibility: phone is implied but not visible, preserving the selfie/creator-shot perspective.",
"screen_reflection": "Phone visibility: screen glow or reflection visible in the scene.",
"ring_light_visible": "Phone visibility: ring light or tripod visible enough to read as self-recorded content.",
}
CAMERA_PRIORITY_PROMPTS = {
"soft_hint": "Camera priority: treat the camera notes as style guidance.",
"strong": "Camera priority: strongly preserve the selected camera, lens, angle, crop, and phone-shot perspective.",
"locked": "Camera priority: locked camera constraint; do not replace this with a studio, third-person, cinematic, or unrelated camera view.",
}
_EXTENSIONS_APPLIED = False
class SafeFormatDict(dict):
def __missing__(self, key: str) -> str:
return "{" + key + "}"
def _json_files() -> list[Path]:
if not CATEGORY_DIR.exists():
return []
return sorted(path for path in CATEGORY_DIR.glob("*.json") if path.is_file())
def _read_json(path: Path) -> dict[str, Any]:
try:
data = json.loads(path.read_text(encoding="utf-8"))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
if not isinstance(data, dict):
raise ValueError(f"{path} must contain a JSON object")
return data
def _slug(value: str) -> str:
return g.slugify(value) or "custom"
def _list_from(value: Any) -> list[Any]:
if value is None:
return []
if isinstance(value, list):
return value
return [value]
def _is_false(value: Any) -> bool:
if isinstance(value, bool):
return value is False
if isinstance(value, str):
return value.strip().lower() in ("false", "0", "no", "off")
return False
def _unique_extend(target: list[Any], additions: list[Any]) -> None:
seen = set()
for item in target:
try:
seen.add(json.dumps(item, sort_keys=True))
except TypeError:
seen.add(repr(item))
for item in additions:
try:
marker = json.dumps(item, sort_keys=True)
except TypeError:
marker = repr(item)
if marker not in seen:
target.append(item)
seen.add(marker)
def _pair_from(value: Any) -> tuple[str, str]:
if isinstance(value, dict):
text = str(
value.get("prompt")
or value.get("description")
or value.get("text")
or value.get("name")
or ""
).strip()
slug = str(value.get("slug") or _slug(str(value.get("name") or text))).strip()
if not text:
raise ValueError(f"Pair extension is missing prompt text: {value!r}")
return slug, text
if isinstance(value, (list, tuple)) and len(value) == 2:
return str(value[0]), str(value[1])
text = str(value).strip()
if not text:
raise ValueError("Pair extension cannot be empty")
return _slug(text), text
def _weighted_choice(rng: random.Random, items: list[Any]) -> Any:
if not items:
raise ValueError("Cannot choose from an empty list")
weights: list[float] = []
for item in items:
weight = item.get("weight", 1.0) if isinstance(item, dict) else 1.0
try:
weights.append(max(0.0, float(weight)))
except (TypeError, ValueError):
weights.append(1.0)
total = sum(weights)
if total <= 0:
return items[rng.randrange(len(items))]
pick = rng.random() * total
running = 0.0
for item, weight in zip(items, weights):
running += weight
if pick <= running:
return item
return items[-1]
def _entry_text(item: Any) -> str:
if isinstance(item, dict):
return str(
item.get("template")
or item.get("prompt")
or item.get("text")
or item.get("description")
or item.get("name")
or ""
).strip()
return str(item).strip()
def _item_text(item: Any) -> str:
return _entry_text(item)
def _item_name(item: Any) -> str:
if isinstance(item, dict):
return str(item.get("name") or _item_text(item)).strip()
return _item_text(item)
def _template_list(category: dict[str, Any], subcategory: dict[str, Any], item: Any, key: str) -> list[Any]:
if isinstance(item, dict) and key in item:
return _list_from(item[key])
if key in subcategory:
return _list_from(subcategory[key])
if key in category:
return _list_from(category[key])
return []
def _constraint_int(entry: dict[str, Any], key: str) -> int | None:
if key not in entry:
return None
try:
return int(entry[key])
except (TypeError, ValueError):
return None
def _cast_requirement_matches(requirement: str, women_count: int, men_count: int) -> bool:
total = women_count + men_count
requirement = requirement.strip().lower()
if requirement in ("", "any"):
return True
if requirement == "women_only":
return women_count > 0 and men_count == 0
if requirement == "men_only":
return men_count > 0 and women_count == 0
if requirement == "mixed":
return women_count > 0 and men_count > 0
if requirement == "has_women":
return women_count > 0
if requirement == "has_men":
return men_count > 0
if requirement == "solo":
return total == 1
if requirement == "couple":
return total == 2
if requirement == "threesome":
return total == 3
if requirement == "group":
return total >= 4
return True
def _is_toy_assisted_double_couple_text(text: str) -> bool:
text = text.lower()
if "toy" not in text:
return False
return any(
token in text
for token in (
"double penetration",
"double-penetration",
"vaginal and anal penetration",
"second penetration point",
"second point of contact",
"second contact",
)
)
def _heuristic_cast_compatible(text: str, women_count: int, men_count: int) -> bool:
text = text.lower()
if not text:
return True
total = women_count + men_count
if total == 2 and women_count == 1 and men_count == 1:
if "{double_act}" in text:
return False
if _is_toy_assisted_double_couple_text(text):
return False
if total == 1:
solo_blocked_terms = (
"partner",
"partners",
"two bodies",
"three bodies",
"bodies still pressed",
"bodies pressed",
"bodies tangled",
"wet bodies",
"chests heaving together",
"straddling a partner",
"shared climax",
"between two",
"from both sides",
"front-and-back",
"body contact",
)
if any(term in text for term in solo_blocked_terms):
return False
solo_toy_terms = ("toy", "dildo", "finger", "fingers", "self")
if "penetration" in text and not any(term in text for term in solo_toy_terms):
return False
if total < 3 and "threesome" in text:
return False
if total != 3 and ("centered threesome" in text or "three-way" in text):
return False
if total < 3 and ("three bodies" in text or "center partner" in text or "center body" in text):
return False
if total < 4 and ("orgy" in text or "group sex" in text or "group-sex" in text or "group pile" in text):
return False
if total < 3 and (
"double penetration" in text
or "two partners penetrating" in text
or "front-and-back penetration" in text
or "one penis in pussy and one penis in ass" in text
or "pussy and ass filled" in text
or "vaginal and anal penetration at the same time" in text
or "front-and-back double penetration" in text
or "hardcore double penetration" in text
or "kneeling double penetration" in text
or "standing supported double penetration" in text
or "deep double penetration" in text
or "between two partners" in text
or "from both sides" in text
):
toy_terms = ("strap-on", "strap on", "dildo", "toy", "finger")
if not any(term in text for term in toy_terms):
return False
if men_count == 0:
toy_terms = ("strap-on", "strap on", "dildo", "toy", "finger", "fingers")
penetration_terms = (
"vaginal penetration",
"deep vaginal sex",
"penetrative sex",
"pussy penetration",
"pussy stretched",
"vaginal thrusting",
"full-body penetrative",
"close-contact vaginal",
"penetration clearly visible",
"explicit penetrative contact",
)
if any(term in text for term in penetration_terms) and not any(term in text for term in toy_terms):
return False
male_terms = (
" penis",
"penis ",
"penises",
"cum",
"creampie",
"facial",
"blowjob",
"fellatio",
"deepthroat",
"ejaculation",
"semen",
)
if any(term in text for term in male_terms) and not any(term in text for term in toy_terms):
return False
elif men_count < 2 and "penises" in text:
return False
if women_count == 0:
if "penetrative sex" in text and not any(term in text for term in ("anal", "ass", "male/male", "men")):
return False
female_terms = (
"pussy",
"vaginal",
"vagina",
"cunnilingus",
"clit",
"clitoris",
"breasts",
"breast ",
"nipples",
"nipple",
"underboob",
)
if any(term in text for term in female_terms):
return False
return True
HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS = (
(r"\bstacked bodies on the bed\b", "close body alignment"),
(r"\bstacked bodies with close body alignment\b", "close body alignment"),
(r"\boverhead tangled-body anal frame\b", "overhead rear-entry anal frame"),
(r"\btangled-body\b", "close-body"),
(r"\bthree bodies tangled on the bed\b", "three bodies tangled in close contact"),
(r"\ba triangle of bodies on the mattress\b", "a triangle of bodies in close contact"),
(r"\bbodies tangled on the sheets\b", "bodies tangled in close contact"),
(r"\bwet bodies tangled on sheets\b", "wet bodies tangled in close contact"),
(r"\bbody arched on rumpled sheets\b", "body arched with clear skin contact"),
(r"\bface-down ass-up on the mattress\b", "face-down ass-up position"),
(r"\bsitting on the edge of the bed\b", "sitting on a raised edge"),
(r"\blying at the bed edge with thighs open\b", "lying near a raised edge with thighs open"),
(r"\bedge[- ]of[- ]bed\b", "edge-supported"),
(r"\bbed[- ]edge\b", "raised edge"),
(r"\bedge of (?:the )?bed\b", "raised edge"),
(r"\bbed edge\b", "raised edge"),
(r"\bhands? braced on the bed\b", "hands braced beside the body"),
(r"\bone hand pressing into the mattress\b", "one hand braced beside the body"),
(r"\bone foot planted on the bed\b", "one foot planted for leverage"),
(r"\bfingers gripping sheets and skin\b", "fingers gripping skin"),
(r"\bfingers gripping sheets\b", "fingers gripping skin"),
(r"\bhands gripping sheets\b", "hands gripping skin"),
(r"\bone hand gripping the sheets\b", "one hand gripping skin"),
(r"\brumpled bed sheets\b", "wrinkled body-contact fabric"),
(r"\bwet sheets beneath the bodies\b", "visible wetness beneath the bodies"),
(r"\bsexual fluids on sheets\b", "sexual fluids visible on skin"),
(r"\bcum dripping onto sheets\b", "cum visible on skin"),
(r"\bfluid dripping onto sheets\b", "fluid visible on skin"),
(r"\bsquirting fluid on the sheets\b", "squirting fluid visible on skin"),
(r"\bsoft sheets\b", "soft fabric"),
(r"\bsilk sheets\b", "silk fabric"),
(r"\bsheets\b", "fabric"),
(r"\bmattress\b", "low support surface"),
(r"\ba low support surface\b", "a low body support"),
(r"\ba low mattress\b", "a low body support"),
(r"\ba wide couch\b", "a wide body support"),
(r"\bwide couch\b", "wide body support"),
(r"\bcouch\b", "body support"),
(r"\bsofa\b", "body support"),
(r"\bon the bed\b", "on a body support"),
(r"\bon a bed\b", "on a body support"),
(r"\bbedroom-floor\b", "floor-level"),
(r"\bbedroom floor\b", "floor-level"),
)
def _sanitize_hardcore_environment_anchors(value: Any) -> str:
text = str(value or "")
if not text:
return ""
for pattern, replacement in HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
text = re.sub(r"\s+,", ",", text)
text = re.sub(r",\s*,", ",", text)
text = re.sub(r"\s{2,}", " ", text)
return text.strip()
def _sanitize_hardcore_axis_values(values: dict[str, str]) -> dict[str, str]:
return {key: _sanitize_hardcore_environment_anchors(value) for key, value in values.items()}
def _compatible_entry(entry: Any, women_count: int, men_count: int) -> bool:
if not isinstance(entry, dict):
return _heuristic_cast_compatible(_entry_text(entry), women_count, men_count)
total = women_count + men_count
for key, value in (
("min_women", women_count),
("min_men", men_count),
("min_people", total),
):
minimum = _constraint_int(entry, key)
if minimum is not None and value < minimum:
return False
for key, value in (
("max_women", women_count),
("max_men", men_count),
("max_people", total),
):
maximum = _constraint_int(entry, key)
if maximum is not None and value > maximum:
return False
requirements = _list_from(entry.get("cast", [])) + _list_from(entry.get("requires", []))
if requirements and not all(_cast_requirement_matches(str(req), women_count, men_count) for req in requirements):
return False
if any(key in entry for key in ("subcategories", "item_templates", "item_axes")):
return True
return _heuristic_cast_compatible(_entry_text(entry), women_count, men_count)
def _compatible_entries(entries: list[Any], women_count: int, men_count: int) -> list[Any]:
filtered = [entry for entry in entries if _compatible_entry(entry, women_count, men_count)]
return filtered or entries
def _merged_axes(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> dict[str, list[Any]]:
axes: dict[str, list[Any]] = {}
for source in (category, subcategory, item if isinstance(item, dict) else None):
if not isinstance(source, dict):
continue
raw_axes = source.get("item_axes", {})
if raw_axes is None:
continue
if not isinstance(raw_axes, dict):
raise ValueError("item_axes must be a JSON object")
for key, values in raw_axes.items():
axes[str(key)] = _list_from(values)
return axes
def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]:
position_text = str(position or "").lower()
if not position_text:
return values
def act_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(predicate: Callable[[str], bool]) -> list[Any]:
matches = [value for value in values if predicate(act_text(value))]
return matches or values
penis_terms = ("fellatio", "blowjob", "deepthroat", "penis sucking", "penis in mouth")
cunnilingus_terms = ("cunnilingus", "pussy licking", "tongue on pussy", "oral sex with tongue and fingers", "mouth on genitals")
if "sixty-nine" in position_text:
return filtered(lambda text: "sixty-nine" in text)
if "face-sitting" in position_text:
return filtered(lambda text: "face-sitting" in text or any(term in text for term in cunnilingus_terms))
if "kneeling oral" in position_text:
return filtered(lambda text: any(term in text for term in penis_terms))
if "straddled oral" in position_text or "reclining cunnilingus" in position_text:
return filtered(lambda text: "sixty-nine" not in text and not any(term in text for term in penis_terms))
if "spread-leg oral" in position_text:
return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text)
if any(term in position_text for term in ("standing oral", "kneeling oral", "edge-of-bed oral", "chair oral", "side-lying oral")):
return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text)
return values
def _oral_axis_values_for_context(values: list[Any], position: str, oral_act: str, axis_name: str) -> list[Any]:
axis_name = str(axis_name or "").lower()
if axis_name not in {"body_contact", "hand_detail", "mouth_detail", "saliva_detail", "climax_hint", "visibility"}:
return values
position_text = str(position or "").lower()
act_text = str(oral_act or "").lower()
woman_gives = any(
term in act_text
for term in ("fellatio", "blowjob", "deepthroat", "penis sucking", "penis in mouth")
)
man_gives = any(
term in act_text
for term in ("cunnilingus", "pussy licking", "tongue on pussy")
)
if not (woman_gives or man_gives):
return values
def value_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(terms: tuple[str, ...], excluded_terms: tuple[str, ...] = ()) -> list[Any]:
matches = [
value
for value in values
if any(term in value_text(value) for term in terms)
and not any(term in value_text(value) for term in excluded_terms)
]
return matches or values
if woman_gives:
by_axis = {
"body_contact": ("hips pushed", "fingers tangled", "bodies stacked", "hands on thighs"),
"hand_detail": ("hips", "penis", "head", "hair"),
"mouth_detail": ("lips", "mouth", "deep mouth", "saliva"),
"saliva_detail": ("saliva", "wet lips", "slick wet mouth", "drool", "mouth"),
"climax_hint": ("mouth", "lips", "tongue", "breasts", "belly", "sexual fluids"),
"visibility": ("mouth", "penis", "oral"),
}
excluded = {
"body_contact": ("legs held open", "spread legs", "ass lifted", "chest pressed to thighs"),
"hand_detail": ("spreading thighs", "sheets", "cupping breasts", "pressing into thighs", "holding the ass"),
}
return filtered(by_axis.get(axis_name, ("mouth", "penis")), excluded.get(axis_name, ()))
if man_gives and ("kneeling oral" in position_text or "standing oral" in position_text):
by_axis = {
"body_contact": ("legs held open", "one body kneeling", "chest pressed", "ass lifted", "hands on thighs"),
"hand_detail": ("thigh", "hips", "head", "ass"),
"mouth_detail": ("tongue", "wet lips", "deep mouth", "genitals"),
"saliva_detail": ("saliva", "wet lips", "tongue", "drool"),
"climax_hint": ("sexual fluids", "orgasmic tension"),
"visibility": ("mouth", "pussy", "oral", "genital"),
}
return filtered(by_axis.get(axis_name, ("mouth", "pussy", "tongue")), ("penis", "breasts"))
return values
def _outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]:
position_text = str(position or "").lower()
if not position_text:
return values
def act_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(predicate: Callable[[str], bool]) -> list[Any]:
matches = [value for value in values if predicate(act_text(value))]
return matches or values
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
return filtered(lambda text: any(term in text for term in ("boobjob", "titjob", "breast sex", "breasts")))
if any(term in position_text for term in ("testicle", "balls")):
return filtered(lambda text: any(term in text for term in ("testicle", "balls")))
if "penis-licking" in position_text or "penis licking" in position_text:
return filtered(lambda text: "licking" in text or "tongue" in text)
if "handjob" in position_text or "hand job" in position_text:
return filtered(lambda text: any(term in text for term in ("handjob", "hand job", "hand wrapped", "two-handed")))
if "footjob" in position_text:
return filtered(lambda text: any(term in text for term in ("footjob", "feet", "soles", "toes")))
return values
def _outercourse_axis_values_for_position(values: list[Any], position: str, axis_name: str) -> list[Any]:
position_text = str(position or "").lower()
if not position_text:
return values
axis_name = str(axis_name or "").lower()
if axis_name not in {"contact_detail", "hand_detail", "texture_detail", "visibility", "body_contact"}:
return values
def value_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(terms: tuple[str, ...], excluded_terms: tuple[str, ...] = ()) -> list[Any]:
matches = [
value
for value in values
if any(term in value_text(value) for term in terms)
and not any(term in value_text(value) for term in excluded_terms)
]
return matches or values
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
by_axis = {
"contact_detail": ("compressed", "glans", "breast", "breasts", "soft tissue", "skin visibly"),
"hand_detail": ("breast", "breasts", "fingers"),
"texture_detail": ("compression", "soft flesh", "skin", "flesh", "asymmetry"),
"visibility": ("breast", "breasts", "glans", "shaft"),
"body_contact": ("torso", "body angled", "shoulders", "hips"),
}
excluded_by_axis = {
"contact_detail": ("hand wrapped", "fingers and palm", "soles", "toes", "balls", "tongue"),
"hand_detail": ("base of the penis", "penis shaft", "balls", "thigh", "ankles", "stroking"),
"texture_detail": ("toes", "soles", "tongue"),
"visibility": ("balls", "soles", "toes", "hand"),
"body_contact": ("head tucked", "face directly", "base of the penis"),
}
return filtered(
by_axis.get(axis_name, ("breast", "breasts", "shaft")),
excluded_by_axis.get(axis_name, ()),
)
if any(term in position_text for term in ("testicle", "balls")):
by_axis = {
"contact_detail": ("balls", "lips", "tongue", "wet"),
"hand_detail": ("balls", "base", "thigh"),
"texture_detail": ("wet", "saliva", "skin"),
"visibility": ("balls", "mouth"),
"body_contact": ("torso", "shoulders", "head tucked", "base of the penis", "knees", "thigh"),
}
return filtered(by_axis.get(axis_name, ("balls", "mouth", "tongue")))
if "penis-licking" in position_text or "penis licking" in position_text:
by_axis = {
"contact_detail": ("tongue", "lips", "glans", "shaft", "wet"),
"hand_detail": ("base", "penis", "thigh"),
"texture_detail": ("wet", "saliva", "skin"),
"visibility": ("tongue", "penis"),
"body_contact": ("head low", "face directly", "torso", "pelvis", "base of the penis", "hips", "body angled"),
}
return filtered(by_axis.get(axis_name, ("tongue", "glans", "shaft")))
if "handjob" in position_text or "hand job" in position_text:
by_axis = {
"contact_detail": ("hand", "fingers", "palm", "shaft", "glans"),
"hand_detail": ("hand", "hands", "shaft", "penis"),
"texture_detail": ("fingers", "pressure", "skin", "shaft"),
"visibility": ("hand", "penis", "shaft", "glans"),
"body_contact": ("hips", "knees", "body angle"),
}
return filtered(by_axis.get(axis_name, ("hand", "penis", "shaft")))
if "footjob" in position_text:
by_axis = {
"contact_detail": ("soles", "toes", "shaft"),
"hand_detail": ("ankles", "thighs"),
"texture_detail": ("toes", "soles", "pressure"),
"visibility": ("feet", "soles"),
"body_contact": ("legs", "knees", "body angled"),
}
return filtered(by_axis.get(axis_name, ("feet", "soles", "toes")))
return values
def _compose_item(
rng: random.Random,
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
women_count: int = 1,
men_count: int = 1,
) -> tuple[str, str, dict[str, str]]:
templates = _template_list(category, subcategory, item, "item_templates")
axes = _merged_axes(category, subcategory, item)
if templates and axes:
template = _entry_text(_weighted_choice(rng, _compatible_entries(templates, women_count, men_count)))
fields = [key for _, key, _, _ in Formatter().parse(template) if key]
unique_fields = list(dict.fromkeys(fields))
axis_values: dict[str, str] = {}
subcategory_slug = str(subcategory.get("slug") or "").lower()
if subcategory_slug in ("oral_sex", "outercourse_sex") and "position" in unique_fields and axes.get("position"):
position_values = _compatible_entries(axes["position"], women_count, men_count)
axis_values["position"] = _entry_text(_weighted_choice(rng, position_values))
for name in unique_fields:
if name in axis_values or name not in axes or not axes[name]:
continue
values = _compatible_entries(axes[name], women_count, men_count)
if subcategory_slug == "oral_sex" and name == "oral_act":
values = _oral_acts_for_position(values, axis_values.get("position", ""))
elif subcategory_slug == "oral_sex":
values = _oral_axis_values_for_context(
values,
axis_values.get("position", ""),
axis_values.get("oral_act", ""),
name,
)
if subcategory_slug == "outercourse_sex" and name == "outer_act":
values = _outercourse_acts_for_position(values, axis_values.get("position", ""))
if subcategory_slug == "outercourse_sex":
values = _outercourse_axis_values_for_position(values, axis_values.get("position", ""), name)
axis_values[name] = _entry_text(_weighted_choice(rng, values))
item_text = _format(template, axis_values).strip()
item_name = _item_name(item) or subcategory["name"]
return item_text, item_name, axis_values
return _item_text(item), _item_name(item), {}
def _choose_text(rng: random.Random, items: list[Any]) -> str:
item = _weighted_choice(rng, items)
return _item_text(item)
def _choose_distinct_text(rng: random.Random, items: list[Any], first_text: str) -> str:
first_text = _item_text(first_text).lower()
distinct = [item for item in items if _item_text(item).lower() != first_text]
if not distinct:
return ""
return _choose_text(rng, distinct)
def _choose_pair(rng: random.Random, items: list[Any]) -> tuple[str, str]:
return _pair_from(_weighted_choice(rng, items))
def _normalize_subcategories(category: dict[str, Any]) -> list[dict[str, Any]]:
raw = category.get("subcategories", [])
if isinstance(raw, dict):
raw = [
{"name": name, **(value if isinstance(value, dict) else {"items": value})}
for name, value in raw.items()
]
subcategories: list[dict[str, Any]] = []
for entry in _list_from(raw):
if isinstance(entry, str):
sub = {"name": entry, "items": [entry]}
elif isinstance(entry, dict):
sub = dict(entry)
else:
raise ValueError(f"Subcategory must be an object or string: {entry!r}")
name = str(sub.get("name") or sub.get("slug") or "General").strip()
sub["name"] = name
sub["slug"] = str(sub.get("slug") or _slug(name))
if "items" not in sub and "prompts" in sub:
sub["items"] = sub["prompts"]
if "items" not in sub:
sub["items"] = [name]
subcategories.append(sub)
if not subcategories:
name = str(category.get("name") or "General")
subcategories.append({"name": "General", "slug": "general", "items": [name]})
return subcategories
def _normalize_categories(raw_categories: Any) -> list[dict[str, Any]]:
if isinstance(raw_categories, dict):
iterable = [
{"name": name, **(value if isinstance(value, dict) else {"subcategories": value})}
for name, value in raw_categories.items()
]
else:
iterable = _list_from(raw_categories)
categories: list[dict[str, Any]] = []
for entry in iterable:
if not isinstance(entry, dict):
raise ValueError(f"Category must be an object: {entry!r}")
category = dict(entry)
name = str(category.get("name") or category.get("slug") or "Custom").strip()
category["name"] = name
category["slug"] = str(category.get("slug") or _slug(name))
category["subcategories"] = _normalize_subcategories(category)
categories.append(category)
return categories
def load_category_library() -> list[dict[str, Any]]:
categories: list[dict[str, Any]] = []
for path in _json_files():
data = _read_json(path)
categories.extend(_normalize_categories(data.get("categories", [])))
return categories
def _load_named_pool_library(key: str) -> dict[str, list[Any]]:
pools: dict[str, list[Any]] = {}
for path in _json_files():
data = _read_json(path)
raw_pools = data.get(key, {})
if not raw_pools:
continue
if not isinstance(raw_pools, dict):
raise ValueError(f"{key} in {path} must be an object")
for name, entries in raw_pools.items():
pool_name = str(name).strip()
if not pool_name:
continue
pools.setdefault(pool_name, [])
_unique_extend(pools[pool_name], _list_from(entries))
return pools
def load_scene_pool_library() -> dict[str, list[Any]]:
return _load_named_pool_library("scene_pools")
LOCATION_POOL_PRESETS = {
"custom_only": (),
"all_json_locations": ("*",),
"casual_all": ("casual_",),
"casual_urban": ("casual_urban_scenes",),
"casual_summer": ("casual_summer_scenes",),
"casual_home": ("casual_lounge_scenes",),
"casual_smart": ("casual_smart_scenes",),
"creator_softcore": ("softcore_creator_scenes", "mirror_scenes", "boudoir_bedroom_scenes"),
"mirror_rooms": ("mirror_scenes", "hardcore_mirror_scenes"),
"boudoir_bedroom": ("boudoir_bedroom_scenes", "hardcore_bed_scenes"),
"fetish_studio": ("fetish_studio_scenes",),
"costume_backstage": ("costume_backstage_scenes",),
"hardcore_all": ("hardcore_",),
"hardcore_private": ("hardcore_private_scenes",),
"hardcore_bed": ("hardcore_bed_scenes",),
"hardcore_penetrative": ("hardcore_penetrative_scenes",),
"hardcore_oral": ("hardcore_oral_scenes",),
"hardcore_anal": ("hardcore_anal_scenes",),
"hardcore_threesome": ("hardcore_threesome_scenes",),
"hardcore_group": ("hardcore_group_scenes",),
"hardcore_climax": ("hardcore_climax_scenes",),
}
def location_pool_preset_choices() -> list[str]:
pool_choices = [f"pool:{key}" for key in sorted(load_scene_pool_library())]
return list(LOCATION_POOL_PRESETS) + pool_choices
def load_expression_pool_library() -> dict[str, list[Any]]:
return _load_named_pool_library("expression_pools")
def load_composition_pool_library() -> dict[str, list[Any]]:
return _load_named_pool_library("composition_pools")
COMPOSITION_POOL_PRESETS = {
"custom_only": (),
"all_json_compositions": ("*",),
"casual_all": ("casual_", "streetwear_", "summer_", "cozy_home_", "smart_casual_", "athleisure_"),
"creator_softcore": ("softcore_creator_compositions", "boudoir_body_compositions"),
"hardcore_all": ("hardcore_",),
"hardcore_explicit": ("hardcore_explicit_compositions",),
"no_outfit_check": (),
}
COMPOSITION_INLINE_PRESETS = {
"no_outfit_check": [
"environment-led frame with no outfit-check wording",
"mid-distance scene composition with the room context readable",
"partly occluded candid frame through foreground architecture",
"long perspective frame using repeating background structure",
"waist-up or three-quarter frame without bag, shoes, or footwear emphasis",
],
}
def composition_pool_preset_choices() -> list[str]:
pool_choices = [f"pool:{key}" for key in sorted(load_composition_pool_library())]
return list(COMPOSITION_POOL_PRESETS) + pool_choices
THEMATIC_LOCATION_PRESETS = {
"classical_library": {
"locations": [
{"slug": "classical_large_library", "prompt": "grand classical library hall with towering dark-wood bookshelves, carved columns, rolling ladders, marble floor, warm brass lamps, arched windows, and deep quiet academic atmosphere"},
{"slug": "old_world_reading_room", "prompt": "large old-world reading room with floor-to-ceiling bookshelves, heavy wooden tables, green banker lamps, leather chairs, tall arched windows, and warm amber evening light"},
{"slug": "hidden_library_stacks", "prompt": "quiet library stacks with endless tall bookshelves, narrow aisles, rolling ladders, brass lamps, and hidden sightlines between shelves"},
],
"compositions": [
"narrow aisle frame between towering bookshelves",
"over-the-shoulder view through foreground books",
"warm lamp-lit reading-table composition",
"long vanishing-point frame down repeated library stacks",
"partly hidden frame behind carved columns and shelf edges",
],
},
"semi_public_affair": {
"locations": [
{"slug": "hotel_corridor_affair", "prompt": "upscale hotel corridor with repeating numbered doors, patterned carpet, brass wall lamps, luggage carts, and a secluded corner near a service alcove"},
{"slug": "hotel_service_hall", "prompt": "luxury hotel service corridor with repeating linen carts, beige doors, utility shelves, wall sconces, and a private turn away from the main hallway"},
{"slug": "parking_garage_hidden", "prompt": "empty multi-level parking garage with repeating concrete pillars, parked cars, painted floor lines, low fluorescent light, and shadowed blind spots"},
{"slug": "office_afterhours_affair", "prompt": "empty corporate office after hours with rows of glass partitions, repeating desks, blinds, copier alcove, muted city light, and no visible coworkers"},
{"slug": "library_stacks_secret", "prompt": "classical library stacks with endless tall bookshelves, narrow aisles, rolling ladders, carved columns, warm brass lamps, and hidden sightlines between shelves"},
],
"compositions": [
"partly concealed frame from behind a doorway edge",
"long corridor vanishing-point composition with repeated doors",
"hidden alcove frame with foreground obstruction",
"surveillance-like candid angle from across the empty space",
"tight frame using pillars, shelves, or walls to block side visibility",
],
},
"hotel_corridor": {
"locations": [
{"slug": "upscale_hotel_corridor", "prompt": "upscale hotel corridor with repeating doors, patterned carpet, brass wall lamps, quiet service alcoves, and warm late-night light"},
{"slug": "hotel_service_alcove", "prompt": "hotel service alcove with linen carts, beige utility doors, folded towels, soft wall sconces, and a secluded turn off the main corridor"},
{"slug": "boutique_hotel_stair_landing", "prompt": "boutique hotel stair landing with repeating railings, framed wall panels, low amber lamps, and a quiet corner between floors"},
],
"compositions": [
"long hallway frame with repeated doors receding behind the body",
"corner-alcove composition partly hidden by a wall edge",
"low corridor angle with patterned carpet leading lines",
"over-the-shoulder frame toward a closed hotel-room door",
],
},
"parking_garage": {
"locations": [
{"slug": "empty_parking_garage", "prompt": "empty multi-level parking garage with repeating concrete pillars, parked cars, painted bay lines, low fluorescent light, and deep shadowed corners"},
{"slug": "underground_garage_corner", "prompt": "underground parking garage corner with numbered pillars, glossy concrete floor, parked cars, and blue-green fluorescent light"},
{"slug": "rooftop_parking_deck_night", "prompt": "rooftop parking deck at night with repeated concrete barriers, distant city lights, painted lines, and open wind"},
],
"compositions": [
"pillar-framed composition with repeated concrete columns",
"low angle across painted parking lines",
"hidden corner frame between parked cars",
"wide empty garage frame with strong fluorescent perspective",
],
},
"theater_backstage": {
"locations": [
{"slug": "old_theater_backstage", "prompt": "old theater backstage with repeated velvet curtains, prop racks, costume rails, bulb mirrors, dark wings, and narrow hidden passages"},
{"slug": "cabaret_backstage_wings", "prompt": "cabaret backstage wings with red curtains, costume racks, vanity bulbs, stage ropes, and warm theatrical shadows"},
{"slug": "prop_storage_corridor", "prompt": "theater prop storage corridor with stacked trunks, repeated scenery flats, rolling racks, and dim practical lamps"},
],
"compositions": [
"frame between layered velvet curtains",
"backstage mirror-bulb composition with costume racks behind",
"hidden wing angle looking toward the stage light spill",
"narrow prop-aisle frame with repeated vertical flats",
],
},
"wine_cellar": {
"locations": [
{"slug": "private_wine_cellar", "prompt": "private wine cellar with repeating bottle racks, arched brick walls, narrow aisles, dim amber lamps, and secluded corners between shelves"},
{"slug": "restaurant_wine_storage", "prompt": "restaurant wine storage room with stacked bottle shelves, crate rows, stone floor, soft utility light, and hidden service-door access"},
{"slug": "arched_cellar_corridor", "prompt": "arched cellar corridor with repeated brick niches, wine racks, low golden lamps, and cool shadowed depth"},
],
"compositions": [
"narrow aisle frame between repeated bottle racks",
"arched brick corridor composition with warm lamps",
"foreground bottle-rack occlusion framing the body",
"low cellar angle with shelves receding behind",
],
},
"museum_archive": {
"locations": [
{"slug": "museum_archive_room", "prompt": "museum archive room with repeating storage shelves, labeled boxes, rolling ladders, long work tables, soft overhead lights, and hidden aisles"},
{"slug": "gallery_storage_backroom", "prompt": "gallery storage backroom with stacked frames, rolling racks, crate labels, clean concrete floor, and muted work lights"},
{"slug": "rare_books_archive", "prompt": "rare-books archive with compact shelving, catalog drawers, reading lamps, archival boxes, and narrow private aisles"},
],
"compositions": [
"hidden archive-aisle frame between storage shelves",
"table-edge composition with labeled boxes in the background",
"foreground crate or shelf occlusion",
"long compact-shelving perspective with repeated rows",
],
},
"laundromat_late_night": {
"locations": [
{"slug": "late_night_laundromat", "prompt": "late-night laundromat with repeating washing machines, chrome reflections, tiled floor, fluorescent lights, empty aisles, and a secluded back corner"},
{"slug": "coin_laundry_back_row", "prompt": "coin laundry back row with stacked dryers, plastic folding tables, detergent shelves, buzzing fluorescent light, and no other customers"},
{"slug": "laundromat_mirror_windows", "prompt": "quiet laundromat with mirrored machine doors, repeated round windows, tile floor, and cool blue night light through front glass"},
],
"compositions": [
"repeating washer-door perspective behind the body",
"folding-table edge frame with chrome reflections",
"low tiled-floor angle down an empty machine row",
"back-corner composition partly hidden by laundry machines",
],
},
"train_station_lockers": {
"locations": [
{"slug": "train_station_locker_corridor", "prompt": "quiet train-station locker corridor with repeating metal lockers, tiled walls, vending machines, fluorescent light, and a hidden side alcove"},
{"slug": "empty_platform_underpass", "prompt": "empty station underpass with tiled walls, repeated poster frames, stair railings, fluorescent lights, and late-night quiet"},
{"slug": "station_service_passage", "prompt": "station service passage with repeating utility doors, metal lockers, warning stripes, and cool overhead light"},
],
"compositions": [
"locker-row vanishing-point composition",
"side-alcove frame partly blocked by metal lockers",
"fluorescent underpass frame with repeated tile lines",
"candid angle from behind a vending machine edge",
],
},
"nightclub_back_hall": {
"locations": [
{"slug": "nightclub_back_hall", "prompt": "nightclub back hallway with black doors, repeated neon strips, coat-check racks, textured walls, and distant colored dance-floor light"},
{"slug": "club_vip_corridor", "prompt": "VIP club corridor with velvet ropes, mirrored wall panels, low red light, repeated booths, and a private bend in the hallway"},
{"slug": "music_venue_greenroom_hall", "prompt": "music venue greenroom corridor with stickered doors, cable cases, dim practical lamps, and repeated black curtains"},
],
"compositions": [
"neon hallway frame with repeated dark doors",
"partly hidden VIP-booth angle",
"mirror-panel composition with colored light streaks",
"tight backstage corridor frame with curtains at the edges",
],
},
"restaurant_private_booth": {
"locations": [
{"slug": "restaurant_private_booth", "prompt": "dim restaurant private booth with high banquettes, repeating table lamps, dark wood partitions, folded napkins, and secluded sightlines"},
{"slug": "empty_bistro_back_corner", "prompt": "empty bistro back corner with tiled floor, small round tables, brass lamps, mirrored walls, and a hidden booth"},
{"slug": "afterhours_dining_room", "prompt": "after-hours dining room with stacked chairs, repeated tables, low amber sconces, and a quiet service doorway"},
],
"compositions": [
"booth-partition frame with high seat backs blocking the sides",
"table-edge composition with lamps repeating behind",
"mirror-wall restaurant angle with dark wood partitions",
"after-hours dining-room perspective through empty tables",
],
},
}
def location_theme_choices() -> list[str]:
return list(THEMATIC_LOCATION_PRESETS)
def _extension_targets() -> dict[str, tuple[list[Any], bool]]:
return {
"women_clothes": (g.WOMEN_CLOTHES, False),
"women_clothes_minimal": (g.WOMEN_CLOTHES_MINIMAL, False),
"men_clothes": (g.MEN_CLOTHES, False),
"men_clothes_minimal": (g.MEN_CLOTHES_MINIMAL, False),
"couple_outfits": (g.COUPLE_OUTFITS, False),
"couple_outfits_minimal": (g.COUPLE_OUTFITS_MINIMAL, False),
"poses": (g.POSES, False),
"evocative_poses": (g.EVOCATIVE_POSES, False),
"backside_poses": (g.BACKSIDE_POSES, False),
"expressions": (g.EXPRESSIONS, False),
"compositions": (g.COMPOSITIONS, False),
"props": (g.PROPS, False),
"figure_curvy": (g.FIGURE_CURVY, False),
"figure_athletic": (g.FIGURE_ATHLETIC, False),
"figure_bombshell": (g.FIGURE_BOMBSHELL, False),
"scenes": (g.SCENES, True),
"group_scenes": (g.GROUP_SCENES, True),
"layouts_full": (g.LAYOUTS_FULL, True),
"layouts_minimal": (g.LAYOUTS_MINIMAL, True),
"group_compositions": (g.GROUP_COMPOSITIONS, False),
"group_ages": (g.GROUP_AGES, False),
}
def apply_pool_extensions() -> None:
global _EXTENSIONS_APPLIED
if _EXTENSIONS_APPLIED:
return
targets = _extension_targets()
for path in _json_files():
data = _read_json(path)
extensions = data.get("pool_extensions", {})
if not isinstance(extensions, dict):
raise ValueError(f"pool_extensions in {path} must be an object")
for target_name, additions in extensions.items():
if target_name not in targets:
known = ", ".join(sorted(targets))
raise ValueError(f"Unknown pool extension '{target_name}' in {path}. Known: {known}")
target, expects_pair = targets[target_name]
normalized = [_pair_from(item) for item in _list_from(additions)] if expects_pair else [
_item_text(item) for item in _list_from(additions)
]
_unique_extend(target, normalized)
g.EVOCATIVE_ALL = g.EVOCATIVE_POSES + g.BACKSIDE_POSES
_EXTENSIONS_APPLIED = True
def category_choices() -> list[str]:
apply_pool_extensions()
custom = [category["name"] for category in load_category_library()]
return BUILTIN_CATEGORIES + [name for name in custom if name not in BUILTIN_CATEGORIES]
def subcategory_choices() -> list[str]:
apply_pool_extensions()
choices = [RANDOM_SUBCATEGORY]
for category in load_category_library():
for subcategory in category["subcategories"]:
choices.append(f"{category['name']} / {subcategory['name']}")
return choices
def seed_mode_choices() -> list[str]:
return list(SEED_MODE_CHOICES)
CATEGORY_PRESETS = {
"auto_weighted": ("auto_weighted", RANDOM_SUBCATEGORY),
"auto_full": ("auto_full", RANDOM_SUBCATEGORY),
"women_casual": ("Casual clothes", RANDOM_SUBCATEGORY),
"men_casual": ("Men casual clothes", RANDOM_SUBCATEGORY),
"couple_casual": ("Couple casual clothes", RANDOM_SUBCATEGORY),
"provocative_erotic": ("Provocative erotic clothes", RANDOM_SUBCATEGORY),
"hardcore_pose": ("Hardcore sexual poses", RANDOM_SUBCATEGORY),
"custom_random": ("custom_random", RANDOM_SUBCATEGORY),
}
CAST_PRESETS = {
"solo_woman": (1, 0),
"solo_man": (0, 1),
"mixed_couple": (1, 1),
"two_women": (2, 0),
"two_men": (0, 2),
"threesome_2w1m": (2, 1),
"small_group_3w2m": (3, 2),
}
GENERATION_PROFILE_PRESETS = {
"balanced": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.5,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
"standard_pose_ratio": -1.0,
"trigger": "sxcpinup_coloredpencil",
"prepend_trigger_to_prompt": True,
},
"casual_clean": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.35,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
"standard_pose_ratio": -1.0,
"trigger": "sxcpinup_coloredpencil",
"prepend_trigger_to_prompt": True,
},
"evocative_softcore": {
"clothing": "minimal",
"poses": "evocative",
"expression_enabled": True,
"expression_intensity": 0.65,
"backside_bias": 0.2,
"minimal_clothing_ratio": -1.0,
"standard_pose_ratio": -1.0,
"trigger": "sxcpinup_coloredpencil",
"prepend_trigger_to_prompt": True,
},
"hardcore_intense": {
"clothing": "minimal",
"poses": "evocative",
"expression_enabled": True,
"expression_intensity": 0.9,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
"standard_pose_ratio": -1.0,
"trigger": "sxcpinup_coloredpencil",
"prepend_trigger_to_prompt": True,
},
"krea2_friendly": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.55,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
"standard_pose_ratio": -1.0,
"trigger": "sxcpinup_coloredpencil",
"prepend_trigger_to_prompt": False,
},
"flux_original": {
"clothing": "full",
"poses": "standard",
"expression_enabled": True,
"expression_intensity": 0.5,
"backside_bias": 0.0,
"minimal_clothing_ratio": -1.0,
"standard_pose_ratio": -1.0,
"trigger": "sxcpinup_coloredpencil",
"prepend_trigger_to_prompt": True,
},
}
def category_preset_choices() -> list[str]:
return list(CATEGORY_PRESETS)
def cast_preset_choices() -> list[str]:
return list(CAST_PRESETS) + ["custom_counts"]
def generation_profile_choices() -> list[str]:
return list(GENERATION_PROFILE_PRESETS)
def build_category_config_json(preset: str = "auto_weighted", subcategory: str = RANDOM_SUBCATEGORY) -> str:
category, default_subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
chosen_subcategory = subcategory if subcategory and subcategory != RANDOM_SUBCATEGORY else default_subcategory
return json.dumps(
{
"preset": preset if preset in CATEGORY_PRESETS else "auto_weighted",
"category": category,
"subcategory": chosen_subcategory,
},
ensure_ascii=True,
sort_keys=True,
)
def _parse_category_config(category_config: str | dict[str, Any] | None) -> tuple[str, str]:
if not category_config:
return CATEGORY_PRESETS["auto_weighted"]
if isinstance(category_config, dict):
raw = category_config
else:
try:
raw = json.loads(str(category_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid category_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("category_config must be a JSON object")
preset = str(raw.get("preset") or "auto_weighted")
category, subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
category = str(raw.get("category") or category)
subcategory = str(raw.get("subcategory") or subcategory or RANDOM_SUBCATEGORY)
return category, subcategory
def build_cast_config_json(cast_mode: str = "mixed_couple", women_count: int = 1, men_count: int = 1) -> str:
if cast_mode in CAST_PRESETS:
women_count, men_count = CAST_PRESETS[cast_mode]
else:
women_count = max(0, min(12, int(women_count)))
men_count = max(0, min(12, int(men_count)))
if women_count + men_count == 0:
women_count = 1
cast_mode = "custom_counts"
return json.dumps(
{
"cast_mode": cast_mode,
"women_count": int(women_count),
"men_count": int(men_count),
},
ensure_ascii=True,
sort_keys=True,
)
def _parse_cast_config(cast_config: str | dict[str, Any] | None) -> dict[str, int | str]:
if not cast_config:
return {"cast_mode": "mixed_couple", "women_count": 1, "men_count": 1}
if isinstance(cast_config, dict):
raw = cast_config
else:
try:
raw = json.loads(str(cast_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid cast_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("cast_config must be a JSON object")
return json.loads(build_cast_config_json(str(raw.get("cast_mode") or "custom_counts"), raw.get("women_count", 1), raw.get("men_count", 1)))
def build_generation_profile_json(
profile: str = "balanced",
clothing_override: str = "profile_default",
poses_override: str = "profile_default",
expression_intensity_mode: str = "profile_default",
expression_intensity: float = -1.0,
backside_bias: float = -1.0,
minimal_clothing_ratio: float = -1.0,
standard_pose_ratio: float = -1.0,
trigger_policy: str = "profile_default",
expression_enabled: bool = True,
) -> str:
profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced"
config = dict(GENERATION_PROFILE_PRESETS[profile])
if clothing_override in ("full", "minimal", "random"):
config["clothing"] = clothing_override
if poses_override in ("standard", "evocative", "random"):
config["poses"] = poses_override
config["expression_enabled"] = not _is_false(expression_enabled)
if expression_intensity_mode == "random":
config["expression_intensity"] = -1.0
elif expression_intensity_mode == "fixed" and float(expression_intensity) >= 0:
config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"])
if float(backside_bias) >= 0:
config["backside_bias"] = _clamped_float(backside_bias, config["backside_bias"])
if float(minimal_clothing_ratio) >= 0:
config["minimal_clothing_ratio"] = _clamped_float(minimal_clothing_ratio, config["minimal_clothing_ratio"])
if float(standard_pose_ratio) >= 0:
config["standard_pose_ratio"] = _clamped_float(standard_pose_ratio, config["standard_pose_ratio"])
if trigger_policy == "prepend_trigger":
config["prepend_trigger_to_prompt"] = True
elif trigger_policy == "do_not_prepend":
config["prepend_trigger_to_prompt"] = False
config["profile"] = profile
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _parse_generation_profile(profile_config: str | dict[str, Any] | None) -> dict[str, Any]:
if not profile_config:
return dict(GENERATION_PROFILE_PRESETS["balanced"])
if isinstance(profile_config, dict):
raw = profile_config
else:
try:
raw = json.loads(str(profile_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid generation_profile JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("generation_profile must be a JSON object")
profile = str(raw.get("profile") or "balanced")
parsed = dict(GENERATION_PROFILE_PRESETS.get(profile, GENERATION_PROFILE_PRESETS["balanced"]))
parsed.update(raw)
parsed["clothing"] = parsed["clothing"] if parsed.get("clothing") in ("full", "minimal", "random") else "full"
parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative", "random") else "standard"
parsed["expression_enabled"] = not _is_false(parsed.get("expression_enabled", True))
try:
raw_expression_intensity = float(parsed.get("expression_intensity"))
except (TypeError, ValueError):
raw_expression_intensity = 0.5
parsed["expression_intensity"] = -1.0 if raw_expression_intensity < 0 else _clamped_float(raw_expression_intensity, 0.5)
parsed["backside_bias"] = _clamped_float(parsed.get("backside_bias"), 0.0)
parsed["minimal_clothing_ratio"] = _clamped_float(parsed.get("minimal_clothing_ratio"), -1.0, -1.0, 1.0)
parsed["standard_pose_ratio"] = _clamped_float(parsed.get("standard_pose_ratio"), -1.0, -1.0, 1.0)
parsed["trigger"] = str(parsed.get("trigger") or "sxcpinup_coloredpencil")
parsed["prepend_trigger_to_prompt"] = bool(parsed.get("prepend_trigger_to_prompt"))
return parsed
def build_filter_config_json(
ethnicity: str = "any",
figure: str = "curvy",
no_plus_women: bool = False,
no_black: bool = False,
include_european: bool = True,
include_mediterranean_mena: bool = True,
include_latina: bool = True,
include_east_asian: bool = True,
include_southeast_asian: bool = True,
include_south_asian: bool = True,
include_black_african: bool = True,
include_indigenous: bool = True,
include_mixed: bool = True,
include_plus_size: bool = True,
) -> str:
include_flags = {
"european": include_european,
"mediterranean_mena": include_mediterranean_mena,
"latina": include_latina,
"east_asian": include_east_asian,
"southeast_asian": include_southeast_asian,
"south_asian": include_south_asian,
"black_african": include_black_african,
"indigenous": include_indigenous,
"mixed": include_mixed,
}
selected_ethnicities = [key for key, enabled in include_flags.items() if enabled]
disabled_ethnicities = [key for key, enabled in include_flags.items() if not enabled]
enabled_ethnicities = list(selected_ethnicities)
if enabled_ethnicities:
enabled_ethnicities.extend(f"exclude_{key}" for key in disabled_ethnicities)
if 0 < len(selected_ethnicities) < len(include_flags):
ethnicity = "+".join(enabled_ethnicities)
elif not _is_valid_ethnicity_filter(ethnicity):
ethnicity = "any"
return json.dumps(
{
"ethnicity": ethnicity,
"ethnicity_includes": selected_ethnicities,
"figure": figure if figure in ("curvy", "balanced", "bombshell", "random") else "curvy",
"include_plus_size": bool(include_plus_size),
"include_black_african": bool(include_black_african),
"no_plus_women": not bool(include_plus_size) or bool(no_plus_women),
"no_black": not bool(include_black_african) or bool(no_black),
},
ensure_ascii=True,
sort_keys=True,
)
def _location_pool_names_for_preset(preset: str) -> list[str]:
scene_pools = load_scene_pool_library()
preset = str(preset or "custom_only")
if preset.startswith("pool:"):
pool_name = preset.split(":", 1)[1].strip()
return [pool_name] if pool_name in scene_pools else []
selectors = LOCATION_POOL_PRESETS.get(preset, ())
names: list[str] = []
for selector in selectors:
if selector == "*":
_unique_extend(names, sorted(scene_pools))
elif selector.endswith("_"):
_unique_extend(names, sorted(name for name in scene_pools if name.startswith(selector)))
elif selector in scene_pools:
_unique_extend(names, [selector])
return names
def _custom_location_entries(custom_locations: str) -> list[dict[str, str]]:
entries: list[dict[str, str]] = []
for raw_line in str(custom_locations or "").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
slug = ""
prompt = line
if ":" in line:
maybe_slug, maybe_prompt = line.split(":", 1)
if maybe_slug.strip() and maybe_prompt.strip():
slug = _slug(maybe_slug)
prompt = maybe_prompt.strip()
prompt = prompt.strip()
if prompt:
entries.append({"slug": slug or _slug(prompt), "prompt": prompt})
return entries
def _scene_entries_for_pool_names(pool_names: list[str]) -> list[Any]:
scene_pools = load_scene_pool_library()
entries: list[Any] = []
for pool_name in pool_names:
if pool_name not in scene_pools:
continue
_unique_extend(entries, scene_pools[pool_name])
return entries
def build_location_pool_json(
enabled: bool = True,
combine_mode: str = "replace",
preset: str = "custom_only",
custom_locations: str = "",
location_config: str | dict[str, Any] | None = "",
) -> str:
incoming = _parse_location_config(location_config)
combine_mode = combine_mode if combine_mode in ("replace", "add") else "replace"
pool_names = _location_pool_names_for_preset(preset)
entries = _scene_entries_for_pool_names(pool_names)
_unique_extend(entries, _custom_location_entries(custom_locations))
if combine_mode == "add" and incoming.get("enabled"):
apply_mode = str(incoming.get("apply_mode") or "replace")
merged_pool_names = _list_from(incoming.get("pool_names"))
_unique_extend(merged_pool_names, pool_names)
merged_entries = _list_from(incoming.get("scene_entries"))
_unique_extend(merged_entries, entries)
else:
apply_mode = "replace" if combine_mode == "replace" else "add"
merged_pool_names = pool_names
merged_entries = entries
active = bool(enabled) and bool(merged_entries)
summary = (
f"{apply_mode}; pools={len(merged_pool_names)}; locations={len(merged_entries)}"
if active
else "disabled or empty"
)
return json.dumps(
{
"enabled": active,
"apply_mode": apply_mode,
"pool_names": merged_pool_names,
"scene_entries": merged_entries,
"summary": summary,
},
ensure_ascii=True,
sort_keys=True,
)
def _parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]:
if not location_config:
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "scene_entries": []}
if isinstance(location_config, dict):
raw = dict(location_config)
else:
try:
raw = json.loads(str(location_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid location_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("location_config must be a JSON object")
entries = _list_from(raw.get("scene_entries"))
if not entries and raw.get("pool_names"):
entries = _scene_entries_for_pool_names([str(name) for name in _list_from(raw.get("pool_names"))])
return {
"enabled": bool(raw.get("enabled")) and bool(entries),
"apply_mode": str(raw.get("apply_mode") or "replace") if str(raw.get("apply_mode") or "replace") in ("replace", "add") else "replace",
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
"scene_entries": entries,
"summary": str(raw.get("summary") or ""),
}
def _location_config_active(location_config: dict[str, Any]) -> bool:
return bool(location_config.get("enabled")) and bool(location_config.get("scene_entries"))
def _composition_pool_names_for_preset(preset: str) -> list[str]:
composition_pools = load_composition_pool_library()
preset = str(preset or "custom_only")
if preset.startswith("pool:"):
pool_name = preset.split(":", 1)[1].strip()
return [pool_name] if pool_name in composition_pools else []
selectors = COMPOSITION_POOL_PRESETS.get(preset, ())
names: list[str] = []
for selector in selectors:
if selector == "*":
_unique_extend(names, sorted(composition_pools))
elif selector.endswith("_"):
_unique_extend(names, sorted(name for name in composition_pools if name.startswith(selector)))
elif selector in composition_pools:
_unique_extend(names, [selector])
return names
def _custom_composition_entries(custom_compositions: str) -> list[str]:
entries: list[str] = []
for raw_line in str(custom_compositions or "").splitlines():
line = raw_line.strip()
if not line or line.startswith("#"):
continue
entries.append(line)
return entries
def _composition_entries_for_pool_names(pool_names: list[str]) -> list[Any]:
composition_pools = load_composition_pool_library()
entries: list[Any] = []
for pool_name in pool_names:
if pool_name not in composition_pools:
continue
_unique_extend(entries, composition_pools[pool_name])
return entries
def build_composition_pool_json(
enabled: bool = True,
combine_mode: str = "replace",
preset: str = "custom_only",
custom_compositions: str = "",
composition_config: str | dict[str, Any] | None = "",
) -> str:
incoming = _parse_composition_config(composition_config)
combine_mode = combine_mode if combine_mode in ("replace", "add") else "replace"
pool_names = _composition_pool_names_for_preset(preset)
entries = _composition_entries_for_pool_names(pool_names)
_unique_extend(entries, COMPOSITION_INLINE_PRESETS.get(str(preset or ""), []))
_unique_extend(entries, _custom_composition_entries(custom_compositions))
if combine_mode == "add" and incoming.get("enabled"):
apply_mode = str(incoming.get("apply_mode") or "replace")
merged_pool_names = _list_from(incoming.get("pool_names"))
_unique_extend(merged_pool_names, pool_names)
merged_entries = _list_from(incoming.get("composition_entries"))
_unique_extend(merged_entries, entries)
else:
apply_mode = "replace" if combine_mode == "replace" else "add"
merged_pool_names = pool_names
merged_entries = entries
active = bool(enabled) and bool(merged_entries)
summary = (
f"{apply_mode}; pools={len(merged_pool_names)}; compositions={len(merged_entries)}"
if active
else "disabled or empty"
)
return json.dumps(
{
"enabled": active,
"apply_mode": apply_mode,
"pool_names": merged_pool_names,
"composition_entries": merged_entries,
"summary": summary,
},
ensure_ascii=True,
sort_keys=True,
)
def _parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]:
if not composition_config:
return {"enabled": False, "apply_mode": "replace", "pool_names": [], "composition_entries": []}
if isinstance(composition_config, dict):
raw = dict(composition_config)
else:
try:
raw = json.loads(str(composition_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid composition_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("composition_config must be a JSON object")
entries = _list_from(raw.get("composition_entries"))
if not entries and raw.get("pool_names"):
entries = _composition_entries_for_pool_names([str(name) for name in _list_from(raw.get("pool_names"))])
return {
"enabled": bool(raw.get("enabled")) and bool(entries),
"apply_mode": str(raw.get("apply_mode") or "replace") if str(raw.get("apply_mode") or "replace") in ("replace", "add") else "replace",
"pool_names": [str(name) for name in _list_from(raw.get("pool_names")) if str(name).strip()],
"composition_entries": entries,
"summary": str(raw.get("summary") or ""),
}
def _composition_config_active(composition_config: dict[str, Any]) -> bool:
return bool(composition_config.get("enabled")) and bool(composition_config.get("composition_entries"))
def build_thematic_location_json(
enabled: bool = True,
combine_mode: str = "replace",
theme: str = "semi_public_affair",
custom_locations: str = "",
custom_compositions: str = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
) -> tuple[str, str, str]:
theme_data = THEMATIC_LOCATION_PRESETS.get(str(theme or ""), THEMATIC_LOCATION_PRESETS["semi_public_affair"])
location_lines = "\n".join(
f"{entry['slug']}: {entry['prompt']}"
for entry in theme_data.get("locations", [])
if isinstance(entry, dict) and entry.get("slug") and entry.get("prompt")
)
if custom_locations.strip():
location_lines = "\n".join(part for part in (location_lines, custom_locations.strip()) if part)
composition_lines = "\n".join(str(entry) for entry in theme_data.get("compositions", []) if str(entry).strip())
if custom_compositions.strip():
composition_lines = "\n".join(part for part in (composition_lines, custom_compositions.strip()) if part)
resolved_location_config = build_location_pool_json(
enabled=enabled,
combine_mode=combine_mode,
preset="custom_only",
custom_locations=location_lines,
location_config=location_config or "",
)
resolved_composition_config = build_composition_pool_json(
enabled=enabled,
combine_mode=combine_mode,
preset="custom_only",
custom_compositions=composition_lines,
composition_config=composition_config or "",
)
location_summary = json.loads(resolved_location_config).get("summary", "")
composition_summary = json.loads(resolved_composition_config).get("summary", "")
summary = f"{theme}; locations={location_summary}; compositions={composition_summary}"
return resolved_location_config, resolved_composition_config, summary
def _ethnicity_text_from_value(value: Any) -> str:
if isinstance(value, dict):
return str(value.get("ethnicity") or "").strip()
text = str(value or "").strip()
if not text:
return ""
if text.startswith("{"):
try:
raw = json.loads(text)
except json.JSONDecodeError:
return text
if isinstance(raw, dict):
return str(raw.get("ethnicity") or "").strip()
return text
def _is_valid_ethnicity_filter(value: Any) -> bool:
text = _ethnicity_text_from_value(value)
return text == "any" or text in ETHNICITY_FILTER_CHOICES or "+" in text
def normalize_ethnicity_filter(value: Any, default: str = "any", allow_random: bool = False) -> str:
text = _ethnicity_text_from_value(value)
if text.lower() in CHARACTER_RANDOM_TOKENS:
return "random" if allow_random else default
return text if _is_valid_ethnicity_filter(text) else default
def build_ethnicity_list_json(
include_european: bool = False,
include_mediterranean_mena: bool = False,
include_latina: bool = False,
include_east_asian: bool = False,
include_southeast_asian: bool = False,
include_south_asian: bool = False,
include_black_african: bool = False,
include_indigenous: bool = False,
include_mixed: bool = False,
include_asian: bool = False,
include_white_asian: bool = False,
include_western_european: bool = False,
include_french_european: bool = False,
include_germanic_european: bool = False,
include_nordic_european: bool = False,
include_celtic_european: bool = False,
include_slavic_european: bool = False,
include_baltic_european: bool = False,
include_alpine_european: bool = False,
include_balkan_european: bool = False,
include_greek_mediterranean: bool = False,
include_italian_mediterranean: bool = False,
include_iberian_mediterranean: bool = False,
strict_excludes: bool = True,
) -> dict[str, str]:
include_flags = {
"european": include_european,
"mediterranean_mena": include_mediterranean_mena,
"latina": include_latina,
"east_asian": include_east_asian,
"southeast_asian": include_southeast_asian,
"south_asian": include_south_asian,
"black_african": include_black_african,
"indigenous": include_indigenous,
"mixed": include_mixed,
"asian": include_asian,
"white_asian": include_white_asian,
"western_european": include_western_european,
"french_european": include_french_european,
"germanic_european": include_germanic_european,
"nordic_european": include_nordic_european,
"celtic_european": include_celtic_european,
"slavic_european": include_slavic_european,
"baltic_european": include_baltic_european,
"alpine_european": include_alpine_european,
"balkan_european": include_balkan_european,
"greek_mediterranean": include_greek_mediterranean,
"italian_mediterranean": include_italian_mediterranean,
"iberian_mediterranean": include_iberian_mediterranean,
}
selected = [key for key in ETHNICITY_LIST_KEYS if include_flags.get(key)]
if not selected or set(selected) == set(ETHNICITY_LIST_KEYS):
ethnicity = "any"
else:
tokens = list(selected)
if strict_excludes:
protected: set[str] = set()
if "asian" in selected:
protected.update(("east_asian", "southeast_asian", "south_asian"))
if "white_asian" in selected:
protected.update(("european", "east_asian", "southeast_asian", "south_asian", "mixed"))
if any(key in selected for key in EUROPEAN_REGIONAL_LIST_KEYS):
protected.add("european")
if any(key in selected for key in MEDITERRANEAN_REGIONAL_LIST_KEYS):
protected.add("mediterranean_mena")
if "mixed" in selected:
protected.update(ETHNICITY_BASE_LIST_KEYS)
tokens.extend(
f"exclude_{key}"
for key in ETHNICITY_BASE_LIST_KEYS
if key not in selected and key not in protected
)
ethnicity = "+".join(tokens)
filter_config = {
"ethnicity": ethnicity,
"ethnicity_includes": selected,
}
summary = "any ethnicity" if ethnicity == "any" else "ethnicity list: " + ", ".join(selected)
return {
"ethnicity": ethnicity,
"filter_config": json.dumps(filter_config, ensure_ascii=True, sort_keys=True),
"summary": summary,
}
def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
defaults = {
"ethnicity": "any",
"figure": "curvy",
"no_plus_women": False,
"no_black": False,
"include_plus_size": True,
"include_black_african": True,
}
if not filter_config:
return defaults
if isinstance(filter_config, dict):
raw = filter_config
else:
text = str(filter_config).strip()
if not text.startswith("{"):
raw = {"ethnicity": text}
else:
try:
raw = json.loads(text)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid filter_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("filter_config must be a JSON object")
parsed = {**defaults, **raw}
parsed["ethnicity"] = normalize_ethnicity_filter(parsed.get("ethnicity"), "any")
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell", "random") else "curvy"
parsed["include_plus_size"] = bool(parsed.get("include_plus_size"))
parsed["include_black_african"] = bool(parsed.get("include_black_african"))
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
parsed["no_black"] = bool(parsed.get("no_black"))
return parsed
def _normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
text = str(value or default).strip()
return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default
def _normalize_hardcore_position_values(values: Any) -> list[str]:
raw_values = _list_from(values)
selected: list[str] = []
for value in raw_values:
text = str(value or "").strip()
if not text or text == "any":
continue
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
if normalized in HARDCORE_POSITION_KEY_CHOICES and normalized not in selected:
selected.append(normalized)
return selected
def _empty_hardcore_position_config() -> dict[str, Any]:
return {
"config_type": "hardcore_position",
"enabled": False,
"family": "any",
"positions": [],
"require_position": False,
"allow_toys": True,
"allow_double": True,
"allow_penetration": True,
"allow_foreplay": True,
"allow_interaction": True,
"allow_manual": True,
"allow_oral": True,
"allow_outercourse": True,
"allow_anal": True,
"allow_climax": True,
}
def _parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return _empty_hardcore_position_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return _empty_hardcore_position_config()
if not isinstance(raw, dict):
return _empty_hardcore_position_config()
parsed = {**_empty_hardcore_position_config(), **raw}
parsed["enabled"] = bool(parsed.get("enabled", True))
parsed["family"] = _normalize_hardcore_position_family(parsed.get("family"))
parsed["positions"] = _normalize_hardcore_position_values(parsed.get("positions"))
parsed["require_position"] = not _is_false(parsed.get("require_position", False))
for key in (
"allow_toys",
"allow_double",
"allow_penetration",
"allow_foreplay",
"allow_interaction",
"allow_manual",
"allow_oral",
"allow_outercourse",
"allow_anal",
"allow_climax",
):
parsed[key] = not _is_false(parsed.get(key, True))
return parsed
def _hardcore_position_summary(config: dict[str, Any]) -> str:
if not config.get("enabled"):
return "hardcore position unrestricted"
parts = [f"family={config.get('family', 'any')}"]
positions = config.get("positions") or []
if positions:
parts.append("positions=" + ",".join(positions))
elif config.get("require_position"):
parts.append("position_templates=required")
disabled = [
label
for key, label in (
("allow_toys", "toys"),
("allow_double", "double"),
("allow_penetration", "penetration"),
("allow_foreplay", "foreplay"),
("allow_interaction", "interaction"),
("allow_manual", "manual"),
("allow_oral", "oral"),
("allow_outercourse", "outercourse"),
("allow_anal", "anal"),
("allow_climax", "climax"),
)
if not config.get(key, True)
]
if disabled:
parts.append("blocked=" + ",".join(disabled))
return "; ".join(parts)
def build_hardcore_position_pool_json(
hardcore_position_config: str | dict[str, Any] | None = "",
combine_mode: str = "replace",
family: str = "any",
selected_positions: list[str] | tuple[str, ...] | str | None = None,
) -> str:
base = _parse_hardcore_position_config(hardcore_position_config)
if combine_mode == "replace":
base = {**_empty_hardcore_position_config(), "enabled": True}
else:
base["enabled"] = True
base["family"] = _normalize_hardcore_position_family(family, base.get("family", "any"))
selected = _normalize_hardcore_position_values(selected_positions)
if combine_mode == "add":
existing = list(base.get("positions") or [])
for value in selected:
if value not in existing:
existing.append(value)
base["positions"] = existing
else:
base["positions"] = selected
base["require_position"] = bool(base.get("require_position")) or bool(base["positions"]) or base["family"] != "any"
base["summary"] = _hardcore_position_summary(base)
return json.dumps(base, ensure_ascii=True, sort_keys=True)
def build_hardcore_action_filter_json(
hardcore_position_config: str | dict[str, Any] | None = "",
focus: str = "keep_pool",
allow_toys: bool = False,
allow_double: bool = False,
allow_penetration: bool = True,
allow_foreplay: bool = True,
allow_interaction: bool = True,
allow_manual: bool = True,
allow_oral: bool = True,
allow_outercourse: bool = True,
allow_anal: bool = True,
allow_climax: bool = True,
) -> str:
config = _parse_hardcore_position_config(hardcore_position_config)
config["enabled"] = True
focus = str(focus or "keep_pool").strip()
focus_family = {
"penetration_only": "penetrative",
"foreplay_only": "foreplay",
"interaction_only": "interaction",
"manual_only": "manual",
"oral_only": "oral",
"outercourse_only": "outercourse",
"anal_only": "anal",
"climax_only": "climax",
"threesome_only": "threesome",
"group_only": "group",
}.get(focus)
if focus_family:
config["family"] = focus_family
config["allow_toys"] = bool(allow_toys)
config["allow_double"] = bool(allow_double)
config["allow_penetration"] = bool(allow_penetration)
config["allow_foreplay"] = bool(allow_foreplay)
config["allow_interaction"] = bool(allow_interaction)
config["allow_manual"] = bool(allow_manual)
config["allow_oral"] = bool(allow_oral)
config["allow_outercourse"] = bool(allow_outercourse)
config["allow_anal"] = bool(allow_anal)
config["allow_climax"] = bool(allow_climax)
if not focus_family and config["family"] != "any":
enabled_action_families = {
family
for enabled, family in (
(config["allow_penetration"], "penetrative"),
(config["allow_foreplay"], "foreplay"),
(config["allow_interaction"], "interaction"),
(config["allow_manual"], "manual"),
(config["allow_oral"], "oral"),
(config["allow_outercourse"], "outercourse"),
(config["allow_anal"], "anal"),
(config["allow_climax"], "climax"),
)
if enabled
}
if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
config["family"] = "any"
if focus == "foreplay_only":
config["allow_foreplay"] = True
config["allow_interaction"] = True
elif focus == "interaction_only":
config["allow_interaction"] = True
config["allow_foreplay"] = True
elif focus == "manual_only":
config["allow_manual"] = True
elif focus == "oral_only":
config["allow_oral"] = True
config["allow_penetration"] = False
elif focus == "outercourse_only":
config["allow_outercourse"] = True
config["allow_oral"] = False
config["allow_penetration"] = False
elif focus == "anal_only":
config["allow_anal"] = True
config["allow_penetration"] = True
elif focus == "climax_only":
config["allow_climax"] = True
config["summary"] = _hardcore_position_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _hardcore_position_config_active(config: dict[str, Any]) -> bool:
return bool(config.get("enabled"))
def _hardcore_position_template_required(config: dict[str, Any]) -> bool:
if not _hardcore_position_config_active(config):
return False
return bool(config.get("require_position")) or bool(config.get("positions")) or _normalize_hardcore_position_family(config.get("family")) != "any"
def _is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
return str(category.get("slug") or "").strip() == "hardcore_sexual_poses" or str(category.get("name") or "").strip().lower() == "hardcore sexual poses"
def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
family = _normalize_hardcore_position_family(config.get("family"))
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
if not config.get("allow_penetration", True):
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
if not config.get("allow_foreplay", True):
allowed.discard("foreplay_teasing")
if not config.get("allow_interaction", True):
allowed.difference_update(
{
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"group_coordination",
"aftercare_cleanup",
}
)
if not config.get("allow_manual", True):
allowed.discard("manual_stimulation")
if not config.get("allow_oral", True):
allowed.discard("oral_sex")
if not config.get("allow_outercourse", True):
allowed.discard("outercourse_sex")
if not config.get("allow_anal", True):
allowed.discard("anal_double_penetration")
if not config.get("allow_climax", True):
allowed.discard("cumshot_climax")
if not config.get("allow_double", True) and family == "anal":
allowed.add("anal_double_penetration")
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
def _filter_hardcore_categories_for_position(
categories: list[dict[str, Any]],
config: dict[str, Any],
women_count: int,
men_count: int,
) -> list[dict[str, Any]]:
if not _hardcore_position_config_active(config):
return categories
allowed = _hardcore_allowed_subcategory_slugs(config)
filtered_categories: list[dict[str, Any]] = []
for category in categories:
if not _is_hardcore_sexual_category(category):
filtered_categories.append(category)
continue
category_copy = dict(category)
subcategories = [
subcategory
for subcategory in category.get("subcategories", [])
if str(subcategory.get("slug") or "") in allowed and _compatible_entry(subcategory, women_count, men_count)
and _hardcore_subcategory_supports_positions(subcategory, config)
]
if subcategories:
category_copy["subcategories"] = subcategories
filtered_categories.append(category_copy)
return filtered_categories
def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str, Any]) -> bool:
text = str(text or "").lower()
axis_name = str(axis_name or "").lower()
if not config.get("allow_toys", True) and any(term in text for term in ("toy", "dildo", "strap-on", "strap on")):
return True
if not config.get("allow_double", True) and (
axis_name == "double_act"
or any(term in text for term in ("double penetration", "double-penetration", "front-and-back", "front and back", "second penetration", "both sides", "two partners penetrating", "multiple penetrations"))
):
return True
if not config.get("allow_anal", True) and (
axis_name == "anal_act"
or any(term in text for term in (" anal", "anal sex", "anal penetration", "anus", "rear-entry anal", "penis entering ass", "thrusts into her ass", "thrusts into his ass"))
):
return True
if not config.get("allow_oral", True) and (
axis_name in ("oral_act", "oral_detail")
or any(term in text for term in ("oral sex", "mouth on genitals", "mouth on pussy", "blowjob", "cunnilingus", "tongue on pussy", "deepthroat", "fellatio"))
):
return True
if not config.get("allow_outercourse", True) and (
axis_name in ("outer_act", "contact_detail", "texture_detail")
or any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex", "testicle", "balls", "penis licking", "penis-licking", "footjob", "soles", "toes"))
):
return True
if not config.get("allow_penetration", True) and (
axis_name in ("penetration_act", "penetration_detail", "anal_act", "double_act", "thrust_detail")
or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex"))
):
return True
if not config.get("allow_foreplay", True) and (
axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail")
or any(
term in text
for term in (
"kiss",
"kissing",
"mouth-to-mouth",
"caress",
"caressing",
"stroking skin",
"hands roaming",
"touching breasts",
"cupping breasts",
"hand on the cheek",
"fingers under the chin",
"undressing",
"removing clothing",
"removing clothes",
"pulling clothing",
"sliding straps",
"unbuttoning",
)
)
):
return True
if not config.get("allow_interaction", True) and (
axis_name
in (
"tease_act",
"touch_detail",
"clothing_detail",
"foreplay_detail",
"face_detail",
"body_contact",
"mood_detail",
"worship_act",
"transition_act",
"control_act",
"performance_act",
"coordination_act",
"aftercare_act",
"cleanup_detail",
)
or any(
term in text
for term in (
"kiss",
"kissing",
"caress",
"body worship",
"nipple",
"ass grab",
"thigh",
"hair holding",
"wrists",
"dirty talk",
"whispering",
"undressing",
"position transition",
"guided",
"camera",
"watching",
"aftercare",
"cleanup",
"wiping",
)
)
):
return True
if not config.get("allow_manual", True) and (
axis_name in ("manual_act", "manual_detail")
or any(
term in text
for term in (
"fingering",
"fingers inside",
"clit",
"clitoris",
"manual stimulation",
"mutual masturbation",
"masturbating together",
"fingers on pussy",
"fingers on clit",
)
)
):
return True
if not config.get("allow_climax", True) and (
axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
):
return True
return False
def _hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
positions = config.get("positions") or []
if not positions:
return True
text = _entry_text(entry).lower()
for position in positions:
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
return True
return False
def _hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> bool:
selected = set(config.get("positions") or [])
if not selected:
return False
text = _entry_text(entry).lower()
matched = {
position
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
if any(term in text for term in terms)
}
return bool(matched) and not bool(matched & selected)
def _hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
if not _hardcore_position_template_required(config):
return True
axes = subcategory.get("item_axes")
if not isinstance(axes, dict):
return True
for axis_name, values in axes.items():
if str(axis_name) in HARDCORE_POSITION_AXIS_KEYS and any(
_hardcore_position_entry_matches(value, config)
for value in _list_from(values)
):
return True
return False
def _filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, Any]) -> list[Any]:
if not _hardcore_position_config_active(config):
return values
filtered = [
value
for value in values
if not _hardcore_text_blocked_by_action(_entry_text(value), axis_name, config)
and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and _hardcore_position_entry_conflicts(value, config))
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or _hardcore_position_entry_matches(value, config))
]
return filtered or values
def _filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> list[Any]:
if not _hardcore_position_config_active(config):
return templates
filtered: list[Any] = []
for template in templates:
text = _entry_text(template)
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
blocked = _hardcore_position_template_required(config) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS)
blocked = blocked or any(_hardcore_text_blocked_by_action(text, field, config) for field in fields | {""})
if not blocked:
filtered.append(template)
return filtered or templates
def _apply_hardcore_position_config_to_subcategory(
subcategory: dict[str, Any],
config: dict[str, Any],
) -> dict[str, Any]:
if not _hardcore_position_config_active(config):
return subcategory
subcategory_copy = dict(subcategory)
if "item_templates" in subcategory_copy:
subcategory_copy["item_templates"] = _filter_hardcore_templates(_list_from(subcategory_copy["item_templates"]), config)
raw_axes = subcategory_copy.get("item_axes")
if isinstance(raw_axes, dict):
axes = {}
for axis_name, values in raw_axes.items():
axes[axis_name] = _filter_hardcore_axis(str(axis_name), _list_from(values), config)
subcategory_copy["item_axes"] = axes
subcategory_copy["hardcore_position_config"] = config
return subcategory_copy
def _ratio_or_none(value: float) -> float | None:
try:
ratio = float(value)
except (TypeError, ValueError):
return None
if ratio < 0:
return None
return max(0.0, min(1.0, ratio))
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):
return default
return max(min_value, min(max_value, number))
def build_seed_config_json(
category_seed: int = -1,
subcategory_seed: int = -1,
content_seed: int = -1,
person_seed: int = -1,
scene_seed: int = -1,
pose_seed: int = -1,
role_seed: int = -1,
expression_seed: int = -1,
composition_seed: int = -1,
category_seed_mode: str = "auto",
subcategory_seed_mode: str = "auto",
content_seed_mode: str = "auto",
person_seed_mode: str = "auto",
scene_seed_mode: str = "auto",
pose_seed_mode: str = "auto",
role_seed_mode: str = "auto",
expression_seed_mode: str = "auto",
composition_seed_mode: str = "auto",
) -> str:
rng = random.SystemRandom()
def axis_seed(value: int, mode: str) -> int:
mode = mode if mode in SEED_MODE_CHOICES else "auto"
if mode == "auto":
return int(value)
if mode == "random":
return rng.randint(0, 0xFFFFFFFF)
if mode == "fixed":
return max(0, int(value))
return -1
return json.dumps(
{
"category_seed": axis_seed(category_seed, category_seed_mode),
"subcategory_seed": axis_seed(subcategory_seed, subcategory_seed_mode),
"content_seed": axis_seed(content_seed, content_seed_mode),
"person_seed": axis_seed(person_seed, person_seed_mode),
"scene_seed": axis_seed(scene_seed, scene_seed_mode),
"pose_seed": axis_seed(pose_seed, pose_seed_mode),
"role_seed": axis_seed(role_seed, role_seed_mode),
"expression_seed": axis_seed(expression_seed, expression_seed_mode),
"composition_seed": axis_seed(composition_seed, composition_seed_mode),
},
ensure_ascii=True,
sort_keys=True,
)
def build_seed_lock_config_json(
base_seed: int = 20260614,
reroll_axis: str = "none",
reroll_seed: int = -1,
) -> str:
base_seed = int(base_seed)
reroll_seed = int(reroll_seed)
reroll_groups = {
"none": (),
"category": ("category",),
"subcategory": ("subcategory",),
"content": ("content",),
"person": ("person",),
"scene": ("scene",),
"pose": ("pose", "role"),
"role": ("role",),
"expression": ("expression",),
"composition": ("composition",),
"content_pose": ("content", "pose", "role"),
"scene_pose": ("scene", "pose", "role"),
}
reroll = set(reroll_groups.get(str(reroll_axis or "none"), ()))
config: dict[str, int] = {}
for axis in SEED_LOCK_AXES:
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
if not seed_config:
return {}
if isinstance(seed_config, dict):
raw = seed_config
else:
try:
raw = json.loads(str(seed_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid seed_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("seed_config must be a JSON object")
parsed: dict[str, int] = {}
for key, value in raw.items():
try:
parsed[str(key)] = int(value)
except (TypeError, ValueError):
continue
return parsed
def _configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
for key in SEED_AXIS_ALIASES.get(axis, (axis,)):
value = seed_config.get(key)
if value is not None and value >= 0:
return value
return None
def _axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
configured = _configured_axis_seed(seed_config, axis)
salt = SEED_AXIS_SALTS.get(axis, 0)
if configured is None:
return random.Random(_row_seed(base_seed, row_number, salt))
return random.Random(_row_seed(configured, row_number, salt))
def _is_pose_content_category(category: dict[str, Any], subcategory: dict[str, Any]) -> bool:
haystack = " ".join(
str(value)
for value in (
category.get("name", ""),
category.get("slug", ""),
category.get("item_label", ""),
subcategory.get("name", ""),
subcategory.get("slug", ""),
subcategory.get("item_label", ""),
)
).lower()
return "pose" in haystack or "sex" in haystack
def _format(template: str, context: dict[str, Any]) -> str:
fields = {key for _, key, _, _ in Formatter().parse(template) if key}
safe_context = SafeFormatDict({key: str(value) for key, value in context.items()})
for field in fields:
safe_context.setdefault(field, "{" + field + "}")
return template.format_map(safe_context)
def _clean_prompt_punctuation(text: str) -> str:
text = re.sub(r"\s+", " ", str(text or "")).strip()
text = re.sub(r"\s+([,.;:])", r"\1", text)
text = re.sub(r"(?:,\s*){2,}", ", ", text)
text = re.sub(r"\.\s*\.", ".", text)
text = re.sub(r":\s*\.", ".", text)
return text.strip()
def _strip_expression_text(text: str, expression: Any = "") -> str:
text = str(text or "")
if not text:
return ""
text = re.sub(r"\s*Facial expressions?:\s*[^.]*\.\s*", " ", text, flags=re.IGNORECASE)
text = re.sub(r",\s*one with [^,]+ and the other with [^,]+(?=,)", "", text, flags=re.IGNORECASE)
text = re.sub(r",\s*a lively mix of expressions from [^,]+(?=,)", "", text, flags=re.IGNORECASE)
text = re.sub(r"\s+with\s+(?:an?|the)\s+[^,]*expression(?=,)", "", text, flags=re.IGNORECASE)
expression_text = str(expression or "").strip()
if expression_text:
for part in [piece.strip() for piece in expression_text.split(";") if piece.strip()]:
escaped = re.escape(part)
text = re.sub(rf",\s*{escaped}(?=,)", "", text, flags=re.IGNORECASE)
text = re.sub(rf"\s+with\s+(?:an?|the)?\s*{escaped}", "", text, flags=re.IGNORECASE)
return _clean_prompt_punctuation(text)
def _disable_row_expression(row: dict[str, Any], source: str = "disabled") -> dict[str, Any]:
previous_expression = row.get("expression", "")
row["prompt"] = _strip_expression_text(row.get("prompt", ""), previous_expression)
row["caption"] = _strip_expression_text(row.get("caption", ""), previous_expression)
row["expression"] = ""
row["shared_expression"] = ""
row["character_expressions"] = []
row["character_expression_text"] = ""
row["expression_enabled"] = False
row["expression_disabled"] = True
row["expression_intensity"] = None
row["expression_intensity_source"] = source
return row
def _labeled_expression_sentence(label: str, expression: Any) -> str:
expression = str(expression or "").strip()
if not expression:
return ""
return f"{label}: {expression}. "
def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str:
trigger = trigger.strip()
if not enabled or not trigger:
return prompt
if prompt.lower().startswith(trigger.lower()):
return prompt
return f"{trigger}, {prompt}"
def _combined_negative(base: str, extra: str) -> str:
parts = [part.strip() for part in (base, extra) if part and part.strip()]
return ", ".join(parts)
def camera_mode_choices() -> list[str]:
return list(CAMERA_MODE_PROMPTS)
def ethnicity_choices() -> list[str]:
return list(ETHNICITY_FILTER_CHOICES)
def character_label_choices() -> list[str]:
return list(CHARACTER_LABEL_CHOICES)
def character_age_choices() -> list[str]:
return list(CHARACTER_AGE_CHOICES)
def character_body_choices() -> list[str]:
return list(CHARACTER_BODY_CHOICES)
def character_woman_body_choices() -> list[str]:
return list(CHARACTER_WOMAN_BODY_CHOICES)
def character_man_body_choices() -> list[str]:
return list(CHARACTER_MAN_BODY_CHOICES)
def character_descriptor_detail_choices() -> list[str]:
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
def character_presence_choices() -> list[str]:
return list(CHARACTER_PRESENCE_CHOICES)
def character_hair_color_choices() -> list[str]:
return list(CHARACTER_HAIR_COLOR_CHOICES)
def character_hair_length_choices() -> list[str]:
return list(CHARACTER_HAIR_LENGTH_CHOICES)
def character_hair_style_choices() -> list[str]:
return list(CHARACTER_HAIR_STYLE_CHOICES)
def character_eye_color_choices() -> list[str]:
return list(CHARACTER_EYE_COLOR_CHOICES)
def character_ethnicity_choices() -> list[str]:
return ["random"] + list(ETHNICITY_FILTER_CHOICES)
def character_figure_choices() -> list[str]:
return ["random", "curvy", "balanced", "bombshell"]
def camera_detail_choices() -> list[str]:
return list(CAMERA_DETAIL_CHOICES)
def hardcore_detail_density_choices() -> list[str]:
return list(HARDCORE_DETAIL_DENSITY_CHOICES)
def hardcore_position_family_choices() -> list[str]:
return list(HARDCORE_POSITION_FAMILY_CHOICES)
def hardcore_position_focus_choices() -> list[str]:
return list(HARDCORE_POSITION_FOCUS_CHOICES)
def hardcore_position_key_choices() -> list[str]:
return list(HARDCORE_POSITION_KEY_CHOICES)
def character_softcore_outfit_source_choices() -> list[str]:
return [
"no_change",
"social_tease",
"lingerie_tease",
"implied_nude",
"explicit_tease",
"explicit_nude",
"partner_woman",
"partner_man",
"custom",
]
def character_hardcore_clothing_state_choices() -> list[str]:
return [
"no_change",
"fully_nude",
"partly_exposed",
"same_outfit",
"partially_removed",
"custom",
]
def camera_orbit_framing_choices() -> list[str]:
return list(CAMERA_ORBIT_FRAMING_CHOICES)
def camera_orbit_focus_choices() -> list[str]:
return list(CAMERA_ORBIT_FOCUS_CHOICES)
def camera_shot_choices() -> list[str]:
return list(CAMERA_SHOT_PROMPTS)
def camera_angle_choices() -> list[str]:
return list(CAMERA_ANGLE_PROMPTS)
def camera_lens_choices() -> list[str]:
return list(CAMERA_LENS_PROMPTS)
def camera_distance_choices() -> list[str]:
return list(CAMERA_DISTANCE_PROMPTS)
def camera_orientation_choices() -> list[str]:
return list(CAMERA_ORIENTATION_PROMPTS)
def camera_phone_choices() -> list[str]:
return list(CAMERA_PHONE_PROMPTS)
def camera_priority_choices() -> list[str]:
return list(CAMERA_PRIORITY_PROMPTS)
def build_camera_config_json(
camera_mode: str = "standard",
shot_size: str = "auto",
angle: str = "auto",
lens: str = "auto",
distance: str = "auto",
orientation: str = "auto",
phone_visibility: str = "auto",
priority: str = "strong",
camera_detail: str = "compact",
) -> str:
return json.dumps(
{
"camera_mode": camera_mode,
"shot_size": shot_size,
"angle": angle,
"lens": lens,
"distance": distance,
"orientation": orientation,
"phone_visibility": phone_visibility,
"priority": priority,
"camera_detail": camera_detail,
},
ensure_ascii=True,
sort_keys=True,
)
def _camera_orbit_direction(horizontal_angle: Any) -> str:
h_angle = int(float(horizontal_angle or 0)) % 360
if h_angle < 22.5 or h_angle >= 337.5:
return "front view"
if h_angle < 67.5:
return "front-right quarter view"
if h_angle < 112.5:
return "right side view"
if h_angle < 157.5:
return "back-right quarter view"
if h_angle < 202.5:
return "back view"
if h_angle < 247.5:
return "back-left quarter view"
if h_angle < 292.5:
return "left side view"
return "front-left quarter view"
def _camera_orbit_elevation(vertical_angle: Any) -> str:
vertical = int(float(vertical_angle or 0))
if vertical < -15:
return "low-angle shot"
if vertical < 15:
return "eye-level shot"
if vertical < 45:
return "elevated shot"
return "high-angle shot"
def _camera_orbit_distance(zoom: Any, framing: str = "from_zoom") -> str:
framing = framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom"
framing_labels = {
"wide": "wide shot",
"medium": "medium shot",
"full_body": "full-body shot",
"three_quarter": "three-quarter body shot",
"close_up": "close-up",
"extreme_close_up": "extreme close-up",
}
if framing != "from_zoom":
return framing_labels[framing]
zoom_value = float(zoom or 0.0)
if zoom_value < 2:
return "wide shot"
if zoom_value < 6:
return "medium shot"
return "close-up"
def _camera_orbit_focus(subject_focus: str) -> str:
return {
"face": "face and expression centered",
"torso": "torso and hands centered",
"hips": "hips and lower body centered",
"full_body": "full body centered",
"action": "main action centered",
"contact_points": "body contact points centered",
"environment": "subject and room both readable",
}.get(str(subject_focus or "auto"), "")
def _camera_orbit_prompt(
horizontal_angle: Any,
vertical_angle: Any,
zoom: Any,
framing: str = "from_zoom",
subject_focus: str = "auto",
include_degrees: bool = True,
) -> tuple[str, dict[str, Any]]:
azimuth = max(0, min(359, int(float(horizontal_angle or 0))))
elevation = max(-90, min(90, int(float(vertical_angle or 0))))
zoom_value = max(0.0, min(10.0, float(zoom or 0.0)))
direction = _camera_orbit_direction(azimuth)
elevation_label = _camera_orbit_elevation(elevation)
distance_label = _camera_orbit_distance(zoom_value, framing)
focus_label = _camera_orbit_focus(subject_focus)
pieces = [direction, elevation_label, distance_label, focus_label]
prompt = ", ".join(piece for piece in pieces if piece)
if include_degrees:
prompt = f"{azimuth}-degree {prompt}"
return prompt, {
"orbit_azimuth": azimuth,
"orbit_elevation": elevation,
"orbit_zoom": zoom_value,
"orbit_direction": direction,
"orbit_elevation_label": elevation_label,
"orbit_distance_label": distance_label,
"orbit_framing": framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom",
"orbit_focus": subject_focus if subject_focus in CAMERA_ORBIT_FOCUS_CHOICES else "auto",
}
def build_camera_orbit_config_json(
enabled: bool = True,
camera_mode: str = "standard",
horizontal_angle: int = 0,
vertical_angle: int = 0,
zoom: float = 5.0,
framing: str = "from_zoom",
subject_focus: str = "auto",
lens: str = "auto",
orientation: str = "auto",
phone_visibility: str = "auto",
priority: str = "locked",
camera_detail: str = "compact",
include_degrees: bool = True,
) -> str:
orbit_prompt, orbit_metadata = _camera_orbit_prompt(
horizontal_angle,
vertical_angle,
zoom,
framing=framing,
subject_focus=subject_focus,
include_degrees=include_degrees,
)
config = {
"camera_mode": "disabled" if _is_false(enabled) else _choice(camera_mode, CAMERA_MODE_PROMPTS, "standard"),
"shot_size": "auto",
"angle": "auto",
"lens": _choice(lens, CAMERA_LENS_PROMPTS, "auto"),
"distance": "auto",
"orientation": _choice(orientation, CAMERA_ORIENTATION_PROMPTS, "auto"),
"phone_visibility": _choice(phone_visibility, CAMERA_PHONE_PROMPTS, "auto"),
"priority": _choice(priority, CAMERA_PRIORITY_PROMPTS, "locked"),
"camera_detail": camera_detail if camera_detail in CAMERA_DETAIL_CHOICES else "compact",
"camera_source": "orbit",
"custom_camera_prompt": orbit_prompt if not _is_false(enabled) else "",
**orbit_metadata,
}
return json.dumps(config, ensure_ascii=True, sort_keys=True)
QWEN_CAMERA_DIRECTIONS = {
"front-right quarter view": 45,
"right side view": 90,
"back-right quarter view": 135,
"back view": 180,
"back-left quarter view": 225,
"left side view": 270,
"front-left quarter view": 315,
"front view": 0,
}
QWEN_CAMERA_ELEVATIONS = {
"low-angle shot": -30,
"eye-level shot": 0,
"elevated shot": 30,
"high-angle shot": 60,
}
QWEN_CAMERA_ZOOMS = {
"wide shot": 0.0,
"medium shot": 5.0,
"close-up": 8.0,
}
QWEN_CAMERA_SCENE_CENTER_Y = 0.5
def _qwen_prompt_camera_values(qwen_prompt: Any) -> tuple[int, int, float]:
text = _clean_prompt_punctuation(str(qwen_prompt or "").lower().replace(",", " "))
horizontal_angle = 0
vertical_angle = 0
zoom = 5.0
for label, value in QWEN_CAMERA_DIRECTIONS.items():
if label in text:
horizontal_angle = value
break
for label, value in QWEN_CAMERA_ELEVATIONS.items():
if label in text:
vertical_angle = value
break
for label, value in QWEN_CAMERA_ZOOMS.items():
if label in text:
zoom = value
break
return horizontal_angle, vertical_angle, zoom
def _camera_info_dict(camera_info: Any) -> dict[str, Any] | None:
if not camera_info:
return None
if isinstance(camera_info, dict):
return camera_info
if isinstance(camera_info, str):
try:
raw = json.loads(camera_info)
except json.JSONDecodeError:
return None
return raw if isinstance(raw, dict) else None
return None
def _qwen_camera_info_values(camera_info: Any) -> tuple[int, int, float] | None:
info = _camera_info_dict(camera_info)
if not info:
return None
position = info.get("position") if isinstance(info.get("position"), dict) else {}
target = info.get("target") if isinstance(info.get("target"), dict) else {}
try:
dx = float(position.get("x", 0.0)) - float(target.get("x", 0.0))
dy = float(position.get("y", QWEN_CAMERA_SCENE_CENTER_Y)) - float(
target.get("y", QWEN_CAMERA_SCENE_CENTER_Y)
)
dz = float(position.get("z", 0.0)) - float(target.get("z", 0.0))
except (TypeError, ValueError):
return None
distance = math.sqrt(dx * dx + dy * dy + dz * dz)
if distance <= 0:
return None
horizontal_angle = int(round(math.degrees(math.atan2(dx, dz)))) % 360
vertical_angle = int(round(math.degrees(math.asin(max(-1.0, min(1.0, dy / distance))))))
zoom = max(0.0, min(10.0, ((2.6 - distance) / 2.0) * 10.0))
return horizontal_angle, vertical_angle, round(zoom, 2)
def build_qwen_camera_config_json(
qwen_prompt: str = "",
camera_info: Any = None,
prefer_camera_info: bool = True,
camera_mode: str = "standard",
subject_focus: str = "auto",
lens: str = "auto",
orientation: str = "auto",
phone_visibility: str = "auto",
priority: str = "locked",
camera_detail: str = "compact",
include_degrees: bool = False,
suppress_phone_visibility: bool = True,
) -> str:
info_values = _qwen_camera_info_values(camera_info)
if prefer_camera_info and info_values is not None:
horizontal_angle, vertical_angle, zoom = info_values
source = "qwen_multiangle_camera_info"
else:
horizontal_angle, vertical_angle, zoom = _qwen_prompt_camera_values(qwen_prompt)
source = "qwen_multiangle_prompt"
config = json.loads(
build_camera_orbit_config_json(
enabled=True,
camera_mode=camera_mode,
horizontal_angle=horizontal_angle,
vertical_angle=vertical_angle,
zoom=zoom,
framing="from_zoom",
subject_focus=subject_focus,
lens=lens,
orientation=orientation,
phone_visibility="auto" if not _is_false(suppress_phone_visibility) else phone_visibility,
priority=priority,
camera_detail=camera_detail,
include_degrees=include_degrees,
)
)
config["camera_source"] = source
config["qwen_prompt"] = str(qwen_prompt or "").strip()
if info_values is not None:
config["qwen_camera_info_values"] = {
"horizontal_angle": info_values[0],
"vertical_angle": info_values[1],
"zoom": info_values[2],
}
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _choice(value: Any, choices: dict[str, str], default: str) -> str:
value = str(value or default)
return value if value in choices else default
def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
defaults = {
"camera_mode": "standard",
"shot_size": "auto",
"angle": "auto",
"lens": "auto",
"distance": "auto",
"orientation": "auto",
"phone_visibility": "auto",
"priority": "strong",
"camera_detail": "compact",
}
if not camera_config:
return defaults
if isinstance(camera_config, dict):
raw = camera_config
else:
try:
raw = json.loads(str(camera_config))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid camera_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("camera_config must be a JSON object")
parsed = {**defaults, **raw}
custom_camera_prompt = _clean_prompt_punctuation(parsed.get("custom_camera_prompt", "")).rstrip(".")
camera_source = str(parsed.get("camera_source") or "")
normalized = {
"camera_mode": _choice(parsed.get("camera_mode"), CAMERA_MODE_PROMPTS, defaults["camera_mode"]),
"shot_size": _choice(parsed.get("shot_size"), CAMERA_SHOT_PROMPTS, defaults["shot_size"]),
"angle": _choice(parsed.get("angle"), CAMERA_ANGLE_PROMPTS, defaults["angle"]),
"lens": _choice(parsed.get("lens"), CAMERA_LENS_PROMPTS, defaults["lens"]),
"distance": _choice(parsed.get("distance"), CAMERA_DISTANCE_PROMPTS, defaults["distance"]),
"orientation": _choice(parsed.get("orientation"), CAMERA_ORIENTATION_PROMPTS, defaults["orientation"]),
"phone_visibility": _choice(parsed.get("phone_visibility"), CAMERA_PHONE_PROMPTS, defaults["phone_visibility"]),
"priority": _choice(parsed.get("priority"), CAMERA_PRIORITY_PROMPTS, defaults["priority"]),
"camera_detail": str(parsed.get("camera_detail") or defaults["camera_detail"])
if str(parsed.get("camera_detail") or defaults["camera_detail"]) in CAMERA_DETAIL_CHOICES
else defaults["camera_detail"],
}
if custom_camera_prompt:
normalized["custom_camera_prompt"] = custom_camera_prompt
if camera_source:
normalized["camera_source"] = camera_source
for key in (
"orbit_azimuth",
"orbit_elevation",
"orbit_zoom",
"orbit_direction",
"orbit_elevation_label",
"orbit_distance_label",
"orbit_framing",
"orbit_focus",
):
if key in parsed:
normalized[key] = parsed[key]
return normalized
def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, Any]:
parsed = _parse_camera_config(camera_config)
if camera_mode and camera_mode != "from_camera_config":
parsed["camera_mode"] = _choice(camera_mode, CAMERA_MODE_PROMPTS, parsed["camera_mode"])
return parsed
def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, Any]]:
parsed = _parse_camera_config(camera_config)
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
return "", parsed
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
if parsed["camera_detail"] == "compact":
values = [
parsed["camera_mode"],
parsed["shot_size"],
parsed["angle"],
parsed["lens"],
parsed["distance"],
parsed["orientation"],
parsed["phone_visibility"],
]
labels = [CAMERA_COMPACT_LABELS.get(value, value.replace("_", " ")) for value in values]
labels = [label for value, label in zip(values, labels) if label and value != "auto"]
if custom_camera_prompt:
labels.append(custom_camera_prompt)
if not labels:
return "", parsed
directive = "Camera: " + ", ".join(labels) + "."
if parsed["priority"] == "locked":
directive += " Keep this camera framing."
return directive, parsed
parts = [
CAMERA_MODE_PROMPTS[parsed["camera_mode"]],
CAMERA_SHOT_PROMPTS[parsed["shot_size"]],
CAMERA_ANGLE_PROMPTS[parsed["angle"]],
CAMERA_LENS_PROMPTS[parsed["lens"]],
CAMERA_DISTANCE_PROMPTS[parsed["distance"]],
CAMERA_ORIENTATION_PROMPTS[parsed["orientation"]],
CAMERA_PHONE_PROMPTS[parsed["phone_visibility"]],
]
if custom_camera_prompt:
parts.append(f"Camera orbit: {custom_camera_prompt}.")
parts = [part for part in parts if part]
if not parts:
return "", parsed
parts.append(CAMERA_PRIORITY_PROMPTS[parsed["priority"]])
return " ".join(parts), parsed
def _insert_positive_directive(prompt: str, directive: str) -> str:
marker = " Avoid:"
if marker in prompt:
before, after = prompt.split(marker, 1)
return f"{before.rstrip()} {directive}{marker}{after}"
return f"{prompt.rstrip()} {directive}"
def _camera_caption_text(parsed: dict[str, Any]) -> str:
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
if custom_camera_prompt:
return custom_camera_prompt
camera_mode = str(parsed.get("camera_mode") or "").replace("_", " ").strip()
if not camera_mode or camera_mode == "standard":
return ""
return f"{camera_mode} camera framing"
def _is_coworking_scene(scene_text: Any) -> bool:
text = str(scene_text or "").lower()
return any(
term in text
for term in (
"coworking",
"cowork",
"office lounge",
"business cafe",
"work cafe",
"shared office",
"corporate office",
"office after hours",
"laptops",
"warm desks",
"repeating desks",
"glass partitions",
"copier alcove",
)
)
def _camera_geometry_phrase(parsed: dict[str, Any]) -> str:
direction = str(parsed.get("orbit_direction") or "").strip()
elevation = str(parsed.get("orbit_elevation_label") or "").strip()
distance = str(parsed.get("orbit_distance_label") or "").strip()
custom = str(parsed.get("custom_camera_prompt") or "").strip()
if not any((direction, elevation, distance)) and custom:
return custom
parts = [part for part in (direction, elevation, distance) if part and part != "auto"]
if parts:
return ", ".join(parts)
compact_parts = [
CAMERA_COMPACT_LABELS.get(str(parsed.get(key) or ""), str(parsed.get(key) or "").replace("_", " "))
for key in ("shot_size", "angle", "distance")
]
compact_parts = [part for part in compact_parts if part and part != "auto"]
return ", ".join(compact_parts)
def _camera_direction_from_text(text: Any) -> str:
source = str(text or "").lower()
for label in (
"front-right quarter view",
"right side view",
"back-right quarter view",
"back view",
"back-left quarter view",
"left side view",
"front-left quarter view",
"front view",
):
if label in source:
return label
return ""
def _camera_elevation_from_text(text: Any) -> str:
source = str(text or "").lower()
for label in ("low-angle shot", "eye-level shot", "elevated shot", "high-angle shot"):
if label in source:
return label
return ""
def _camera_distance_from_text(text: Any) -> str:
source = str(text or "").lower()
for label in ("wide shot", "full-body shot", "three-quarter body shot", "medium shot", "close-up", "extreme close-up"):
if label in source:
return label
return ""
def _coworking_location_profile(scene_text: Any) -> dict[str, str]:
text = str(scene_text or "").lower()
if "business cafe" in text or "work cafe" in text or "cafe" in text:
return {
"layout_label": "Business cafe camera layout",
"place": "business cafe coworking counter",
"foreground": "counter edge, laptop corner, and small plant",
"midground": "bar stools, warm desk lamps, and coffee-counter work spots",
"background": "plants, mirror strip, menu wall, and repeated cafe work tables",
}
if "corporate office" in text or "office after hours" in text or "copier" in text:
return {
"layout_label": "Office camera layout",
"place": "empty after-hours office",
"foreground": "copier alcove edge, chair backs, and nearest desk corner",
"midground": "repeating desks, glass partition seams, and muted monitor glow",
"background": "rows of empty workstations, city-light windows, and quiet office depth",
}
return {
"layout_label": "Coworking camera layout",
"place": "coworking lounge",
"foreground": "near desk edge, laptop corner, and chair back",
"midground": "warm work desks, laptop tables, and glass partition seams",
"background": "tall windows, repeated desk rows, plants, and soft shared-office depth",
}
def _coworking_subject_terms(subject_kind: str, pov_labels: list[str] | None = None) -> tuple[str, str]:
if pov_labels:
return "the visible partner", "them"
if subject_kind == "woman":
return "the woman", "her"
if subject_kind == "man":
return "the man", "him"
if subject_kind == "couple":
return "the couple", "them"
return "the subjects", "them"
def _coworking_direction_detail(
direction: str,
profile: dict[str, str],
pov_labels: list[str] | None = None,
subject_kind: str = "subjects",
) -> str:
direction = str(direction or "").strip().lower()
foreground = profile["foreground"]
midground = profile["midground"]
background = profile["background"]
subject, pronoun = _coworking_subject_terms(subject_kind, pov_labels)
if pov_labels:
if "right side" in direction:
return f"{subject} is in right-side profile; {midground} run behind {pronoun} toward {background}, with coworking details kept at the frame edges"
if "left side" in direction:
return f"{subject} is in left-side profile; {midground} run behind {pronoun} toward {background}, with coworking details kept at the frame edges"
if "back-right" in direction or "back-left" in direction:
return f"{subject} stays close in one continuous diagonal first-person body angle; {midground} lead toward {background} behind {pronoun} at the edges, not in the lower foreground"
if direction == "back view":
return f"the viewer looks past {subject}'s back toward {midground}, then into {background}; only POV body cues sit low in frame"
if "front-right" in direction or "front-left" in direction:
return f"{subject} fills the first-person front-quarter view; {midground} recede diagonally behind {pronoun} toward {background}"
return f"{subject} faces the viewer in first-person view; {midground} and {background} stay behind {pronoun}, not between viewer and body"
if "right side" in direction or "left side" in direction:
return f"{subject} is held in side profile along the {foreground}; {midground} run laterally behind {pronoun}, with {background} still readable"
if "back-right" in direction or "back-left" in direction:
return f"{subject} is viewed from a rear-quarter angle, partly turning back toward camera; the {foreground} stays low in frame while {midground} lead into {background}"
if direction == "back view":
return f"{subject} is seen from behind with the {foreground} at camera side, facing into {midground} and {background}"
if "front-right" in direction or "front-left" in direction:
return f"{subject} is placed beside the {foreground}; {midground} recede diagonally behind {pronoun} toward {background}"
return f"{subject} faces camera beside the {foreground}; {midground} sit between {pronoun} and {background}"
def _coworking_distance_detail(distance: str, profile: dict[str, str], subject_kind: str, pov_labels: list[str] | None = None) -> str:
distance = str(distance or "").strip().lower()
subject, _pronoun = _coworking_subject_terms(subject_kind, pov_labels)
if pov_labels:
if "wide" in distance or "full-body" in distance or "full body" in distance:
return f"wide POV keeps {subject} readable with coworking context behind them"
if "close" in distance:
return f"close POV keeps {subject} dominant with coworking context only at the sides or background"
return f"medium POV keeps {subject} dominant with room context behind them"
if "wide" in distance or "full-body" in distance or "full body" in distance:
return "wide crop keeps floor aisle, table rows, and window depth readable"
if "close" in distance:
return "close crop keeps one desk or counter anchor visible"
return f"medium crop keeps {subject} dominant"
def _coworking_elevation_detail(elevation: str, profile: dict[str, str], subject_kind: str, pov_labels: list[str] | None = None) -> str:
elevation = str(elevation or "").strip().lower()
subject, pronoun = _coworking_subject_terms(subject_kind, pov_labels)
if pov_labels:
if "low-angle" in elevation:
return f"low angle keeps POV body cues low while windows and partition lines rise behind {pronoun}"
if "elevated" in elevation:
return f"elevated POV keeps the viewer's eye line slightly higher than {subject}, with tabletop and glass lines only behind or at the side edges"
if "high-angle" in elevation:
return f"high angle looks down from the viewer's position with desks and aisle only in the background"
return f"eye-level angle keeps tabletop lines and glass seams behind {pronoun}"
if "low-angle" in elevation:
return f"low angle keeps the foreground desk edge low while windows and partitions rise behind {pronoun}"
if "elevated" in elevation:
return f"elevated angle shows tabletop surfaces, laptop shapes, chairs, and walking aisle around {pronoun}"
if "high-angle" in elevation:
return f"high angle shows the desk grid, chairs, floor aisle, and placement of {pronoun}"
return f"eye-level angle keeps tabletop lines and glass seams straight"
def _coworking_camera_scene_directive(
scene_text: Any,
parsed: dict[str, Any],
pov_labels: list[str] | None = None,
subject_kind: str = "subjects",
) -> str:
if not _is_coworking_scene(scene_text):
return ""
direction = str(parsed.get("orbit_direction") or "").strip()
elevation = str(parsed.get("orbit_elevation_label") or "").strip()
distance = str(parsed.get("orbit_distance_label") or "").strip()
custom_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
direction = direction or _camera_direction_from_text(custom_prompt)
elevation = elevation or _camera_elevation_from_text(custom_prompt)
distance = distance or _camera_distance_from_text(custom_prompt)
if not any((direction, elevation, distance, custom_prompt)):
return ""
profile = _coworking_location_profile(scene_text)
direction_detail = _coworking_direction_detail(direction, profile, pov_labels, subject_kind)
distance_detail = _coworking_distance_detail(distance, profile, subject_kind, pov_labels)
elevation_detail = _coworking_elevation_detail(elevation, profile, subject_kind, pov_labels)
if pov_labels:
return (
f"{profile['layout_label']} from POV: {direction_detail}. "
f"{distance_detail}; {elevation_detail}; use the multiangle camera only as first-person spatial geometry."
)
geometry = _camera_geometry_phrase(parsed)
geometry_clause = f" ({geometry})" if geometry else ""
return (
f"{profile['layout_label']}{geometry_clause}: {direction_detail}; "
f"{distance_detail}; {elevation_detail}."
)
def _coworking_composition_prompt(scene_text: Any, composition: Any, subject_kind: str = "subjects") -> str:
text = str(composition or "").strip()
if not text or not _is_coworking_scene(scene_text):
return text
lower = text.lower()
if not any(term in lower for term in ("office-lobby", "office lobby", "walking composition", "outfit-check")):
return text
subject, _pronoun = _coworking_subject_terms(subject_kind)
if subject_kind == "woman":
return "coworking lounge selfie frame with the woman near a desk edge and tall-window depth behind her"
if subject_kind == "man":
return "coworking lounge portrait frame with the man near a desk edge and tall-window depth behind him"
return f"coworking lounge frame with {subject} near a desk edge and tall-window depth behind them"
def _apply_coworking_composition(row: dict[str, Any], subject_kind: str) -> dict[str, Any]:
scene_text = row.get("scene_text") or row.get("source_scene_text") or row.get("scene")
old_composition = str(row.get("composition") or "").strip()
new_composition = _coworking_composition_prompt(scene_text, old_composition, subject_kind)
if not old_composition or new_composition == old_composition:
return row
row["source_composition"] = row.get("source_composition") or old_composition
row["composition"] = new_composition
row["composition_prompt"] = _composition_prompt(new_composition)
prompt = str(row.get("prompt") or "")
replacements = (
(f"Composition: vertical {old_composition}.", f"Composition: {_composition_prompt(new_composition)}."),
(f"Composition: {old_composition}.", f"Composition: {_composition_prompt(new_composition)}."),
(f"Framed as {old_composition}.", f"Framed as {new_composition}."),
)
for old_fragment, new_fragment in replacements:
if old_fragment in prompt:
row["prompt"] = prompt.replace(old_fragment, new_fragment)
break
row["caption"] = str(row.get("caption") or "").replace(f", {old_composition},", f", {new_composition},")
return row
def _camera_scene_directive_for_context(
scene_text: Any,
composition: Any,
camera_config: str | dict[str, Any] | None,
pov_labels: list[str] | None = None,
subject_kind: str = "subjects",
) -> tuple[str, dict[str, Any]]:
parsed = _parse_camera_config(camera_config)
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
return "", parsed
return _coworking_camera_scene_directive(scene_text, parsed, pov_labels, subject_kind), parsed
def _row_camera_subject_kind(row: dict[str, Any]) -> str:
subject_type = str(row.get("subject_type") or row.get("primary_subject") or "").lower()
if subject_type in ("woman", "adult woman") or subject_type == "single_any":
return "woman"
if subject_type in ("man", "adult man"):
return "man"
try:
women_count = int(row.get("women_count") or 0)
men_count = int(row.get("men_count") or 0)
except (TypeError, ValueError):
women_count = men_count = 0
if women_count == 1 and men_count == 0:
return "woman"
if women_count == 0 and men_count == 1:
return "man"
if women_count + men_count == 2:
return "couple"
return "subjects"
def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
directive, parsed = _camera_directive(camera_config)
pov_labels = _pov_character_labels(
_character_slot_label_map(_parse_character_cast(row.get("character_cast_slots"))),
int(row.get("men_count") or 0) if str(row.get("men_count") or "").isdigit() else 0,
)
if not pov_labels:
pov_labels = [str(label) for label in _list_from(row.get("pov_character_labels")) if str(label).strip()]
subject_kind = _row_camera_subject_kind(row)
row = _apply_coworking_composition(row, subject_kind)
scene_directive, parsed = _camera_scene_directive_for_context(
row.get("scene_text") or row.get("source_scene_text") or row.get("scene"),
row.get("composition") or row.get("source_composition"),
parsed,
pov_labels,
subject_kind,
)
row["camera_config"] = parsed
row["camera_scene_directive"] = scene_directive
row["camera_directive"] = "" if pov_labels else directive
combined_directive = " ".join(part for part in (scene_directive, row["camera_directive"]) if part)
if not combined_directive:
return row
row["prompt"] = _insert_positive_directive(row["prompt"], combined_directive)
camera_caption = _camera_caption_text(parsed)
if camera_caption and not pov_labels:
row["caption"] = f"{row.get('caption', '').rstrip()}, {camera_caption}"
return row
def _row_seed(seed: int, row_number: int, salt: int = 0) -> int:
return int(seed) + int(row_number) * 1009 + salt * 9176
def _pick_clothing_mode(rng: random.Random, clothing: str, minimal_ratio: float | None) -> str:
if clothing == "random":
return "minimal" if rng.random() < 0.5 else "full"
if minimal_ratio is None:
return clothing
return "minimal" if rng.random() < minimal_ratio else "full"
def _pick_pose_mode(rng: random.Random, poses: str, standard_ratio: float | None) -> str:
if poses == "random":
return "standard" if rng.random() < 0.5 else "evocative"
if standard_ratio is None:
return poses
return "standard" if rng.random() < standard_ratio else "evocative"
def _pick_figure_bias(rng: random.Random, figure: str) -> str:
if figure in ("curvy", "balanced", "bombshell"):
return figure
return g.choose(rng, ["curvy", "balanced", "bombshell"])
def _pick_expression_intensity(rng: random.Random, expression_intensity: Any) -> tuple[float, str]:
try:
value = float(expression_intensity)
except (TypeError, ValueError):
return 0.5, "default"
if value < 0:
return round(rng.random(), 2), "random"
return _clamped_float(value, 0.5), "input"
def _build_auto_weighted_row(
row_number: int,
start_index: int,
clothing: str,
ethnicity: str,
poses: str,
backside_bias: float,
figure: str,
no_plus_women: bool,
no_black: bool,
minimal_clothing_ratio: float | None,
standard_pose_ratio: float | None,
seed: int,
) -> dict[str, Any]:
batch_number = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
rows = g.build_rows(
batch_number * g.BATCH_SIZE,
start_index,
clothing,
ethnicity,
poses,
backside_bias,
figure,
no_plus_women,
no_black,
minimal_clothing_ratio,
standard_pose_ratio,
seed,
g.EXPRESSION_SEED + seed,
)
row = rows[row_number - 1]
row["main_category"] = "auto_weighted"
row["subcategory"] = row.get("primary_subject", "auto")
row["source"] = "built_in_generator"
return row
def _build_direct_builtin_row(
category: str,
row_number: int,
start_index: int,
clothing: str,
ethnicity: str,
poses: str,
backside_bias: float,
figure: str,
no_plus_women: bool,
no_black: bool,
minimal_clothing_ratio: float | None,
standard_pose_ratio: float | None,
seed: int,
) -> dict[str, Any]:
rng = random.Random(_row_seed(seed, row_number))
expr_deck = g.ExpressionDeck(g.EXPRESSIONS, random.Random(_row_seed(g.EXPRESSION_SEED + seed, row_number)))
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
index = start_index + row_number - 1
row_clothing = _pick_clothing_mode(rng, clothing, minimal_clothing_ratio)
row_poses = _pick_pose_mode(rng, poses, standard_pose_ratio)
if category == "woman":
row = g.make_single(
index,
batch,
rng,
"woman",
expr_deck,
row_clothing,
ethnicity,
row_poses,
backside_bias,
figure,
no_plus_women,
no_black,
)
elif category == "man":
row = g.make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_poses, backside_bias, figure)
elif category == "couple":
row = g.make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
elif category == "group_or_layout":
row = g.make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
else:
raise ValueError(f"Unknown built-in category: {category}")
row["main_category"] = category
row["subcategory"] = row.get("pose_mode", category)
row["source"] = "built_in_generator"
return row
def _auto_full_choice(seed_config: dict[str, int], seed: int, row_number: int) -> str:
categories = load_category_library()
if not categories:
return "auto_weighted"
category_rng = _axis_rng(seed_config, "category", seed, row_number)
choices: list[dict[str, Any]] = [{"category": "auto_weighted", "weight": 1.0}]
choices.extend(
{
"category": category["name"],
"weight": category.get("weight", 1.0),
}
for category in categories
)
choice = _weighted_choice(category_rng, choices)
return str(choice.get("category") or "auto_weighted")
def _find_category(categories: list[dict[str, Any]], name_or_slug: str) -> dict[str, Any] | None:
wanted = name_or_slug.strip().lower()
for category in categories:
if category["name"].lower() == wanted or category["slug"].lower() == wanted:
return category
return None
def _base_cast_counts(women_count: int, men_count: int) -> tuple[int, int]:
women_count = max(0, int(women_count))
men_count = max(0, int(men_count))
if women_count + men_count == 0:
women_count = 1
return women_count, men_count
def _counts_for_exact_subcategory(
subcategory: dict[str, Any],
women_count: int,
men_count: int,
) -> tuple[int, int]:
women_count, men_count = _base_cast_counts(women_count, men_count)
min_women = _constraint_int(subcategory, "min_women")
if min_women is not None and women_count < min_women:
women_count = min_women
min_men = _constraint_int(subcategory, "min_men")
if min_men is not None and men_count < min_men:
men_count = min_men
min_people = _constraint_int(subcategory, "min_people")
if min_people is not None:
missing = min_people - (women_count + men_count)
if missing > 0:
if women_count > 0 or men_count == 0:
women_count += missing
else:
men_count += missing
return women_count, men_count
def _find_subcategory(
categories: list[dict[str, Any]],
category_choice: str,
subcategory_choice: str,
category_rng: random.Random,
subcategory_rng: random.Random,
women_count: int = 1,
men_count: int = 1,
) -> tuple[dict[str, Any], dict[str, Any], int, int]:
women_count, men_count = _base_cast_counts(women_count, men_count)
if subcategory_choice and subcategory_choice != RANDOM_SUBCATEGORY and " / " in subcategory_choice:
category_name, subcategory_name = subcategory_choice.split(" / ", 1)
category = _find_category(categories, category_name)
if not category:
raise ValueError(f"Unknown category in subcategory picker: {category_name}")
wanted = subcategory_name.strip().lower()
for subcategory in category["subcategories"]:
if subcategory["name"].lower() == wanted or subcategory["slug"].lower() == wanted:
adjusted_women_count, adjusted_men_count = _counts_for_exact_subcategory(
subcategory,
women_count,
men_count,
)
if not _compatible_entry(subcategory, adjusted_women_count, adjusted_men_count):
raise ValueError(
f"Subcategory '{subcategory['name']}' is not compatible with "
f"women_count={women_count}, men_count={men_count}"
)
return category, subcategory, adjusted_women_count, adjusted_men_count
raise ValueError(f"Unknown subcategory '{subcategory_name}' for category '{category_name}'")
if category_choice == "custom_random":
if not categories:
raise ValueError("No custom categories found in categories/*.json")
category = _weighted_choice(category_rng, categories)
else:
category = _find_category(categories, category_choice)
if not category:
raise ValueError(f"Unknown custom category: {category_choice}")
subcategories = _compatible_entries(category["subcategories"], women_count, men_count)
subcategory = _weighted_choice(subcategory_rng, subcategories)
return category, subcategory, women_count, men_count
def _merged_field(category: dict[str, Any], subcategory: dict[str, Any], item: Any, key: str, default: Any = None) -> Any:
if isinstance(item, dict) and key in item:
return item[key]
if key in subcategory:
return subcategory[key]
if key in category:
return category[key]
return default
def _body_phrase(body: Any, figure_note: Any = "") -> str:
body = str(body or "").strip()
figure_note = str(figure_note or "").strip()
if not body:
return figure_note
if not figure_note:
return f"{body} figure"
if "figure" in figure_note.lower():
return f"{body} build and {figure_note}"
return f"{body} figure with {figure_note}"
def _safe_profile_name(profile_name: str) -> str:
profile_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(profile_name or "").strip()).strip("_")
return profile_name[:64] or "profile"
def _profile_path(profile_name: str) -> Path:
return PROFILE_DIR / f"{_safe_profile_name(profile_name)}.json"
def character_profile_choices() -> list[str]:
if not PROFILE_DIR.exists():
return ["manual"]
names = sorted(path.stem for path in PROFILE_DIR.glob("*.json") if path.is_file())
return ["manual"] + names
def _load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[str, Any]:
if not value:
return {}
if isinstance(value, dict):
return value
try:
raw = json.loads(str(value))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid {label} JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError(f"{label} must be a JSON object")
return raw
CHARACTER_MANUAL_FIELDS = (
"manual_age",
"manual_body",
"body_phrase",
"skin",
"hair",
"eyes",
"softcore_outfit",
"hardcore_clothing",
)
def _parse_character_manual_config(value: str | dict[str, Any] | None) -> dict[str, str]:
if not value:
return {}
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return {}
if not isinstance(raw, dict):
return {}
return {
key: str(raw.get(key) or "").strip()
for key in CHARACTER_MANUAL_FIELDS
if str(raw.get(key) or "").strip()
}
def _character_manual_summary(config: dict[str, str]) -> str:
parts = [f"{key}={value}" for key, value in config.items() if value]
return "; ".join(parts) if parts else "manual unrestricted"
def build_character_manual_config_json(
manual: str | dict[str, Any] | None = "",
combine_mode: str = "merge_nonempty",
manual_age: str = "",
manual_body: str = "",
body_phrase: str = "",
skin: str = "",
hair: str = "",
eyes: str = "",
softcore_outfit: str = "",
hardcore_clothing: str = "",
) -> str:
base = {} if combine_mode == "replace_all" else _parse_character_manual_config(manual)
updates = {
"manual_age": manual_age,
"manual_body": manual_body,
"body_phrase": body_phrase,
"skin": skin,
"hair": hair,
"eyes": eyes,
"softcore_outfit": softcore_outfit,
"hardcore_clothing": hardcore_clothing,
}
for key, value in updates.items():
value = str(value or "").strip()
if value:
base[key] = value
result = {"config_type": "character_manual", **base}
result["summary"] = _character_manual_summary(base)
return json.dumps(result, ensure_ascii=True, sort_keys=True)
def _slot_value(value: Any) -> str:
text = str(value or "").strip()
if text.lower() in CHARACTER_RANDOM_TOKENS:
return ""
return text
CHARACTER_CHARACTERISTIC_AXES = {
"ages": CHARACTER_AGE_CHOICES,
"bodies": list(dict.fromkeys([*CHARACTER_BODY_CHOICES, *CHARACTER_WOMAN_BODY_CHOICES, *CHARACTER_MAN_BODY_CHOICES])),
"eyes": CHARACTER_EYE_COLOR_CHOICES,
}
def _empty_characteristics_config() -> dict[str, Any]:
return {
"config_type": "characteristics",
"ages": [],
"bodies": [],
"eyes": [],
"softcore_outfits": [],
"hardcore_clothing": [],
}
def _normalize_characteristic_choice(value: Any, choices: list[str] | tuple[str, ...]) -> str:
text = str(value or "").strip()
if not text:
return ""
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
for choice in choices:
if normalized == re.sub(r"[^a-z0-9]+", "_", str(choice).lower()).strip("_"):
return str(choice)
return ""
def _normalize_characteristic_values(
values: Any,
choices: list[str] | tuple[str, ...] | None = None,
*,
allow_free_text: bool = False,
) -> list[str]:
if isinstance(values, str):
raw_values = [part.strip() for part in re.split(r"[\n;]+", values) if part.strip()]
if len(raw_values) == 1 and "," in raw_values[0] and not allow_free_text:
raw_values = [part.strip() for part in raw_values[0].split(",") 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 choices is None else _normalize_characteristic_choice(raw_value, choices)
if not value or value in ("random", "manual"):
continue
if value not in normalized:
normalized.append(value)
return normalized
def _parse_characteristics_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return _empty_characteristics_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return _empty_characteristics_config()
if not isinstance(raw, dict):
return _empty_characteristics_config()
return {
"config_type": "characteristics",
"ages": _normalize_characteristic_values(raw.get("ages"), CHARACTER_AGE_CHOICES),
"bodies": _normalize_characteristic_values(raw.get("bodies"), CHARACTER_CHARACTERISTIC_AXES["bodies"]),
"eyes": _normalize_characteristic_values(raw.get("eyes"), CHARACTER_EYE_COLOR_CHOICES),
"softcore_outfits": _normalize_characteristic_values(raw.get("softcore_outfits"), None, allow_free_text=True),
"hardcore_clothing": _normalize_characteristic_values(raw.get("hardcore_clothing"), None, allow_free_text=True),
}
def _characteristics_summary(config: dict[str, Any]) -> str:
parts = []
for key, label in (
("ages", "ages"),
("bodies", "bodies"),
("eyes", "eyes"),
("softcore_outfits", "soft_outfits"),
("hardcore_clothing", "hard_clothing"),
):
values = config.get(key) or []
if not values:
continue
if key in ("softcore_outfits", "hardcore_clothing"):
parts.append(f"{label}={len(values)}")
else:
parts.append(f"{label}={','.join(values)}")
return "; ".join(parts) if parts else "characteristics unrestricted"
def build_characteristics_config_json(
characteristics: str | dict[str, Any] | None = "",
axis: str = "ages",
selected_values: list[str] | tuple[str, ...] | str | None = None,
combine_mode: str = "replace_axis",
) -> str:
config = _parse_characteristics_config(characteristics)
axis_key = str(axis or "").strip().lower()
if axis_key not in config:
config["summary"] = _characteristics_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
choices = CHARACTER_CHARACTERISTIC_AXES.get(axis_key)
values = _normalize_characteristic_values(
selected_values,
choices,
allow_free_text=choices is None,
)
if combine_mode == "add_to_axis":
existing = list(config.get(axis_key) or [])
for value in values:
if value not in existing:
existing.append(value)
config[axis_key] = existing
else:
config[axis_key] = values
config["summary"] = _characteristics_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _characteristic_choice(config: dict[str, Any], key: str, rng: random.Random) -> str:
values = config.get(key) or []
return g.choose(rng, values) if values else ""
def _eye_phrase_from_key(key: str) -> str:
return {
"blue": "blue eyes",
"pale_blue": "pale blue eyes",
"ice_blue": "ice blue eyes",
"blue_gray": "blue-gray eyes",
"green": "green eyes",
"emerald_green": "emerald green eyes",
"hazel": "hazel eyes",
"light_hazel": "light hazel eyes",
"green_hazel": "green-hazel eyes",
"amber": "amber eyes",
"amber_brown": "amber-brown eyes",
"honey_brown": "honey-brown eyes",
"brown": "brown eyes",
"deep_brown": "deep brown eyes",
"dark_brown": "dark brown eyes",
"dark": "dark eyes",
"gray": "gray eyes",
"gray_brown": "gray-brown eyes",
}.get(key, "")
def _normalize_descriptor_detail(value: Any) -> str:
text = str(value or "auto").strip()
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
def _normalize_presence_mode(value: Any, subject_type: str) -> str:
text = str(value or "visible").strip().lower()
if text not in CHARACTER_PRESENCE_CHOICES:
text = "visible"
if subject_type != "man":
return "visible"
return text
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
if not slot:
return False
return slot.get("subject_type") == "man" and slot.get("presence_mode") == "pov"
def _normalize_slot_expression_intensity(value: Any) -> float:
try:
intensity = float(value)
except (TypeError, ValueError):
return -1.0
if intensity < 0:
return -1.0
return _clamped_float(intensity, 0.5)
def _slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
if not slot:
return True
return not _is_false(slot.get("expression_enabled", True))
def _slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
if not slot or not _slot_expression_enabled(slot):
return None
intensity = _normalize_slot_expression_intensity(slot.get("expression_intensity"))
return intensity if intensity >= 0 else None
def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
if not slot or not _slot_expression_enabled(slot):
return None
phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else ""
if phase_key:
intensity = _normalize_slot_expression_intensity(slot.get(phase_key))
if intensity >= 0:
return intensity
return _slot_expression_intensity(slot)
def _normalize_slot_seed(value: Any) -> int:
try:
seed = int(value)
except (TypeError, ValueError):
return -1
if seed < 0:
return -1
return min(seed, CHARACTER_SLOT_SEED_MAX)
def _slot_seed(slot: dict[str, Any] | None) -> int:
if not slot:
return -1
return _normalize_slot_seed(slot.get("slot_seed"))
def _slot_seeded_rng(slot: dict[str, Any] | None, salt: int) -> random.Random | None:
seed = _slot_seed(slot)
if seed < 0:
return None
return random.Random(_row_seed(seed, 1, salt))
def _slot_context_rng(slot: dict[str, Any], fallback_rng: random.Random) -> random.Random:
return _slot_seeded_rng(slot, 701) or fallback_rng
def _slot_effective_figure(
slot: dict[str, Any],
subject_type: str,
fallback_figure: str,
) -> str:
raw_figure = str(slot.get("figure") or "random").strip()
if raw_figure in ("curvy", "balanced", "bombshell"):
return raw_figure
seeded_rng = _slot_seeded_rng(slot, 709)
if subject_type == "woman" and seeded_rng is not None:
return g.choose(seeded_rng, ["curvy", "balanced", "bombshell"])
return fallback_figure
def _mean(values: list[float]) -> float:
return sum(values) / len(values)
def _cast_expression_intensity_override(
fallback: float,
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
expression_phase: str = "",
) -> tuple[float | None, str]:
groups: list[tuple[str, list[str]]] = [
("women", [f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))]),
("men", [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]),
]
all_values: list[float] = []
matching_slots: list[dict[str, Any]] = []
for group_name, labels in groups:
values: list[float] = []
value_labels: list[str] = []
for label in labels:
slot = label_map.get(label)
if _slot_is_pov(slot):
continue
if slot:
matching_slots.append(slot)
value = _slot_expression_intensity_for_phase(slot, expression_phase)
if value is not None:
values.append(value)
value_labels.append(label)
all_values.append(value)
if values:
if len(values) == 1:
return values[0], f"character_slot:{value_labels[0]}"
return _mean(values), f"character_slots:{group_name}"
if all_values:
return _mean(all_values), "character_slots:cast"
if matching_slots and all(not _slot_expression_enabled(slot) for slot in matching_slots):
return None, "character_slots:disabled"
return fallback, "input"
def _character_expression_entries(
rng: random.Random,
expression_pool: list[Any],
fallback_intensity: float,
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
expression_phase: str = "",
) -> list[str]:
labels = [
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
]
expressions: list[str] = []
used: set[str] = set()
for label in labels:
slot = label_map.get(label)
if not slot:
continue
if _slot_is_pov(slot):
continue
if not _slot_expression_enabled(slot):
continue
intensity = _slot_expression_intensity_for_phase(slot, expression_phase)
if intensity is None:
intensity = fallback_intensity
entries = _compatible_entries(
_expression_entries_for_intensity(expression_pool, intensity),
women_count,
men_count,
)
if not entries:
continue
choice = ""
for _attempt in range(5):
candidate = _choose_text(rng, entries)
if candidate not in used:
choice = candidate
break
if not choice:
choice = _choose_text(rng, entries)
used.add(choice)
expressions.append(f"{label} has {choice}")
return expressions
def _sanitize_character_expression_text_for_action(
expression_text: str,
role_graph: Any,
item: Any,
axis_values: Any = None,
) -> str:
text = str(expression_text or "").strip()
if not text:
return ""
context = " ".join(
str(part or "").lower()
for part in (
role_graph,
item,
*((axis_values or {}).values() if isinstance(axis_values, dict) else ()),
)
)
woman_active_outercourse = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"boobjob",
"titjob",
"breast sex",
"breasts tightly",
"testicle",
"balls-licking",
"balls licking",
"penis-licking",
"penis licking",
"handjob",
"hand job",
"footjob",
)
)
)
woman_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"takes man",
"penis in her mouth",
"mouth at penis level",
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"lips wrapped",
)
)
)
man_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"mouth on her pussy",
"mouth on woman",
"mouth pressed to her pussy",
"cunnilingus",
"pussy licking",
"tongue on pussy",
)
)
)
mouth_expression_terms = ("mouth", "oral", "tongue", "lips", "gagging", "saliva")
clauses = [clause.strip() for clause in text.split(";") if clause.strip()]
if woman_active_outercourse:
clauses = [clause for clause in clauses if not re.match(r"^Man [A-Z] has\b", clause)]
if woman_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Man [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
if man_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Woman [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
return "; ".join(clauses)
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
detail = _normalize_descriptor_detail(descriptor_detail)
if detail != "auto":
return detail
return "compact" if str(subject or "").strip().lower() == "man" else "full"
def _descriptor_from_parts(
subject: Any,
age: Any,
body_phrase: Any,
skin: Any,
hair: Any,
eyes: Any,
descriptor_detail: Any = "auto",
) -> str:
subject = str(subject or "person").strip() or "person"
age_text = " ".join(str(age or "").strip().split())
age_text = age_text.removesuffix(" adults").removesuffix(" adult").strip()
if age_text in ("adult", "adults"):
age_text = ""
subject_phrase = f"{age_text} adult {subject}".strip() if age_text else f"adult {subject}"
detail = _descriptor_detail_for_subject(subject, descriptor_detail)
detail_map = {
"minimal": (body_phrase,),
"compact": (body_phrase, skin),
"medium": (body_phrase, skin, hair),
"full": (body_phrase, skin, hair, eyes),
}
pieces = [subject_phrase, *detail_map.get(detail, detail_map["full"])]
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
choice = str(choice or "").strip()
manual_value = str(manual_value or "").strip()
if choice == "manual":
return manual_value or "random"
if choice.lower() in CHARACTER_RANDOM_TOKENS:
return "random"
return choice
def _normalize_slot_ethnicity(value: Any) -> str:
return normalize_ethnicity_filter(value, "random", allow_random=True)
def _normalize_hair_choice(value: Any, choices: list[str]) -> str:
text = str(value or "random").strip().lower().replace("-", "_").replace(" ", "_")
return text if text in choices else "random"
def _infer_hair_color_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("platinum_blonde", ("platinum-blonde", "platinum blonde", "platinum")),
("strawberry_blonde", ("strawberry-blonde", "strawberry blonde")),
("honey_blonde", ("honey-blonde", "honey blonde")),
("ash_blonde", ("ash-blonde", "ash blonde")),
("dark_blonde", ("dark-blonde", "dark blonde")),
(
"blonde",
(
"light-blonde",
"light blonde",
"blonde",
"flaxen",
"wheat-blonde",
"wheat blonde",
"beige-blonde",
"beige blonde",
"sandy-blonde",
"sandy blonde",
),
),
("silver_gray", ("silver-gray", "silver grey", "silver", "gray", "grey")),
("dark_brown", ("dark-brown", "dark brown", "espresso")),
("chestnut", ("chestnut",)),
("auburn", ("auburn",)),
("copper", ("copper",)),
("red", ("red hair", "redhead")),
("black", ("black",)),
("brown", ("brown", "brunette", "caramel")),
("white", ("white",)),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def _infer_hair_length_key(text: Any) -> str:
value = str(text or "").lower()
if any(token in value for token in ("very long", "waist-length", "hip-length")):
return "very_long"
if "long" in value:
return "long"
if "shoulder-length" in value or "shoulder length" in value:
return "shoulder_length"
if "medium-length" in value or "medium length" in value:
return "medium"
if any(token in value for token in ("bob", "lob")):
return "bob_lob"
if any(token in value for token in ("pixie", "short", "cropped", "tapered")):
return "short"
if any(token in value for token in ("bun", "updo")):
return "updo"
return "random"
def _infer_hair_style_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("pixie_cut", ("pixie",)),
("messy_bun", ("messy bun",)),
("bun", ("bun", "updo")),
("ponytail", ("ponytail",)),
("braids", ("braids", "box braids", "cornrow")),
("braid", ("braid",)),
("locs", ("locs", "dreadlocks")),
("twists", ("twists",)),
("afro", ("afro",)),
("natural_curls", ("natural curls", "natural coils", "coils")),
("tight_curls", ("tight curls", "tight coils")),
("curls", ("curls", "curly")),
("loose_waves", ("loose waves",)),
("waves", ("waves", "wavy")),
("lob", ("lob",)),
("bob", ("bob",)),
("shag", ("shag",)),
("wet_hair", ("wet hair", "damp hair")),
("slicked_back", ("slicked-back", "slicked back")),
("straight", ("straight", "sleek")),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def _choose_hair_key(rng: random.Random, choices: list[str]) -> str:
pool = [choice for choice in choices if choice != "random"]
return g.choose(rng, pool) if pool else "random"
def _normalize_hair_values(values: Any, choices: list[str]) -> 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 value in raw_values:
key = _normalize_hair_choice(value, choices)
if key != "random" and key not in normalized:
normalized.append(key)
return normalized
def _empty_hair_config() -> dict[str, Any]:
return {"config_type": "hair_characteristics", "colors": [], "lengths": [], "styles": []}
def _parse_hair_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return _empty_hair_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return _empty_hair_config()
if not isinstance(raw, dict):
return _empty_hair_config()
return {
"config_type": "hair_characteristics",
"colors": _normalize_hair_values(raw.get("colors"), CHARACTER_HAIR_COLOR_CHOICES),
"lengths": _normalize_hair_values(raw.get("lengths"), CHARACTER_HAIR_LENGTH_CHOICES),
"styles": _normalize_hair_values(raw.get("styles"), CHARACTER_HAIR_STYLE_CHOICES),
}
def _hair_config_summary(config: dict[str, Any]) -> str:
parts = []
for label, key in (("colors", "colors"), ("lengths", "lengths"), ("styles", "styles")):
values = config.get(key) or []
if values:
parts.append(f"{label}={','.join(values)}")
return "; ".join(parts) if parts else "hair unrestricted"
def build_hair_config_json(
hair_config: str | dict[str, Any] | None = "",
axis: str = "color",
selected_values: list[str] | tuple[str, ...] | str | None = None,
combine_mode: str = "replace_axis",
) -> str:
config = _parse_hair_config(hair_config)
axis_key = {"color": "colors", "length": "lengths", "style": "styles"}.get(str(axis or "").strip().lower())
choice_map = {
"colors": CHARACTER_HAIR_COLOR_CHOICES,
"lengths": CHARACTER_HAIR_LENGTH_CHOICES,
"styles": CHARACTER_HAIR_STYLE_CHOICES,
}
if axis_key:
values = _normalize_hair_values(selected_values, choice_map[axis_key])
if combine_mode == "add_to_axis":
existing = list(config.get(axis_key) or [])
for value in values:
if value not in existing:
existing.append(value)
config[axis_key] = existing
else:
config[axis_key] = values
config["summary"] = _hair_config_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _hair_color_text(key: str) -> str:
return {
"black": "black",
"brown": "brown",
"dark_brown": "dark-brown",
"chestnut": "chestnut",
"auburn": "auburn",
"copper": "copper",
"red": "red",
"blonde": "blonde",
"platinum_blonde": "platinum-blonde",
"ash_blonde": "ash-blonde",
"honey_blonde": "honey-blonde",
"strawberry_blonde": "strawberry-blonde",
"dark_blonde": "dark-blonde",
"silver_gray": "silver-gray",
"white": "white",
}.get(key, "brown")
def _hair_length_text(key: str) -> str:
return {
"very_short": "very short",
"short": "short",
"bob_lob": "",
"shoulder_length": "shoulder-length",
"medium": "medium-length",
"long": "long",
"very_long": "very long",
"updo": "",
}.get(key, "")
def _hair_phrase_from_parts(color_key: str, length_key: str, style_key: str) -> str:
color = _hair_color_text(color_key)
length = _hair_length_text(length_key)
prefix = " ".join(part for part in (length, color) if part)
if style_key == "pixie_cut":
return f"short {color} pixie cut"
if style_key == "bob":
return f"{color} bob" if length_key in ("random", "bob_lob", "short") else f"{prefix} bob"
if style_key == "lob":
return f"shoulder-length {color} lob" if length_key in ("random", "bob_lob") else f"{prefix} lob"
if style_key == "shag":
return f"{prefix or color} shag"
if style_key == "ponytail":
return f"{prefix or color} ponytail"
if style_key == "braid":
return f"{prefix or color} braid"
if style_key == "braids":
return f"{prefix or color} braids"
if style_key == "bun":
return f"{prefix} hair in a bun" if length else f"{color} bun"
if style_key == "messy_bun":
return f"{prefix} hair in a messy bun" if length else f"messy {color} bun"
if style_key == "locs":
return f"{prefix or color} locs"
if style_key == "twists":
return f"{prefix or color} twists"
if style_key == "afro":
return f"{color} afro"
if style_key == "natural_curls":
return f"{prefix or color} natural curls"
if style_key == "wet_hair":
return f"{prefix or color} wet hair"
if style_key == "slicked_back":
return f"slicked-back {color} hair"
if style_key == "straight":
return f"{prefix or color} straight hair"
if style_key == "loose_waves":
return f"{prefix or color} loose waves"
if style_key == "tight_curls":
return f"{prefix or color} tight curls"
if style_key == "curls":
return f"{prefix or color} curls"
return f"{prefix or color} waves"
def _hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random.Random) -> str:
hair_config = _parse_hair_config(slot.get("hair_config"))
color_choice = _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES)
length_choice = _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES)
style_choice = _normalize_hair_choice(slot.get("hair_style"), CHARACTER_HAIR_STYLE_CHOICES)
color_options = hair_config.get("colors") or []
length_options = hair_config.get("lengths") or []
style_options = hair_config.get("styles") or []
if (
color_choice == "random"
and length_choice == "random"
and style_choice == "random"
and not color_options
and not length_options
and not style_options
):
return ""
if color_choice != "random":
color_key = color_choice
elif color_options:
color_key = g.choose(rng, color_options)
else:
color_key = _infer_hair_color_key(base_hair)
if length_choice != "random":
length_key = length_choice
elif length_options:
length_key = g.choose(rng, length_options)
else:
length_key = _infer_hair_length_key(base_hair)
if style_choice != "random":
style_key = style_choice
elif style_options:
style_key = g.choose(rng, style_options)
else:
style_key = _infer_hair_style_key(base_hair)
if color_key == "random":
color_key = _choose_hair_key(rng, CHARACTER_HAIR_COLOR_CHOICES)
if length_key == "random":
length_key = _choose_hair_key(rng, CHARACTER_HAIR_LENGTH_CHOICES)
if style_key == "random":
style_key = _choose_hair_key(rng, CHARACTER_HAIR_STYLE_CHOICES)
if length_key == "updo" and style_key not in ("ponytail", "braid", "braids", "bun", "messy_bun", "locs", "twists"):
style_key = g.choose(rng, ["ponytail", "braid", "bun", "messy_bun"])
return _hair_phrase_from_parts(color_key, length_key, style_key)
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
if subject_type not in ("woman", "man"):
subject_type = "woman"
label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
if label == "AUTO_CHAIN":
label = "auto_chain"
if label not in CHARACTER_LABEL_CHOICES:
label = "auto_chain"
manual_config = _parse_character_manual_config(slot.get("manual") or slot.get("manual_config"))
raw_age = str(slot.get("age") or "random")
raw_manual_age = str(slot.get("manual_age") or "").strip()
if not raw_manual_age and manual_config.get("manual_age"):
raw_manual_age = manual_config["manual_age"]
if raw_age.lower() in CHARACTER_RANDOM_TOKENS:
raw_age = "manual"
age = _slot_manual_or_choice(raw_age, raw_manual_age)
raw_body = str(slot.get("body") or "random")
raw_manual_body = str(slot.get("manual_body") or "").strip()
if not raw_manual_body and manual_config.get("manual_body"):
raw_manual_body = manual_config["manual_body"]
if raw_body.lower() in CHARACTER_RANDOM_TOKENS:
raw_body = "manual"
body = _slot_manual_or_choice(raw_body, raw_manual_body)
figure = str(slot.get("figure") or "random").strip()
if figure not in character_figure_choices():
figure = "random"
def manual_fallback(field: str) -> str:
direct = _slot_value(slot.get(field))
return direct or manual_config.get(field, "")
normalized = {
"profile_type": "character_slot",
"subject_type": subject_type,
"label": label,
"slot_seed": _normalize_slot_seed(slot.get("slot_seed")),
"age": age,
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
"figure": figure,
"body": body,
"body_phrase": manual_fallback("body_phrase"),
"skin": manual_fallback("skin"),
"hair": manual_fallback("hair"),
"manual": manual_config,
"characteristics": (
slot.get("characteristics")
if isinstance(slot.get("characteristics"), dict)
else _slot_value(slot.get("characteristics") or slot.get("characteristics_config"))
),
"hair_config": (
slot.get("hair_config")
if isinstance(slot.get("hair_config"), dict)
else _slot_value(slot.get("hair_config"))
),
"hair_color": _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES),
"hair_length": _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES),
"hair_style": _normalize_hair_choice(slot.get("hair_style"), CHARACTER_HAIR_STYLE_CHOICES),
"eyes": manual_fallback("eyes"),
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
"presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type),
"softcore_outfit": manual_fallback("softcore_outfit"),
"hardcore_clothing": (
_slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit"))
or manual_config.get("hardcore_clothing", "")
),
"expression_enabled": not _is_false(slot.get("expression_enabled", True)),
"expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")),
"softcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
"hardcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")),
}
normalized["summary"] = _character_slot_summary(normalized)
return normalized
def _parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
if not character_cast:
return []
if isinstance(character_cast, list):
raw = character_cast
elif isinstance(character_cast, dict):
raw = character_cast
else:
try:
raw = json.loads(str(character_cast))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid character_cast JSON: {exc}") from exc
if isinstance(raw, list):
slots = raw
elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
slots = raw["slots"]
elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
slots = [raw]
elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
slots = [raw]
else:
return []
return [_normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]
def _character_slot_summary(slot: dict[str, Any]) -> str:
subject = str(slot.get("subject_type") or "woman")
label = str(slot.get("label") or "auto_chain")
label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
parts = [
subject,
label_text,
f"seed={slot.get('slot_seed')}" if _slot_seed(slot) >= 0 else "",
f"age={slot.get('age', 'random')}",
f"ethnicity={slot.get('ethnicity', 'random')}",
f"figure={slot.get('figure', 'random')}",
f"body={slot.get('body', 'random')}",
f"detail={slot.get('descriptor_detail', 'auto')}",
]
parts = [part for part in parts if part]
if _slot_is_pov(slot):
parts.append("presence=pov")
if not _slot_expression_enabled(slot):
parts.append("expression=disabled")
else:
expression_intensity = _slot_expression_intensity(slot)
if expression_intensity is not None:
parts.append(f"expression={expression_intensity:.2f}")
softcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "softcore")
hardcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "hardcore")
if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity:
parts.append(f"soft_expr={softcore_expression_intensity:.2f}")
if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity:
parts.append(f"hard_expr={hardcore_expression_intensity:.2f}")
if slot.get("softcore_outfit"):
parts.append(f"soft_outfit={slot['softcore_outfit']}")
if slot.get("hardcore_clothing"):
parts.append(f"hard_clothing={slot['hardcore_clothing']}")
characteristics = _parse_characteristics_config(slot.get("characteristics"))
characteristics_summary = _characteristics_summary(characteristics)
if characteristics_summary != "characteristics unrestricted":
parts.append(f"characteristics={characteristics_summary}")
hair_config = _parse_hair_config(slot.get("hair_config"))
hair_config_summary = _hair_config_summary(hair_config)
if hair_config_summary != "hair unrestricted":
parts.append(f"hair={hair_config_summary}")
for key in ("hair_color", "hair_length", "hair_style"):
value = slot.get(key)
if value and value != "random":
parts.append(f"{key}={value}")
for key in ("body_phrase", "skin", "hair", "eyes"):
value = slot.get(key)
if value:
parts.append(f"{key}={value}")
return "; ".join(parts)
def build_character_slot_json(
subject_type: str = "woman",
label: str = "auto_chain",
slot_seed: int = -1,
age: str = "random",
manual_age: str = "",
manual: str | dict[str, Any] | None = "",
ethnicity: str = "random",
figure: str = "random",
body: str = "random",
manual_body: str = "",
body_phrase: str = "",
skin: str = "",
hair: str = "",
characteristics: str | dict[str, Any] | None = "",
hair_config: str | dict[str, Any] | None = "",
hair_color: str = "random",
hair_length: str = "random",
hair_style: str = "random",
eyes: str = "",
descriptor_detail: str = "auto",
expression_enabled: bool = True,
expression_intensity: float = -1.0,
enabled: bool = True,
character_cast: str | dict[str, Any] | list[Any] | None = "",
presence_mode: str = "visible",
softcore_expression_intensity: float = -1.0,
hardcore_expression_intensity: float = -1.0,
softcore_outfit: str = "",
hardcore_clothing: str = "",
) -> dict[str, str]:
existing_slots = _parse_character_cast(character_cast)
slot = _normalize_character_slot(
{
"subject_type": subject_type,
"label": label,
"slot_seed": slot_seed,
"age": age,
"manual_age": manual_age,
"manual": manual,
"ethnicity": ethnicity,
"figure": figure,
"body": body,
"manual_body": manual_body,
"body_phrase": body_phrase,
"skin": skin,
"hair": hair,
"characteristics": characteristics,
"hair_config": hair_config,
"hair_color": hair_color,
"hair_length": hair_length,
"hair_style": hair_style,
"eyes": eyes,
"descriptor_detail": descriptor_detail,
"presence_mode": presence_mode,
"softcore_outfit": softcore_outfit,
"hardcore_clothing": hardcore_clothing,
"expression_enabled": expression_enabled,
"expression_intensity": expression_intensity,
"softcore_expression_intensity": softcore_expression_intensity,
"hardcore_expression_intensity": hardcore_expression_intensity,
}
)
slots = existing_slots + ([slot] if enabled else [])
cast = {
"profile_type": "character_cast",
"version": 1,
"slots": slots,
}
return {
"character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
"character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
"summary": slot["summary"] if enabled else "disabled",
"status": f"{len(slots)} slot(s)",
}
def _slot_explicit_label(slot: dict[str, Any]) -> str:
label = str(slot.get("label") or "").strip().upper()
if label in CHARACTER_LABEL_CHOICES and label != "AUTO_CHAIN":
return label
return ""
def _character_slot_label_map(slots: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
label_map: dict[str, dict[str, Any]] = {}
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for subject_type, prefix in (("woman", "Woman"), ("man", "Man")):
subject_slots = [slot for slot in slots if slot.get("subject_type") == subject_type]
auto_slots = [slot for slot in subject_slots if not _slot_explicit_label(slot)]
for index, slot in enumerate(reversed(auto_slots)):
if index >= len(letters):
break
label_map[f"{prefix} {letters[index]}"] = slot
for slot in subject_slots:
explicit = _slot_explicit_label(slot)
if explicit:
label_map[f"{prefix} {explicit}"] = slot
return label_map
def _pov_character_labels(
label_map: dict[str, dict[str, Any]],
men_count: int | None = None,
) -> list[str]:
if men_count is None:
labels = sorted(label for label in label_map if label.startswith("Man "))
else:
labels = [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]
return [label for label in labels if _slot_is_pov(label_map.get(label))]
def _pov_text_with_viewer(text: Any, pov_labels: list[str]) -> str:
rendered = str(text or "").strip()
if not rendered or not pov_labels:
return rendered
for label in sorted(pov_labels, key=len, reverse=True):
escaped = re.escape(label)
rendered = re.sub(rf"\b{escaped}'s\b", "the POV viewer's", rendered)
rendered = re.sub(rf"\b{escaped}\b", "the POV viewer", rendered)
rendered = re.sub(r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
return _clean_prompt_punctuation(rendered)
def _pov_role_graph_prompt(role_graph: Any, pov_labels: list[str]) -> str:
role_graph_text = str(role_graph or "").strip()
if not role_graph_text or not pov_labels:
return role_graph_text
viewer_text = _pov_text_with_viewer(role_graph_text, pov_labels)
label_text = ", ".join(pov_labels)
return f"First-person POV from {label_text}; {viewer_text}"
def _pov_prompt_directive(pov_labels: list[str]) -> str:
if not pov_labels:
return ""
label_text = ", ".join(pov_labels)
return (
f"POV participant: {label_text} is the first-person camera viewpoint; "
"he remains the off-camera viewpoint, represented by foreground hands, body position, or camera perspective cues when needed."
)
def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str:
text = str(composition or "").strip()
if not text or not pov_labels:
return text
text = re.sub(r"\ball participants visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball adult bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball three bodies readable\b", "visible partner bodies readable", text, flags=re.IGNORECASE)
text = re.sub(r"\bwide group-sex composition\b", "first-person group-sex POV composition", text, flags=re.IGNORECASE)
if "pov" not in text.lower() and "first-person" not in text.lower():
text = f"{text}, adapted for first-person POV with the POV participant kept off-camera"
return _clean_prompt_punctuation(text)
def _body_exposure_scene_text(scene: Any) -> str:
text = str(scene or "").strip()
if not text:
return ""
replacements = (
(r",?\s*\bscattered (?:clothes|clothing)\b", ""),
(r",?\s*\bfloor clothes\b", ""),
(r"\bclothes scattered\b", "soft floor shadows"),
(r",?\s*\bscattered lingerie\b", ""),
(r",?\s*\blingerie visible nearby\b", ""),
(r"\boutfit racks\b", "mirror shelves"),
(r"\bcostume racks\b", "mirror shelves"),
(r"\bhanging outfits\b", "hanging fabric"),
(r"\bclothing hooks\b", "wall hooks"),
(r"\boutfit-check\b", "creator-shot"),
(r"\boutfit framing\b", "body framing"),
(r"\bfull outfits\b", "full bodies"),
(r"\bcoordinated outfits\b", "coordinated posing"),
)
for pattern, replacement in replacements:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
text = re.sub(r"\bwith,\s*", "with ", text, flags=re.IGNORECASE)
text = re.sub(r",\s*,", ",", text)
return _clean_prompt_punctuation(text)
def _slot_softcore_outfit(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
if not slot:
return ""
outfit = _slot_value(slot.get("softcore_outfit"))
if outfit:
return outfit
if rng is None:
return ""
return _characteristic_choice(_parse_characteristics_config(slot.get("characteristics")), "softcore_outfits", rng)
def _slot_hardcore_clothing(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
if not slot:
return ""
clothing = _slot_value(slot.get("hardcore_clothing"))
if clothing:
return clothing
if rng is None:
return ""
return _characteristic_choice(_parse_characteristics_config(slot.get("characteristics")), "hardcore_clothing", rng)
def _softcore_outfit_sentence(label: str, outfit: str) -> str:
outfit = str(outfit or "").strip()
if not outfit:
return ""
lower = outfit.lower()
if lower.startswith(("wears ", "wearing ", "in ")):
return f"{label} {outfit}"
return f"{label} wears {outfit}"
def _hardcore_clothing_sentence(label: str, clothing: str) -> str:
clothing = str(clothing or "").strip().rstrip(".")
if not clothing:
return ""
lower = clothing.lower()
if lower.startswith(("fully nude", "nude")):
return f"{label}'s body is fully exposed, bare skin unobstructed"
if lower.startswith("partly nude"):
return f"{label}'s body is partly exposed"
if lower.startswith(("is ", "wears ", "wearing ", "keeps ", "has ", "with ")):
return f"{label} {clothing}"
return f"{label}'s clothing: {clothing}"
def _character_hardcore_clothing_entries(
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
pov_labels: list[str] | None = None,
rng: random.Random | None = None,
) -> list[str]:
pov_set = set(pov_labels or [])
labels = [
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
]
entries: list[str] = []
for label in labels:
if label in pov_set:
continue
clothing = _slot_hardcore_clothing(label_map.get(label), rng)
sentence = _hardcore_clothing_sentence(label, clothing)
if sentence:
entries.append(sentence)
return entries
def _context_from_character_slot(
rng: random.Random,
slot: dict[str, Any],
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> dict[str, str]:
slot_ethnicity = _slot_value(slot.get("ethnicity"))
slot_body = _slot_value(slot.get("body"))
effective_ethnicity = slot_ethnicity or ethnicity
effective_figure = _slot_effective_figure(slot, subject_type, figure)
effective_no_plus = bool(no_plus_women) and not slot_body
effective_no_black = bool(no_black) and not slot_ethnicity
appearance_rng = _slot_context_rng(slot, rng)
context = _appearance_for_subject(
appearance_rng,
subject_type,
effective_ethnicity,
effective_figure,
effective_no_plus,
effective_no_black,
)
characteristics = _parse_characteristics_config(slot.get("characteristics"))
age = _slot_value(slot.get("age")) or _characteristic_choice(characteristics, "ages", appearance_rng)
body_phrase = _slot_value(slot.get("body_phrase"))
if not slot_body:
slot_body = _characteristic_choice(characteristics, "bodies", appearance_rng)
if age:
context["age"] = age
if slot_body:
context["body"] = slot_body
if subject_type == "woman":
context["body_phrase"] = _body_phrase(slot_body, context.get("figure", ""))
else:
context["body_phrase"] = f"{slot_body} figure"
if body_phrase:
context["body_phrase"] = body_phrase
skin_value = _slot_value(slot.get("skin"))
if skin_value:
context["skin"] = skin_value
eyes_value = _slot_value(slot.get("eyes"))
if not eyes_value:
eyes_value = _eye_phrase_from_key(_characteristic_choice(characteristics, "eyes", appearance_rng))
if eyes_value:
context["eyes"] = eyes_value
hair_value = _slot_value(slot.get("hair"))
if hair_value:
context["hair"] = hair_value
else:
hair_descriptor = _hair_descriptor_from_slot(context.get("hair"), slot, appearance_rng)
if hair_descriptor:
context["hair"] = hair_descriptor
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
context["presence_mode"] = _normalize_presence_mode(slot.get("presence_mode"), subject_type)
context["expression_enabled"] = _slot_expression_enabled(slot)
expression_intensity = _slot_expression_intensity(slot)
if expression_intensity is not None:
context["expression_intensity"] = expression_intensity
context["subject_type"] = subject_type
context["subject"] = subject_type
context["subject_phrase"] = subject_type
return context
def _character_context_for_label(
label: str,
label_map: dict[str, dict[str, Any]],
rng: random.Random,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> tuple[dict[str, str], dict[str, Any] | None]:
subject_type = "man" if label.startswith("Man ") else "woman"
slot = label_map.get(label)
if slot:
return _context_from_character_slot(rng, slot, subject_type, ethnicity, figure, no_plus_women, no_black), slot
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black), None
def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
for key in (
"subject_type",
"subject",
"subject_phrase",
"age",
"body",
"body_phrase",
"skin",
"hair",
"eyes",
"figure",
"descriptor_detail",
"presence_mode",
"expression_enabled",
"expression_intensity",
):
value = context.get(key)
if value is not None and value != "":
row[key] = value
if context.get("age"):
row["age_band"] = context["age"]
return row
def _cast_descriptor_entries(
seed_config: dict[str, int],
seed: int,
row_number: int,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
women_count: int,
men_count: int,
character_cast: str | dict[str, Any] | list[Any] | None = "",
primary_descriptor: str = "",
) -> tuple[list[str], list[dict[str, Any]]]:
slots = _parse_character_cast(character_cast)
label_map = _character_slot_label_map(slots)
rng = _axis_rng(seed_config, "person", seed, row_number + 997)
descriptors: list[str] = []
for index in range(max(0, women_count)):
label = f"Woman {chr(ord('A') + index)}"
if index == 0 and primary_descriptor:
descriptors.append(f"Woman A / primary creator: {primary_descriptor}")
continue
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
for index in range(max(0, men_count)):
label = f"Man {chr(ord('A') + index)}"
if _slot_is_pov(label_map.get(label)):
continue
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
return descriptors, slots
def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
row = _load_json_object(metadata_json, "metadata_json")
if isinstance(row.get("softcore_row"), dict):
return row["softcore_row"]
return row
def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
slots = _parse_character_cast(character_slot)
if not slots:
return {}
slot = slots[-1]
if _slot_seed(slot) >= 0:
subject_type = str(slot.get("subject_type") or "woman")
return _context_from_character_slot(
random.Random(_row_seed(_slot_seed(slot), 1, 719)),
slot,
subject_type,
"any",
"curvy",
False,
False,
)
return slot
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
return _descriptor_from_parts(
subject,
profile.get("age"),
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
profile.get("skin"),
profile.get("hair"),
profile.get("eyes"),
profile.get("descriptor_detail"),
)
def _normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
subject_type = str(profile.get("subject_type") or profile.get("primary_subject") or profile.get("subject") or "").strip()
if subject_type not in ("woman", "man"):
subject_type = "woman"
body = str(profile.get("body") or profile.get("body_type") or "").strip()
figure = str(profile.get("figure") or "").strip()
body_phrase = str(profile.get("body_phrase") or "").strip() or _body_phrase(body, figure)
normalized = {
"profile_type": "character",
"profile_name": _safe_profile_name(profile_name or str(profile.get("profile_name") or "")),
"subject_type": subject_type,
"subject": subject_type,
"subject_phrase": subject_type,
"age": str(profile.get("age") or profile.get("age_band") or "").strip(),
"body": body,
"body_phrase": body_phrase,
"skin": str(profile.get("skin") or "").strip(),
"hair": str(profile.get("hair") or "").strip(),
"eyes": str(profile.get("eyes") or "").strip(),
"figure": figure,
"descriptor_detail": _normalize_descriptor_detail(profile.get("descriptor_detail")),
}
normalized["descriptor"] = _character_profile_descriptor(normalized)
return normalized
def build_character_profile_json(
profile_name: str = "",
source: str = "metadata_json",
metadata_json: str | dict[str, Any] | None = "",
character_slot: str | dict[str, Any] | None = "",
subject_type: str = "woman",
age: str = "",
body: str = "",
body_phrase: str = "",
skin: str = "",
hair: str = "",
eyes: str = "",
figure: str = "",
save_now: bool = False,
) -> dict[str, str]:
if source == "character_slot":
row = _row_from_character_slot(character_slot or metadata_json)
raw_profile = {
"profile_name": profile_name,
"subject_type": row.get("subject_type") or subject_type,
"age": row.get("age") or age,
"body": row.get("body") or body,
"body_phrase": row.get("body_phrase") or body_phrase,
"skin": row.get("skin") or skin,
"hair": row.get("hair") or hair,
"eyes": row.get("eyes") or eyes,
"figure": row.get("figure") or figure,
"descriptor_detail": row.get("descriptor_detail") or "auto",
}
elif source == "metadata_json":
row = _row_from_profile_metadata(metadata_json)
raw_profile = {
"profile_name": profile_name,
"subject_type": row.get("subject_type") or row.get("primary_subject") or subject_type,
"age": row.get("age") or row.get("age_band") or age,
"body": row.get("body") or row.get("body_type") or body,
"body_phrase": row.get("body_phrase") or body_phrase,
"skin": row.get("skin") or skin,
"hair": row.get("hair") or hair,
"eyes": row.get("eyes") or eyes,
"figure": row.get("figure") or figure,
"descriptor_detail": row.get("descriptor_detail") or "auto",
}
else:
raw_profile = {
"profile_name": profile_name,
"subject_type": subject_type,
"age": age,
"body": body,
"body_phrase": body_phrase,
"skin": skin,
"hair": hair,
"eyes": eyes,
"figure": figure,
"descriptor_detail": "auto",
}
profile = _normalize_character_profile(raw_profile, profile_name)
saved_path = ""
status = "not_saved"
if save_now:
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
path = _profile_path(profile["profile_name"])
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
saved_path = str(path)
status = "saved"
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": saved_path,
"status": status,
}
def save_character_profile_payload(profile_name: str = "", profile_json: str | dict[str, Any] | None = "") -> dict[str, str]:
raw_profile = _load_json_object(profile_json, "profile_json")
if not raw_profile:
raise ValueError("No cached character profile is available to save.")
profile = _normalize_character_profile(raw_profile, profile_name or str(raw_profile.get("profile_name") or ""))
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
path = _profile_path(profile["profile_name"])
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": str(path),
"status": "saved",
}
def _empty_profile_result(status: str = "empty") -> dict[str, str]:
return {
"profile_json": "",
"profile_name": "",
"descriptor": "",
"saved_path": "",
"status": status,
}
def _apply_character_profile_overrides(
profile: dict[str, Any],
override_subject_type: str = "",
override_age: str = "",
override_body: str = "",
override_body_phrase: str = "",
override_skin: str = "",
override_hair: str = "",
override_eyes: str = "",
override_figure: str = "",
override_descriptor_detail: str = "",
) -> dict[str, Any]:
updated = dict(profile)
subject_type = str(override_subject_type or "").strip()
if subject_type in ("woman", "man"):
updated["subject_type"] = subject_type
updated["subject"] = subject_type
updated["subject_phrase"] = subject_type
for key, value in (
("age", override_age),
("body", override_body),
("body_phrase", override_body_phrase),
("skin", override_skin),
("hair", override_hair),
("eyes", override_eyes),
("figure", override_figure),
):
text = str(value or "").strip()
if text:
updated[key] = text
descriptor_detail = str(override_descriptor_detail or "").strip()
if descriptor_detail and descriptor_detail != "keep_profile":
updated["descriptor_detail"] = _normalize_descriptor_detail(descriptor_detail)
if not str(updated.get("body_phrase") or "").strip():
updated["body_phrase"] = _body_phrase(updated.get("body"), updated.get("figure"))
updated["descriptor"] = _character_profile_descriptor(updated)
return updated
def load_character_profile_json(
profile_name: str = "",
fallback_profile_json: str | dict[str, Any] | None = "",
enabled: bool = True,
delete_now: bool = False,
rename_now: bool = False,
rename_to: str = "",
override_subject_type: str = "",
override_age: str = "",
override_body: str = "",
override_body_phrase: str = "",
override_skin: str = "",
override_hair: str = "",
override_eyes: str = "",
override_figure: str = "",
override_descriptor_detail: str = "",
) -> dict[str, str]:
if not enabled:
return _empty_profile_result("disabled")
if delete_now and rename_now:
return _empty_profile_result("choose_delete_or_rename")
raw_profile = _load_json_object(fallback_profile_json, "fallback_profile_json")
saved_path = ""
if profile_name and profile_name != "manual":
path = _profile_path(profile_name)
if delete_now:
if path.exists():
path.unlink()
return _empty_profile_result(f"deleted:{path.stem}")
return _empty_profile_result(f"delete_missing:{_safe_profile_name(profile_name)}")
if rename_now:
new_name = _safe_profile_name(rename_to)
if not rename_to.strip():
return _empty_profile_result("rename_missing_name")
if not path.exists():
return _empty_profile_result(f"rename_missing:{_safe_profile_name(profile_name)}")
target = _profile_path(new_name)
if target.exists() and target != path:
return _empty_profile_result(f"rename_target_exists:{target.stem}")
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
profile = _normalize_character_profile(raw_profile, new_name)
target.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
if target != path:
path.unlink()
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": str(target),
"status": f"renamed:{path.stem}->{target.stem}",
}
if path.exists():
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
saved_path = str(path)
if not raw_profile:
return _empty_profile_result("empty")
profile = _normalize_character_profile(raw_profile, profile_name or raw_profile.get("profile_name", ""))
profile = _apply_character_profile_overrides(
profile,
override_subject_type=override_subject_type,
override_age=override_age,
override_body=override_body,
override_body_phrase=override_body_phrase,
override_skin=override_skin,
override_hair=override_hair,
override_eyes=override_eyes,
override_figure=override_figure,
override_descriptor_detail=override_descriptor_detail,
)
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": saved_path,
"status": "loaded" if saved_path else "fallback",
}
def _parse_character_profile(character_profile: str | dict[str, Any] | None) -> dict[str, Any]:
raw = _load_json_object(character_profile, "character_profile")
if not raw:
return {}
if raw.get("profile_type") == "character" or any(key in raw for key in ("age", "age_band", "skin", "hair", "eyes")):
return _normalize_character_profile(raw, str(raw.get("profile_name") or ""))
return {}
def _apply_character_profile_to_context(
context: dict[str, Any],
character_profile: str | dict[str, Any] | None,
) -> tuple[dict[str, Any], dict[str, Any], str]:
profile = _parse_character_profile(character_profile)
if not profile:
return context, {}, "none"
if context.get("subject_type") not in ("woman", "man"):
return context, profile, "skipped_non_single_subject"
if profile["subject_type"] != context.get("subject_type"):
return context, profile, "skipped_subject_mismatch"
updated = dict(context)
for key in (
"subject_type",
"subject",
"subject_phrase",
"age",
"body",
"body_phrase",
"skin",
"hair",
"eyes",
"figure",
"descriptor_detail",
):
value = profile.get(key)
if value:
updated[key] = value
updated["subject"] = profile["subject_type"]
updated["subject_phrase"] = profile["subject_type"]
return updated, profile, "applied"
def _composition_prompt(composition: str) -> str:
composition = str(composition or "").strip()
if not composition:
return composition
lower = composition.lower()
if lower.startswith("vertical ") or " vertical " in lower or lower.endswith(" vertical"):
return composition
return f"vertical {composition}"
def _appearance_for_subject(
rng: random.Random,
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> dict[str, str]:
if subject_type == "single_any":
subject_type = "woman" if rng.random() < 0.82 else "man"
if subject_type == "man":
men_ethnicity = ethnicity if ethnicity else "any"
subject, age, body, skin, hair, eyes = g.choose(rng, g.by_ethnicity(g.MEN, men_ethnicity))
return {
"subject_type": "man",
"subject": subject,
"subject_phrase": subject,
"age": age,
"body": body,
"skin": skin,
"hair": hair,
"eyes": eyes,
"body_phrase": f"{body} figure",
}
subject, age, body, skin, hair, eyes = g.choose_woman(rng, ethnicity, no_plus_women, no_black)
figure_note = g.choose(rng, g.figure_pool(figure))
return {
"subject_type": "woman",
"subject": subject,
"subject_phrase": subject,
"age": age,
"body": body,
"skin": skin,
"hair": hair,
"eyes": eyes,
"body_phrase": _body_phrase(body, figure_note),
"figure": figure_note,
}
def _count_phrase(count: int, singular: str, plural: str) -> str:
words = {
0: "no",
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
10: "ten",
11: "eleven",
12: "twelve",
}
label = singular if count == 1 else plural
return f"{words.get(count, str(count))} {label}"
def _configured_cast_context(women_count: int, men_count: int) -> dict[str, str]:
women_count = max(0, int(women_count))
men_count = max(0, int(men_count))
if women_count + men_count == 0:
women_count = 1
parts = []
if women_count:
parts.append(_count_phrase(women_count, "adult woman", "adult women"))
if men_count:
parts.append(_count_phrase(men_count, "adult man", "adult men"))
if len(parts) == 1:
subject_phrase = parts[0]
else:
subject_phrase = f"{parts[0]} and {parts[1]}"
person_count = women_count + men_count
if person_count == 1:
scene_kind = "solo adult sexual pose"
elif person_count == 2:
scene_kind = "adult couple sex scene"
elif person_count == 3:
scene_kind = "adult threesome sex scene"
else:
scene_kind = "adult group sex scene"
women_label = "woman" if women_count == 1 else "women"
men_label = "man" if men_count == 1 else "men"
cast_summary = f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults"
return {
"subject_type": "configured_cast",
"subject": f"{women_count}w_{men_count}m_sex_scene",
"subject_phrase": subject_phrase,
"age": "21+ adults",
"body": "varied",
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "varied adult bodies",
"women_count": str(women_count),
"men_count": str(men_count),
"person_count": str(person_count),
"cast_summary": cast_summary,
"scene_kind": scene_kind,
}
def _couple_type_from_counts(
rng: random.Random,
women_count: int,
men_count: int,
) -> tuple[str, str, str, int, int]:
women_count = max(0, int(women_count))
men_count = max(0, int(men_count))
if women_count >= 2 and men_count == 0:
return "two women", "two women", "close affectionate couple pose", 2, 0
if men_count >= 2 and women_count == 0:
return "two men", "two men", "relaxed romantic couple pose", 0, 2
if women_count >= 1 and men_count >= 1:
return "woman and man", "a woman and a man", "playful date-night pose", 1, 1
primary_subject, subject_phrase, pose = g.choose(rng, g.COUPLE_TYPES)
if primary_subject == "two women":
return primary_subject, subject_phrase, pose, 2, 0
if primary_subject == "two men":
return primary_subject, subject_phrase, pose, 0, 2
return primary_subject, subject_phrase, pose, 1, 1
def _lettered(prefix: str, count: int) -> list[str]:
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
return [f"{prefix.capitalize()} {letters[index]}" for index in range(max(0, count))]
def _pick_distinct(rng: random.Random, items: list[str], count: int) -> list[str]:
if not items:
return []
if len(items) >= count:
return rng.sample(items, count)
picked = list(items)
while len(picked) < count:
picked.append(items[rng.randrange(len(items))])
return picked
def _participant_context(women_count: int, men_count: int) -> dict[str, list[str]]:
women = _lettered("woman", women_count)
men = _lettered("man", men_count)
return {"women": women, "men": men, "people": women + men}
def _role_graph(
rng: random.Random,
subcategory: dict[str, Any],
context: dict[str, str],
item_axis_values: dict[str, str] | None = None,
pov_labels: list[str] | None = None,
) -> str:
if context.get("subject_type") != "configured_cast":
return ""
women_count = int(context.get("women_count") or 0)
men_count = int(context.get("men_count") or 0)
people_count = women_count + men_count
if people_count <= 0:
return ""
participants = _participant_context(women_count, men_count)
women = participants["women"]
men = participants["men"]
people = participants["people"]
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
item_text = " ".join((item_axis_values or {}).values()).lower()
pov_set = set(pov_labels or [])
def any_person(exclude: set[str] | None = None) -> str:
exclude = exclude or set()
pool = [person for person in people if person not in exclude] or people
return rng.choice(pool)
def any_woman(exclude: set[str] | None = None) -> str:
exclude = exclude or set()
pool = [person for person in women if person not in exclude] or [person for person in people if person not in exclude] or people
return rng.choice(pool)
def any_man(exclude: set[str] | None = None) -> str:
exclude = exclude or set()
pool = [person for person in men if person not in exclude] or [person for person in people if person not in exclude] or people
return rng.choice(pool)
def support_sentence(exclude: set[str]) -> str:
extras = [person for person in people if person not in exclude]
if not extras:
return ""
extra = rng.choice(extras)
actions = [
"kisses and grips the nearest body",
"holds hips open for the camera",
"touches breasts, thighs, and stomach",
"keeps one hand on a partner's ass",
"watches close and joins the body contact",
"presses in from the side with hands on skin",
]
return f" {extra} {rng.choice(actions)}."
def foreplay_position_graph(primary: str, partner: str) -> str:
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
if any(term in text for term in ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning")):
return (
f"{primary} and {partner} stand close while {partner}'s hands pull clothing aside from {primary}'s body; "
f"{primary}'s exposed skin and the clothing being removed stay clearly visible."
)
if any(term in text for term in ("breast", "breasts", "nipple", "cupping breasts", "touching breasts")):
return (
f"{primary} and {partner} press their bodies close while {partner}'s hand cups {primary}'s breast; "
f"their faces stay close and the breast-touching gesture is clear."
)
if any(term in text for term in ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin")):
return (
f"{primary} and {partner} stand face-to-face at close range while one hand holds {primary}'s cheek and jaw; "
f"their lips are close and the face-touching gesture is clear."
)
if any(term in text for term in ("kiss", "kissing", "mouth-to-mouth", "lips pressed")):
return (
f"{primary} and {partner} press their bodies together and kiss deeply, "
f"with hands on each other's face, waist, and hips."
)
return (
f"{primary} and {partner} are pressed close in a heated foreplay setup, "
f"hands caressing skin while clothing is pulled aside."
)
def interaction_text() -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
def manual_position_graph(primary: str, partner: str = "") -> str:
text = interaction_text()
if not partner:
if "mutual" in text:
return f"{primary} faces the camera with thighs open, both hands on her body for solo mutual-style masturbation framing."
return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy."
if "mutual" in text:
return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible."
if "clit" in text or "clitoris" in text:
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers rubbing her clit as her hips tilt toward the touch."
if "toy" in text or "vibrator" in text:
return f"{primary} reclines with thighs open while {partner} holds a vibrator or toy against her clit, one hand keeping her thigh open."
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers visibly stimulating her pussy."
def interaction_position_graph(primary: str, partner: str, third: str = "") -> str:
text = interaction_text()
if "aftercare" in slug or any(term in text for term in ("aftercare", "cleanup", "wiping", "towel", "post-sex", "cuddle")):
if "cleanup" in text or "wiping" in text or "towel" in text:
return f"{primary} reclines after sex while {partner} kneels close and wipes her skin with a towel, hands and relaxed body contact visible."
return f"{primary} and {partner} lie close together after sex, bodies relaxed and hands resting on skin in a post-sex cuddle."
if "camera_performance" in slug or any(term in text for term in ("camera", "presenting", "showing", "viewer", "creator-shot")):
if third:
return f"{primary} faces the camera while {partner} and {third} hold and present her body, hands framing the exposed skin for the viewer."
return f"{primary} faces the camera and presents her body while {partner}'s hands hold her hips or thighs open for a clear creator-shot reveal."
if "body_worship" in slug or any(term in text for term in ("body worship", "nipple", "thigh", "mouth on skin", "kissing down", "ass grabbing")):
if "ass" in text:
return f"{primary} stands or kneels with hips angled back while {partner}'s hands grip her ass, fingers pressing into skin."
if "thigh" in text:
return f"{primary} reclines with thighs open while {partner} kneels close and kisses along her inner thighs, hands holding her legs in place."
if "nipple" in text or "breast" in text:
return f"{primary} arches toward {partner} while {partner}'s mouth is on her breast and one hand cups or squeezes the other breast."
return f"{primary} reclines or leans back while {partner} kisses down her body, hands tracing breasts, waist, hips, and thighs."
if "clothing_position" in slug or any(term in text for term in ("transition", "turning", "pulling onto", "lifting", "guided backward", "clothing", "garment")):
if "turn" in text or "rear-facing" in text:
return f"{partner}'s hands turn {primary} around by the hips, clothing partly moved aside as her body rotates into the next pose."
if "legs" in text or "thigh" in text:
return f"{primary} lies back while {partner} lifts and spreads her legs into position, hands and clothing movement clearly visible."
return f"{primary} and {partner} are mid-transition, with {partner}'s hands moving clothing aside and guiding {primary}'s hips toward the next pose."
if "dominant" in slug or any(term in text for term in ("hair", "wrist", "wrists", "jaw", "chin", "guided", "dominant", "control", "dirty talk", "whisper", "mouth near the ear", "verbal teasing")):
if "dirty talk" in text or "whisper" in text or "mouth near the ear" in text or "verbal teasing" in text:
return f"{partner} leans close to {primary}'s ear for dirty talk while holding her waist and keeping their bodies pressed close."
if "wrist" in text or "wrists" in text:
return f"{primary} lies back while {partner} pins her wrists above her head, both bodies close and the consensual control gesture clearly visible."
if "hair" in text:
return f"{partner} holds {primary}'s hair back while guiding her body closer, face and hair-hold gesture visible."
if "thigh" in text or "spread" in text:
return f"{primary} reclines with thighs open while {partner}'s hands spread her legs and hold the position for the camera."
return f"{partner} guides {primary}'s body with hands on her jaw, waist, and hips, keeping the consensual control gesture readable."
return foreplay_position_graph(primary, partner)
def group_coordination_graph(primary: str, partner: str, third: str) -> str:
observer = third or any_person({primary, partner})
text = interaction_text()
if "camera" in text or "hold" in text or "present" in text:
return f"{primary} is centered while {partner} and {observer} hold and present the body for the camera, each role clearly visible."
if "watch" in text or "waiting" in text:
return f"{primary} is centered while {partner} touches her body and {observer} watches close beside them, hands and faces readable."
return f"{primary} is centered while {partner} touches her body and {observer} stays close as the watching or guiding partner."
def mentions_ass(text: str) -> bool:
return bool(
re.search(
r"\bass\b|ass[- ](?:up|raised|exposed|lifted)|spread cheeks|lower back and ass|cum (?:on|dripping from) ass|pussy, ass|ass and",
text,
)
)
def climax_position_graph(woman: str, man: str, third: str = "") -> str:
if "lying between two partners" in item_text and third:
return f"{woman} lies between {man} and {third}, with {man} under her hips and {third} positioned above her torso as visible semen lands on her body."
if "held between front-and-back partners" in item_text and third:
return f"{woman} is held between {man} behind her and {third} in front of her as visible semen lands across her body."
if "kneeling between standing partners" in item_text and third:
return f"{woman} kneels between {man} and {third} while both stand close around her face and torso for visible ejaculation."
if "side-lying with thighs parted" in item_text:
return f"{woman} lies on her side with thighs parted while {man} kneels beside her hips and ejaculates semen across her thighs and pussy."
if "sitting on the edge of the bed" in item_text:
return f"{woman} sits on the edge of the bed with knees spread while {man} stands close between her legs and ejaculates semen across her body."
if "lying at the bed edge with thighs open" in item_text:
return f"{woman} lies at the bed edge with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs."
if "reclining with thighs open" in item_text or "lying on the back with legs spread" in item_text:
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs."
if "on all fours with hips raised" in item_text:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and ejaculates semen across her ass, thighs, and lower back."
if "face-down ass-up" in item_text:
return f"{woman} lies face-down with ass raised while {man} is positioned behind her and ejaculates semen across her lower back and ass."
if "bent over with ass raised" in item_text or "bent over" in item_text:
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs."
if "kneeling with mouth open" in item_text:
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
if "kneeling in front of a standing partner" in item_text:
return f"{woman} kneels in front of {man} at hip height while {man} stands over her for visible ejaculation."
if "standing with cum on the body" in item_text:
return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body."
if "squatting on top of a partner" in item_text:
return f"{woman} squats over {man}'s hips while {man} lies on his back under her and ejaculates semen onto her body."
if "reverse cowgirl over a partner's hips" in item_text:
return f"{woman} straddles {man}'s hips facing away while {man} lies on his back under her and ejaculates semen onto her body."
if any(term in item_text for term in ("straddling a partner", "straddling a partner's hips", "shared climax after penetration", "orgasm during penetration")):
return f"{woman} straddles {man}'s hips while {man} lies on his back under her, their bodies still aligned from penetration as he ejaculates semen onto her body."
if "seated in a partner's lap facing them" in item_text:
return f"{woman} sits in {man}'s lap facing him, legs wrapped around his hips as he ejaculates semen across her body."
if any(term in item_text for term in ("lower back", "cum dripping from ass", "cum on lower back")) or mentions_ass(item_text):
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs."
if any(term in item_text for term in ("cum on face", "cum on tongue", "cum on lips", "cum on face and lips", "cum on tongue and chin")):
if third:
return f"{woman} kneels in the center while {man} and {third} stand close around her face and torso for visible ejaculation."
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen onto her body."
def penetration_position_graph(woman: str, man: str) -> str:
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
if "missionary" in text:
return (
f"{woman} lies on her back with legs open around {man}'s hips while {man} is above her between her thighs; "
f"{man}'s hips press close and {man}'s penis thrusts into her pussy."
)
if "reverse cowgirl" in text:
return f"{woman} straddles {man}'s hips facing away while {man} lies under her and {man}'s penis thrusts into her pussy."
if "cowgirl" in text or "straddling" in text:
return f"{woman} straddles {man}'s hips facing him while {man} lies under her and {man}'s penis thrusts into her pussy."
if "doggy" in text or "rear-entry" in text or "bent-over" in text or "bent over" in text:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and {man}'s penis thrusts into her pussy."
if "standing" in text:
return f"{woman} stands braced with hips angled back while {man} stands behind her and {man}'s penis thrusts into her pussy."
if "spooning" in text or "side-lying" in text:
return f"{woman} lies on her side with thighs parted while {man} presses behind her and {man}'s penis thrusts into her pussy."
if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text or "edge-supported" in text or "raised edge" in text:
return (
f"{woman} lies back at a raised edge with hips at the edge and legs open while {man} kneels between her thighs; "
f"{man}'s hips press close and {man}'s penis thrusts into her pussy."
)
if "kneeling straddle" in text:
return f"{woman} kneels straddling {man}'s hips while {man} supports her waist and {man}'s penis thrusts into her pussy."
if "lotus" in text:
return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her pussy."
return (
f"{woman} lies on her back with legs spread wide and knees bent outward while {man} kneels between her open thighs facing her; "
f"{man}'s hips are pressed between her legs and {man}'s penis thrusts into her pussy."
)
def anal_position_graph(woman: str, man: str) -> str:
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
if "bent-over" in text or "bent over" in text:
return f"{woman} is bent forward with hips raised while {man} stands behind her and thrusts his penis into her ass."
if "face-down" in text:
return f"{woman} lies face-down with ass raised while {man} is positioned behind her and thrusts his penis into her ass."
if "doggy" in text or "rear-entry" in text:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
if "standing" in text:
return f"{woman} stands braced with hips angled back while {man} stands behind her and thrusts his penis into her ass."
if "spooning" in text or "side-lying" in text:
return f"{woman} lies on her side with thighs parted while {man} presses behind her and thrusts his penis into her ass."
if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text:
return f"{woman} lies near a raised edge with hips exposed while {man} kneels behind her and thrusts his penis into her ass."
if "kneeling" in text:
return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass."
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
def outercourse_position_graph(woman: str, man: str) -> str:
position_text = str((item_axis_values or {}).get("position") or "").lower()
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
man_is_pov = man in pov_set
if any(term in text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
if man_is_pov:
return (
f"{woman} kneels between the POV viewer's open thighs with her torso bent forward over his pelvis and shoulders low, "
"both hands lifting and pressing her breasts tightly around the POV viewer's penis shaft while the glans sits just below her lips."
)
return (
f"{woman} kneels between {man}'s open thighs with her torso bent forward over his pelvis and shoulders low while {man} sits with legs apart, "
f"{woman}'s hands lifting and pressing her breasts tightly around {man}'s penis shaft while the glans sits just below her lips."
)
if any(term in text for term in ("testicle", "balls-licking", "balls licking", "balls and mouth", "balls held")):
if man_is_pov:
return (
f"{woman} kneels very low between the POV viewer's open thighs with her torso bent forward and shoulders between his knees, "
"head tucked under the penis shaft at the base of the penis, mouth and tongue on the POV viewer's balls while his penis points upward above her face."
)
return (
f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, "
f"head tucked under the penis shaft at the base of his penis, mouth and tongue on his balls while {man}'s penis points upward above her face."
)
if "penis-licking" in position_text or "penis licking" in text or "tongue along" in text or "tongue licking" in text:
if man_is_pov:
return (
f"{woman} bends forward between the POV viewer's open thighs, head low under the POV viewer's penis with her face directly under the penis, "
"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis."
)
return (
f"{woman} bends forward between {man}'s open thighs, head low under {man}'s penis with her face directly under the penis, "
f"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis."
)
if "handjob" in position_text or "handjob" in text or "hand job" in text or "hand wrapped" in text:
if man_is_pov:
return (
f"{woman} kneels between the POV viewer's open thighs with her torso leaning forward and face visible behind the penis shaft, "
"one hand wrapped around the POV viewer's penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
)
return (
f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind the penis shaft, "
f"one hand wrapped around {man}'s penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
)
if "footjob" in text or "soles" in text or "toes curled" in text or "feet stroking" in text:
if man_is_pov:
return (
f"{woman} faces the POV viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera, "
"both soles wrapped around the POV viewer's penis shaft in the lower foreground."
)
return (
f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, "
f"wrapping both soles around {man}'s penis shaft while the contact stays centered."
)
if man_is_pov:
return (
f"{woman} kneels close to the POV viewer's hips and keeps the POV viewer's penis centered in clear non-penetrative contact, "
"with her mouth, hands, breasts, or feet visibly working around the penis shaft."
)
return (
f"{woman} kneels close to {man}'s hips and keeps {man}'s penis centered in clear non-penetrative contact, "
"with her mouth, hands, breasts, or feet visibly working around the penis shaft."
)
def oral_position_graph(woman: str, man: str) -> str:
position_text = str((item_axis_values or {}).get("position") or "").lower()
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
man_is_pov = man in pov_set
woman_gives = any(
term in text
for term in (
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"penis in mouth",
"penis in her mouth",
"mouth stretched around a penis",
"lips wrapped",
)
)
man_gives = any(
term in text
for term in (
"cunnilingus",
"pussy licking",
"tongue on pussy",
"mouth on pussy",
"pussy and tongue",
"face-sitting",
"tongue contact clearly visible",
)
)
if "mouth on genitals" in text and not woman_gives and not man_gives:
if any(term in text for term in ("face-sitting", "reclining", "straddled", "spread-leg", "open thighs")):
man_gives = True
else:
woman_gives = True
if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
return f"{woman} and {man} lie head-to-hips in a sixty-nine position, with {woman}'s mouth on {man}'s penis and {man}'s mouth on {woman}'s pussy."
if "face-sitting" in position_text or ("face-sitting" in text and not position_text):
if man_is_pov:
return (
f"{woman} is above the POV camera, straddling the POV viewer's face with thighs on both sides of his head, "
"pussy directly over the POV viewer's mouth for close first-person underview tongue contact."
)
return f"{man} lies on his back while {woman} straddles his face with her thighs around his head and {man}'s mouth pressed to her pussy."
if "straddled oral" in position_text or ("straddled oral" in text and not position_text):
if woman_gives and not man_gives:
return f"{man} straddles forward near {woman}'s face while {woman} kneels below him with her mouth on his penis."
return f"{woman} straddles above {man}'s face with her thighs framing his head while {man}'s mouth stays pressed to her pussy."
if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
if woman_gives and not man_gives:
return f"{man} lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes his penis in her mouth."
return f"{woman} lies on her side with her top thigh lifted while {man} lies beside her hips with his mouth pressed to her pussy."
if (
"edge-of-bed oral" in position_text
or "edge of bed oral" in position_text
or "edge-supported oral" in position_text
or (("edge-of-bed oral" in text or "edge of bed oral" in text or "edge-supported oral" in text) and not position_text)
):
if woman_gives and not man_gives:
return f"{man} sits at a raised edge with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
return f"{woman} lies at a raised edge with thighs open while {man} kneels between her legs with his mouth on her pussy."
if "standing oral" in position_text or ("standing oral" in text and not position_text):
if man_gives and not woman_gives:
return f"{woman} stands braced with one thigh lifted while {man} kneels between her legs with his mouth on her pussy."
return f"{man} stands with hips forward while {woman} kneels in front of him at hip height and takes his penis in her mouth."
if "chair oral" in position_text or ("chair oral" in text and not position_text):
if man_gives and not woman_gives:
return f"{woman} sits in a chair with thighs open while {man} kneels between her legs with his mouth pressed to her pussy."
return f"{man} sits in a chair with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
if (
"reclining cunnilingus" in position_text
or "spread-leg oral" in position_text
or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text)
):
if woman_gives and not man_gives:
return f"{man} reclines with legs apart while {woman} kneels between his thighs and takes his penis in her mouth."
return f"{woman} reclines on her back with thighs spread while {man} kneels between her legs with his mouth on her pussy."
if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text):
if man_gives and not woman_gives:
return f"{woman} kneels with thighs parted and hips angled forward while {man} kneels in front of her with his mouth on her pussy."
return (
f"{woman} kneels in front of {man}'s penis while {man} stands over her; "
f"{woman} takes {man}'s penis in her mouth with saliva dripping on the penis as {man} looks down toward her."
)
if man_gives and not woman_gives:
return f"{woman} lies on her back with thighs open while {man} kneels between her legs with his mouth pressed to her pussy."
return f"{woman} kneels in front of {man}'s hips and takes his penis in her mouth while {man} keeps his hips aligned with her face."
if people_count == 1:
solo = people[0]
if women_count == 1:
if "manual_stimulation" in slug:
return manual_position_graph(solo)
if "camera_performance" in slug:
return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose."
if "cumshot" in slug or "climax" in slug:
return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets."
return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness."
if "cumshot" in slug or "climax" in slug:
return f"{solo} is shown in a solo visible ejaculation pose with one hand on his penis, body angled toward the camera, and semen visible."
return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing."
if women_count > 0 and men_count == 0:
a, b = _pick_distinct(rng, women, 2)
c = any_woman({a, b}) if len(women) >= 3 else ""
used = {a, b}
if "manual_stimulation" in slug:
graph = manual_position_graph(a, b)
elif "group_coordination" in slug and c:
graph = group_coordination_graph(a, b, c)
used.add(c)
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
graph = interaction_position_graph(a, b, c)
if c and "camera_performance" in slug:
used.add(c)
elif "foreplay" in slug:
graph = foreplay_position_graph(a, b)
elif "outercourse" in slug:
graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact."
elif "oral" in slug:
graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy."
elif "anal" in slug or "double" in slug:
graph = f"{a} uses a strap-on on {b} while keeping her hips held open."
elif "threesome" in slug or "group" in slug or "orgy" in slug:
helper = c or any_woman({a})
graph = f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies."
used.add(helper)
elif "cumshot" in slug or "climax" in slug:
graph = f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets."
else:
graph = f"{a} uses a strap-on on {b} while their bodies stay pressed together."
return graph + support_sentence(used)
if men_count > 0 and women_count == 0:
a, b = _pick_distinct(rng, men, 2)
c = any_man({a, b}) if len(men) >= 3 else ""
used = {a, b}
if "manual_stimulation" in slug:
graph = f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup."
elif "group_coordination" in slug and c:
graph = group_coordination_graph(a, b, c)
used.add(c)
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside."
elif "foreplay" in slug:
graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside."
elif "outercourse" in slug:
graph = f"{a} and {b} keep explicit non-penetrative penis contact visible with hands, mouth, or feet."
elif "oral" in slug:
graph = f"{a} kneels and takes {b}'s penis in his mouth while holding his hips."
elif "anal" in slug or "double" in slug or "penetrative" in slug:
graph = f"{a} penetrates {b} anally while {b}'s hips are held open."
elif "threesome" in slug or "group" in slug or "orgy" in slug:
helper = c or any_man({a})
graph = f"{a} penetrates {b} anally while {helper} gives oral contact from the front."
used.add(helper)
elif "cumshot" in slug or "climax" in slug:
graph = f"{a} ejaculates semen over {b}'s body while {b} keeps eye contact and one hand on his penis."
else:
graph = f"{a} and {b} keep explicit penis and anal contact visible."
return graph + support_sentence(used)
# Mixed cast.
woman = any_woman()
man = any_man()
third = any_person({woman, man}) if people_count >= 3 else ""
if "manual_stimulation" in slug:
graph = manual_position_graph(woman, man)
elif "group_coordination" in slug:
graph = group_coordination_graph(woman, man, third)
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
graph = interaction_position_graph(woman, man, third)
elif "foreplay" in slug:
graph = foreplay_position_graph(woman, man)
elif "outercourse" in slug:
graph = outercourse_position_graph(woman, man)
elif "oral" in slug:
graph = oral_position_graph(woman, man)
elif "anal" in slug or "double" in slug:
if "double" in item_text or "toy" in item_text:
if people_count >= 3:
graph = f"{man} thrusts his penis into {woman} while {third} adds a second penetration point from the front."
else:
if "bent-over" in item_text or "bent over" in item_text:
graph = f"{woman} is bent forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
elif "face-down" in item_text:
graph = f"{woman} lies face-down with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
elif "standing" in item_text:
graph = f"{woman} stands braced with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
elif "kneeling" in item_text:
graph = f"{woman} kneels forward with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
else:
graph = f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass."
elif people_count >= 3:
graph = f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front."
else:
graph = anal_position_graph(woman, man)
elif "threesome" in slug:
graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body."
elif "group" in slug or "orgy" in slug:
graph = f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs."
elif "cumshot" in slug or "climax" in slug:
graph = climax_position_graph(woman, man, third)
else:
graph = penetration_position_graph(woman, man)
return graph + support_sentence({woman, man, third} if third else {woman, man})
def _subject_context(
rng: random.Random,
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
women_count: int = 1,
men_count: int = 1,
) -> dict[str, str]:
if subject_type in ("woman", "man", "single_any"):
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black)
if subject_type == "configured_cast":
return _configured_cast_context(women_count, men_count)
if subject_type == "couple":
primary_subject, subject_phrase, pose, effective_women_count, effective_men_count = _couple_type_from_counts(
rng,
women_count,
men_count,
)
return {
"subject_type": "couple",
"subject": primary_subject,
"subject_phrase": subject_phrase,
"age": g.choose(rng, g.COUPLE_AGES),
"body": g.choose(rng, ["slim and average", "curvy and broad", "stocky and curvy", "average and athletic"]),
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "",
"fallback_pose": pose,
"women_count": str(effective_women_count),
"men_count": str(effective_men_count),
"person_count": "2",
}
if subject_type == "group":
eth = "Asian " if ethnicity == "asian" else ""
return {
"subject_type": "group",
"subject": f"mixed {eth}adult group",
"subject_phrase": f"A mixed {eth}adult group of women and men",
"age": g.choose(rng, g.GROUP_AGES),
"body": "diverse",
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "diverse adult body types",
}
return {
"subject_type": subject_type,
"subject": "layout scene",
"subject_phrase": "Adult layout scene",
"age": "adult",
"body": "varied",
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "varied adult figures",
}
def _scene_pool(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
subject_type: str,
location_config: dict[str, Any] | None = None,
) -> list[Any]:
location_config = location_config or {}
location_entries = _list_from(location_config.get("scene_entries"))
if _location_config_active(location_config) and location_config.get("apply_mode") == "replace":
return location_entries
fallback = g.GROUP_SCENES if subject_type in ("group", "configured_cast") else g.SCENES
scene_entries: list[Any] = []
scene_pools = load_scene_pool_library()
item_source = item if isinstance(item, dict) else None
if item_source is not None and _is_false(item_source.get("inherit_scenes")):
sources = (item_source,)
elif _is_false(subcategory.get("inherit_scenes")):
sources = (subcategory, item_source)
else:
sources = (category, subcategory, item_source)
for source in sources:
if not isinstance(source, dict):
continue
if "scenes" in source:
_unique_extend(scene_entries, _list_from(source["scenes"]))
refs = _list_from(source.get("scene_pool")) + _list_from(source.get("scene_pools"))
for ref in refs:
ref_name = str(ref).strip()
if ref_name not in scene_pools:
raise ValueError(f"Unknown scene pool '{ref_name}'")
_unique_extend(scene_entries, scene_pools[ref_name])
if _location_config_active(location_config) and location_config.get("apply_mode") == "add":
_unique_extend(scene_entries, location_entries)
return scene_entries or fallback
def _legacy_scene_entries_for_row(row: dict[str, Any]) -> list[Any]:
subject = str(row.get("primary_subject") or "").lower()
if "group" in subject or "layout" in subject:
return list(g.GROUP_SCENES)
return list(g.SCENES)
def _legacy_scene_text_for_slug(slug: str) -> str:
for entry in list(g.SCENES) + list(g.GROUP_SCENES):
entry_slug, entry_text = _pair_from(entry)
if entry_slug == slug:
return entry_text
return ""
def _apply_location_config_to_legacy_row(
row: dict[str, Any],
location_config: dict[str, Any],
seed_config: dict[str, int],
seed: int,
row_number: int,
) -> dict[str, Any]:
if not _location_config_active(location_config):
return row
location_entries = _list_from(location_config.get("scene_entries"))
if location_config.get("apply_mode") == "add":
choices = _legacy_scene_entries_for_row(row)
_unique_extend(choices, location_entries)
else:
choices = location_entries
scene_rng = _axis_rng(seed_config, "scene", seed, row_number)
scene_slug, scene_text = _choose_pair(scene_rng, choices)
old_slug = str(row.get("scene") or "")
old_text = _legacy_scene_text_for_slug(old_slug)
row["source_scene"] = old_slug
row["source_scene_text"] = old_text
row["scene"] = scene_slug
row["scene_text"] = scene_text
row["location_config"] = location_config
if old_text:
row["prompt"] = str(row.get("prompt") or "").replace(f"Scene: {old_text}.", f"Scene: {scene_text}.")
row["caption"] = str(row.get("caption") or "").replace(f", {old_text},", f", {scene_text},")
else:
row["prompt"] = re.sub(
r"Scene:\s*.*?\.\s*Pose:",
f"Scene: {scene_text}. Pose:",
str(row.get("prompt") or ""),
count=1,
)
return row
def _legacy_composition_entries_for_row(row: dict[str, Any]) -> list[Any]:
subject = str(row.get("primary_subject") or "").lower()
if "group" in subject or "layout" in subject:
return list(g.GROUP_COMPOSITIONS)
return list(g.COMPOSITIONS)
def _apply_composition_config_to_legacy_row(
row: dict[str, Any],
composition_config: dict[str, Any],
seed_config: dict[str, int],
seed: int,
row_number: int,
) -> dict[str, Any]:
if not _composition_config_active(composition_config):
return row
composition_entries = _list_from(composition_config.get("composition_entries"))
if composition_config.get("apply_mode") == "add":
choices = _legacy_composition_entries_for_row(row)
_unique_extend(choices, composition_entries)
else:
choices = composition_entries
composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
new_composition = _choose_text(composition_rng, choices)
old_composition = str(row.get("composition") or "")
old_prompt_fragment = f"Composition: vertical {old_composition}."
new_prompt_fragment = f"Composition: {_composition_prompt(new_composition)}."
row["source_composition"] = old_composition
row["composition"] = new_composition
row["composition_prompt"] = _composition_prompt(new_composition)
row["composition_config"] = composition_config
if old_composition:
row["prompt"] = str(row.get("prompt") or "").replace(old_prompt_fragment, new_prompt_fragment)
row["caption"] = str(row.get("caption") or "").replace(f", {old_composition},", f", {new_composition},")
else:
row["prompt"] = re.sub(
r"Composition:\s*.*?\.\s*Use",
f"{new_prompt_fragment} Use",
str(row.get("prompt") or ""),
count=1,
)
return row
def _sources_with_inheritance(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
inherit_key: str,
) -> tuple[Any, ...]:
item_source = item if isinstance(item, dict) else None
if item_source is not None and _is_false(item_source.get(inherit_key)):
return (item_source,)
if _is_false(subcategory.get(inherit_key)):
return (subcategory, item_source)
return (category, subcategory, item_source)
def _configured_pool(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
direct_key: str,
pool_key: str,
pool_library: dict[str, list[Any]],
inherit_key: str,
) -> list[Any]:
entries: list[Any] = []
singular_pool_key = pool_key[:-1] if pool_key.endswith("s") else pool_key
for source in _sources_with_inheritance(category, subcategory, item, inherit_key):
if not isinstance(source, dict):
continue
if direct_key in source:
_unique_extend(entries, _list_from(source[direct_key]))
refs = _list_from(source.get(singular_pool_key)) + _list_from(source.get(pool_key))
for ref in refs:
ref_name = str(ref).strip()
if ref_name not in pool_library:
raise ValueError(f"Unknown {singular_pool_key} '{ref_name}'")
_unique_extend(entries, pool_library[ref_name])
return entries
def _expression_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> list[Any]:
return _configured_pool(
category,
subcategory,
item,
"expressions",
"expression_pools",
load_expression_pool_library(),
"inherit_expressions",
) or g.EXPRESSIONS
def _expression_intensity_hint(entry: Any) -> float:
if isinstance(entry, dict):
for key in ("expression_intensity", "intensity"):
if key in entry:
return _clamped_float(entry[key], 0.5)
text = _entry_text(entry).lower()
high_terms = (
"ahegao",
"orgasm",
"climax",
"drool",
"drooling",
"tongue out",
"eyes rolled",
"fucked-out",
"cum-smeared",
"saliva",
"gagging",
"slack jaw",
"jaw slack",
"slack-jawed",
"sex-drunk",
"overwhelmed",
"strained",
"messy",
"panting",
"trembling",
"shaking",
"wide open mouth",
"raw ",
"wild ",
"dazed",
"spent",
)
if any(term in text for term in high_terms):
return 0.9
medium_terms = (
"seductive",
"teasing",
"lustful",
"aroused",
"bedroom",
"dominant",
"predatory",
"control",
"stern",
"strict",
"smirk",
"parted lips",
"open-mouthed",
"heated",
"hungry",
"inviting",
"sensual",
"fetish",
"commanding",
"flushed",
"moan",
)
if any(term in text for term in medium_terms):
return 0.62
low_terms = (
"neutral",
"quiet",
"calm",
"reserved",
"relaxed",
"candid",
"closed-mouth",
"thoughtful",
"controlled",
"focused",
"steady",
"bitten-lip",
"braced",
"held breath",
"concentrated",
"aloof",
"bored",
"tired",
"unfocused",
"contented",
"fashion",
"soft",
"sleepy",
"fresh-faced",
)
if any(term in text for term in low_terms):
return 0.25
return 0.5
def _expression_entries_for_intensity(entries: list[Any], expression_intensity: float) -> list[Any]:
target = _clamped_float(expression_intensity, 0.5)
weighted: list[Any] = []
for entry in entries:
entry_intensity = _expression_intensity_hint(entry)
distance = abs(target - entry_intensity)
if distance <= 0.18:
intensity_weight = 4.0
elif distance <= 0.35:
intensity_weight = 1.4
elif distance <= 0.55:
intensity_weight = 0.35
else:
intensity_weight = 0.05
if isinstance(entry, dict):
adjusted = dict(entry)
try:
base_weight = float(adjusted.get("weight", 1.0))
except (TypeError, ValueError):
base_weight = 1.0
adjusted["weight"] = max(0.0, base_weight) * intensity_weight
weighted.append(adjusted)
else:
weighted.append({"text": _entry_text(entry), "weight": intensity_weight})
return weighted or entries
def _pose_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str, poses: str) -> list[Any]:
configured = _merged_field(category, subcategory, item, "poses")
if configured:
return _list_from(configured)
if subject_type == "couple":
return [entry[2] for entry in g.COUPLE_TYPES]
if subject_type in ("layout", "scene"):
return ["clean designed layout"]
return g.EVOCATIVE_ALL if poses == "evocative" else g.POSES
def _composition_pool(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
subject_type: str,
composition_config: dict[str, Any] | None = None,
) -> list[Any]:
composition_config = composition_config or {}
composition_entries = _list_from(composition_config.get("composition_entries"))
if _composition_config_active(composition_config) and composition_config.get("apply_mode") == "replace":
return composition_entries
configured = _configured_pool(
category,
subcategory,
item,
"compositions",
"composition_pools",
load_composition_pool_library(),
"inherit_compositions",
)
if _composition_config_active(composition_config) and composition_config.get("apply_mode") == "add":
configured = list(configured or [])
_unique_extend(configured, composition_entries)
if configured:
return configured
if subject_type in ("group", "configured_cast"):
return g.GROUP_COMPOSITIONS
if subject_type in ("layout", "scene"):
return ["designed illustration layout"]
return g.COMPOSITIONS
def _build_custom_row(
category_choice: str,
subcategory_choice: str,
row_number: int,
start_index: int,
ethnicity: str,
poses: str,
figure: str,
no_plus_women: bool,
no_black: bool,
women_count: int,
men_count: int,
seed: int,
seed_config: dict[str, int],
expression_enabled: bool,
expression_intensity: float,
expression_intensity_source: str = "input",
character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
categories = load_category_library()
category_rng = _axis_rng(seed_config, "category", seed, row_number)
subcategory_rng = _axis_rng(seed_config, "subcategory", seed, row_number)
person_rng = _axis_rng(seed_config, "person", seed, row_number)
scene_rng = _axis_rng(seed_config, "scene", seed, row_number)
pose_rng = _axis_rng(seed_config, "pose", seed, row_number)
role_rng = _axis_rng(seed_config, "role", seed, row_number)
expression_rng = _axis_rng(seed_config, "expression", seed, row_number)
composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
parsed_hardcore_position_config = _parse_hardcore_position_config(hardcore_position_config)
parsed_location_config = _parse_location_config(location_config)
parsed_composition_config = _parse_composition_config(composition_config)
requested_women_count = women_count
requested_men_count = men_count
categories = _filter_hardcore_categories_for_position(
categories,
parsed_hardcore_position_config,
women_count,
men_count,
)
category, subcategory, women_count, men_count = _find_subcategory(
categories,
category_choice,
subcategory_choice,
category_rng,
subcategory_rng,
women_count,
men_count,
)
count_adjustment = {}
if women_count != requested_women_count or men_count != requested_men_count:
count_adjustment = {
"requested_women_count": requested_women_count,
"requested_men_count": requested_men_count,
"effective_women_count": women_count,
"effective_men_count": men_count,
}
if _is_hardcore_sexual_category(category):
subcategory = _apply_hardcore_position_config_to_subcategory(subcategory, parsed_hardcore_position_config)
content_axis = "pose" if _is_pose_content_category(category, subcategory) else "content"
content_rng = _axis_rng(seed_config, content_axis, seed, row_number)
items = _list_from(subcategory.get("items", [subcategory["name"]]))
item = _weighted_choice(content_rng, items)
item_text, item_name, item_axis_values = _compose_item(content_rng, category, subcategory, item, women_count, men_count)
is_pose_category = _is_pose_content_category(category, subcategory)
if is_pose_category:
item_text = _sanitize_hardcore_environment_anchors(item_text)
item_axis_values = _sanitize_hardcore_axis_values(item_axis_values)
subject_type = str(_merged_field(category, subcategory, item, "subject_type", "single_any"))
context = _subject_context(person_rng, subject_type, ethnicity, figure, no_plus_women, no_black, women_count, men_count)
character_slots = _parse_character_cast(character_cast)
character_slot_map = _character_slot_label_map(character_slots)
applied_slot: dict[str, Any] = {}
slot_status = "none"
if context.get("subject_type") in ("woman", "man"):
slot_label = "Woman A" if context["subject_type"] == "woman" else "Man A"
if slot_label in character_slot_map:
context, applied_slot = _character_context_for_label(
slot_label,
character_slot_map,
person_rng,
ethnicity,
figure,
no_plus_women,
no_black,
)
slot_status = f"applied:{slot_label}"
applied_profile, profile_status = {}, "skipped_character_slot"
else:
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
else:
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
subject_type = context["subject_type"]
pov_character_labels = (
_pov_character_labels(character_slot_map, men_count)
if subject_type == "configured_cast"
else []
)
source_role_graph = _role_graph(role_rng, subcategory, context, item_axis_values, pov_character_labels)
if is_pose_category:
source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph)
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
cast_descriptors: list[str] = []
cast_descriptor_text = ""
expression_intensity_source = expression_intensity_source or "input"
expression_disabled = not bool(expression_enabled)
if expression_disabled:
expression_intensity_source = "disabled"
elif subject_type in ("woman", "man") and applied_slot:
slot_label = "Woman A" if subject_type == "woman" else "Man A"
if not _slot_expression_enabled(applied_slot):
expression_disabled = True
expression_intensity_source = f"character_slot:{slot_label}:disabled"
else:
slot_expression_intensity = _slot_expression_intensity_for_phase(applied_slot, expression_phase)
if slot_expression_intensity is not None:
expression_intensity = slot_expression_intensity
expression_intensity_source = f"character_slot:{slot_label}"
elif subject_type == "configured_cast" and character_slots:
expression_intensity, expression_intensity_source = _cast_expression_intensity_override(
expression_intensity,
character_slot_map,
women_count,
men_count,
expression_phase,
)
if expression_intensity is None:
expression_disabled = True
if subject_type == "configured_cast" and character_slots:
cast_descriptors, _descriptor_slots = _cast_descriptor_entries(
seed_config,
seed,
row_number,
ethnicity,
figure,
no_plus_women,
no_black,
women_count,
men_count,
character_slots,
)
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
scene_slug, scene = _choose_pair(
scene_rng,
_compatible_entries(
_scene_pool(category, subcategory, item, subject_type, parsed_location_config),
women_count,
men_count,
),
)
pose = str(_merged_field(category, subcategory, item, "pose", "") or context.get("fallback_pose") or _choose_text(
pose_rng, _compatible_entries(_pose_pool(category, subcategory, item, subject_type, poses), women_count, men_count)
))
if is_pose_category:
pose = _sanitize_hardcore_environment_anchors(pose)
expression_pool = _expression_pool(category, subcategory, item)
if expression_disabled:
expression = ""
else:
expression_entries = _compatible_entries(
_expression_entries_for_intensity(expression_pool, expression_intensity),
women_count,
men_count,
)
expression = _choose_text(expression_rng, expression_entries)
if subject_type in ("couple", "group") and ";" not in expression:
secondary_expression = _choose_distinct_text(expression_rng, expression_entries, expression)
if secondary_expression:
expression = f"{expression}; {secondary_expression}"
shared_expression = expression
character_expressions: list[str] = []
character_expression_text = ""
if not expression_disabled and subject_type == "configured_cast" and character_slots:
character_expressions = _character_expression_entries(
expression_rng,
expression_pool,
expression_intensity,
character_slot_map,
women_count,
men_count,
expression_phase,
)
character_expression_text = "; ".join(character_expressions)
character_expression_text = _sanitize_character_expression_text_for_action(
character_expression_text,
source_role_graph,
item,
item_axis_values,
)
character_expressions = [part.strip() for part in character_expression_text.split(";") if part.strip()]
if character_expression_text:
expression = character_expression_text
source_composition = _choose_text(
composition_rng,
_compatible_entries(
_composition_pool(category, subcategory, item, subject_type, parsed_composition_config),
women_count,
men_count,
),
)
if is_pose_category:
source_composition = _sanitize_hardcore_environment_anchors(source_composition)
composition = _pov_composition_prompt(source_composition, pov_character_labels)
negative_prompt = str(_merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT))
positive_suffix = str(_merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX))
style = str(
_merged_field(
category,
subcategory,
item,
"style",
"sexy but tasteful adult pin-up coloured-pencil comic illustration",
)
)
item_label = str(_merged_field(category, subcategory, item, "item_label", category["name"]))
context.update(
{
"trigger": g.TRIGGER,
"main_category": category["name"],
"subcategory": subcategory["name"],
"category": category["name"],
"item": item_text,
"item_name": item_name,
"item_label": item_label,
"style": style,
"scene": scene,
"scene_slug": scene_slug,
"pose": pose,
"expression": expression,
"shared_expression": shared_expression,
"character_expressions": character_expressions,
"character_expression_text": character_expression_text,
"expression_enabled": not expression_disabled,
"expression_disabled": expression_disabled,
"expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source,
"composition": composition,
"source_composition": source_composition,
"composition_prompt": _composition_prompt(composition),
"composition_config": parsed_composition_config if _composition_config_active(parsed_composition_config) else {},
"role_graph": role_graph,
"source_role_graph": source_role_graph,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
"cast_descriptors": cast_descriptor_text,
"positive_suffix": positive_suffix,
"negative_prompt": negative_prompt,
}
)
if isinstance(item, dict) and "prompt_template" in item:
template = str(item["prompt_template"])
else:
template = str(subcategory.get("prompt_template") or category.get("prompt_template") or "")
if not template:
if subject_type in ("woman", "man"):
template = SINGLE_TEMPLATE
elif subject_type == "couple":
template = COUPLE_TEMPLATE
elif subject_type == "group":
template = GROUP_TEMPLATE
else:
template = LAYOUT_TEMPLATE
caption_template = str(
(item.get("caption_template") if isinstance(item, dict) else None)
or subcategory.get("caption_template")
or category.get("caption_template")
or "{trigger}, {subject_phrase}, {age}, {item}, {scene}, {composition}, coloured pencil comic illustration"
)
prompt = _format(template, context)
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in template:
prompt = _insert_positive_directive(prompt, f"Characters: {cast_descriptor_text}.")
if subject_type == "configured_cast" and pov_character_labels:
prompt = _insert_positive_directive(prompt, _pov_prompt_directive(pov_character_labels))
caption = _format(caption_template, context)
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in caption_template:
caption = f"{caption.rstrip()}, {cast_descriptor_text}"
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
index = start_index + row_number - 1
row = g.row_base(index, batch, context["subject"], context["age"], context["body"], scene_slug, composition)
row.update(
{
"prompt": prompt,
"caption": caption,
"negative_prompt": negative_prompt,
"expression": expression,
"main_category": category["name"],
"subcategory": subcategory["name"],
"category_slug": category["slug"],
"subcategory_slug": subcategory["slug"],
"subject_type": subject_type,
"subject_phrase": context.get("subject_phrase", ""),
"body_phrase": context.get("body_phrase", ""),
"skin": context.get("skin", ""),
"hair": context.get("hair", ""),
"eyes": context.get("eyes", ""),
"style": style,
"item": item_text,
"item_label": item_label,
"positive_suffix": positive_suffix,
"custom_item": item_name,
"item_axis_values": item_axis_values,
"scene_text": scene,
"location_config": parsed_location_config if _location_config_active(parsed_location_config) else {},
"pose": pose,
"seed_config": seed_config,
"hardcore_position_config": (
parsed_hardcore_position_config
if _hardcore_position_config_active(parsed_hardcore_position_config)
else {}
),
"content_seed_axis": content_axis,
"role_graph": role_graph,
"source_role_graph": source_role_graph,
"source_composition": source_composition,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
"shared_expression": shared_expression,
"character_expressions": character_expressions,
"character_expression_text": character_expression_text,
"expression_enabled": not expression_disabled,
"expression_disabled": expression_disabled,
"cast_summary": context.get("cast_summary", ""),
"cast_descriptors": cast_descriptors,
"cast_descriptor_text": cast_descriptor_text,
"scene_kind": context.get("scene_kind", ""),
"women_count": context.get("women_count", ""),
"men_count": context.get("men_count", ""),
"person_count": context.get("person_count", ""),
"cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {},
"character_profile": applied_profile,
"character_profile_status": profile_status,
"character_slot": applied_slot,
"character_slot_status": slot_status,
"character_cast_slots": character_slots,
"expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source,
"source": "json_category",
}
)
if context.get("figure"):
row["figure"] = context["figure"]
if expression_disabled:
row = _disable_row_expression(row, expression_intensity_source)
return row
def build_prompt(
category: str,
subcategory: str,
row_number: int,
start_index: int,
seed: int,
clothing: str,
ethnicity: str,
poses: str,
backside_bias: float,
figure: str,
no_plus_women: bool,
no_black: bool,
minimal_clothing_ratio: float,
standard_pose_ratio: float,
trigger: str,
prepend_trigger_to_prompt: bool,
extra_positive: str,
extra_negative: str,
seed_config: str | dict[str, Any] | None = None,
women_count: int = 1,
men_count: int = 1,
camera_config: str | dict[str, Any] | None = None,
expression_intensity: float = 0.5,
character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_enabled: bool = True,
expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
apply_pool_extensions()
row_number = max(1, int(row_number))
start_index = max(1, int(start_index))
seed = int(seed)
ethnicity = normalize_ethnicity_filter(ethnicity, "any")
expression_enabled = not _is_false(expression_enabled)
minimal_ratio = _ratio_or_none(minimal_clothing_ratio)
pose_ratio = _ratio_or_none(standard_pose_ratio)
parsed_seed_config = _parse_seed_config(seed_config)
parsed_location_config = _parse_location_config(location_config)
parsed_composition_config = _parse_composition_config(composition_config)
content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number)
pose_axis_rng = _axis_rng(parsed_seed_config, "pose", seed, row_number)
person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
expression_rng = _axis_rng(parsed_seed_config, "expression", seed, row_number)
clothing = clothing if clothing in ("full", "minimal", "random") else "full"
poses = poses if poses in ("standard", "evocative", "random") else "standard"
figure = figure if figure in ("curvy", "balanced", "bombshell", "random") else "curvy"
clothing = _pick_clothing_mode(content_rng, clothing, minimal_ratio)
poses = _pick_pose_mode(pose_axis_rng, poses, pose_ratio)
figure = _pick_figure_bias(person_rng, figure)
minimal_ratio = None
pose_ratio = None
expression_intensity, expression_intensity_source = _pick_expression_intensity(expression_rng, expression_intensity)
exact_custom_subcategory = bool(subcategory and subcategory != RANDOM_SUBCATEGORY and " / " in subcategory)
if category == "auto_full" and not exact_custom_subcategory:
category = _auto_full_choice(parsed_seed_config, seed, row_number)
if category == "auto_weighted" and not exact_custom_subcategory:
row = _build_auto_weighted_row(
row_number,
start_index,
clothing,
ethnicity,
poses,
float(backside_bias),
figure,
bool(no_plus_women),
bool(no_black),
minimal_ratio,
pose_ratio,
seed,
)
elif category in ("woman", "man", "couple", "group_or_layout") and not exact_custom_subcategory:
row = _build_direct_builtin_row(
category,
row_number,
start_index,
clothing,
ethnicity,
poses,
float(backside_bias),
figure,
bool(no_plus_women),
bool(no_black),
minimal_ratio,
pose_ratio,
seed,
)
else:
row = _build_custom_row(
category,
subcategory,
row_number,
start_index,
ethnicity,
poses,
figure,
bool(no_plus_women),
bool(no_black),
int(women_count),
int(men_count),
seed,
parsed_seed_config,
expression_enabled,
expression_intensity,
expression_intensity_source,
character_profile,
character_cast,
expression_phase,
hardcore_position_config,
parsed_location_config,
parsed_composition_config,
)
if row.get("source") == "built_in_generator":
row = _apply_location_config_to_legacy_row(
row,
parsed_location_config,
parsed_seed_config,
seed,
row_number,
)
row = _apply_composition_config_to_legacy_row(
row,
parsed_composition_config,
parsed_seed_config,
seed,
row_number,
)
if not expression_enabled:
row = _disable_row_expression(row, "disabled")
if extra_positive.strip():
row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}"
row = _apply_camera_config(row, camera_config)
active_trigger = trigger.strip() or g.TRIGGER
row["prompt"] = _prepend_trigger(row["prompt"], active_trigger, bool(prepend_trigger_to_prompt))
row["prompt"] = sanitize_prompt_text(row["prompt"], triggers=(active_trigger,))
row["caption"] = sanitize_caption_text(row.get("caption", ""), triggers=(active_trigger,))
row["negative_prompt"] = sanitize_negative_text(
_combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative)
)
row["trigger"] = active_trigger
row.setdefault("expression_intensity", expression_intensity)
row.setdefault("expression_intensity_source", expression_intensity_source)
return row
def build_prompt_from_configs(
row_number: int,
start_index: int,
seed: int,
category_config: str | dict[str, Any] | None = "",
cast_config: str | dict[str, Any] | None = "",
generation_profile: str | dict[str, Any] | None = "",
filter_config: str | dict[str, Any] | None = "",
seed_config: str | dict[str, Any] | None = "",
camera_config: str | dict[str, Any] | None = "",
character_profile: str | dict[str, Any] | None = "",
character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
category, subcategory = _parse_category_config(category_config)
cast = _parse_cast_config(cast_config)
profile = _parse_generation_profile(generation_profile)
filters = _parse_filter_config(filter_config)
return build_prompt(
category=category,
subcategory=subcategory,
row_number=row_number,
start_index=start_index,
seed=seed,
clothing=profile["clothing"],
ethnicity=filters["ethnicity"],
poses=profile["poses"],
expression_enabled=profile["expression_enabled"],
expression_intensity=profile["expression_intensity"],
backside_bias=profile["backside_bias"],
figure=filters["figure"],
no_plus_women=filters["no_plus_women"],
no_black=filters["no_black"],
women_count=int(cast["women_count"]),
men_count=int(cast["men_count"]),
minimal_clothing_ratio=profile["minimal_clothing_ratio"],
standard_pose_ratio=profile["standard_pose_ratio"],
trigger=profile["trigger"],
prepend_trigger_to_prompt=profile["prepend_trigger_to_prompt"],
extra_positive=extra_positive or "",
extra_negative=extra_negative or "",
seed_config=seed_config or "",
camera_config=camera_config or "",
character_profile=character_profile or "",
character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
)
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",
]
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 []
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 []
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",
) -> 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) -> 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"]
)
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_camera_config_with_detail(camera_config: dict[str, Any], camera_detail: str) -> dict[str, Any]:
if camera_detail in CAMERA_DETAIL_CHOICES:
camera_config["camera_detail"] = camera_detail
return camera_config
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
def _insta_of_descriptor(row: dict[str, Any]) -> str:
return _descriptor_from_parts(
"woman",
row.get("age_band") or row.get("age"),
row.get("body_phrase"),
row.get("skin"),
row.get("hair"),
row.get("eyes"),
row.get("descriptor_detail"),
)
def _insta_of_descriptor_from_context(context: dict[str, Any]) -> str:
subject = str(context.get("subject") or context.get("subject_type") or "person").strip()
return _descriptor_from_parts(
subject,
context.get("age"),
context.get("body_phrase"),
context.get("skin"),
context.get("hair"),
context.get("eyes"),
context.get("descriptor_detail"),
)
def _insta_of_cast_descriptors(
primary_descriptor: str,
seed_config: dict[str, int],
seed: int,
row_number: int,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
women_count: int,
men_count: int,
character_cast: str | dict[str, Any] | list[Any] | None = "",
) -> list[str]:
descriptors, _slots = _cast_descriptor_entries(
seed_config,
seed,
row_number,
ethnicity,
figure,
no_plus_women,
no_black,
women_count,
men_count,
character_cast,
primary_descriptor=primary_descriptor,
)
return descriptors
def _insta_of_cast_phrase(women_count: int, men_count: int) -> str:
context = _configured_cast_context(women_count, men_count)
return context["cast_summary"]
def _insta_of_prompt_cast_descriptors(text: str) -> str:
return str(text or "").replace("Woman A / primary creator:", "Woman A:")
SOFTCORE_CAST_POSES = [
"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 _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
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)
def _insta_of_softcore_item_prompt_label(level: str) -> str:
return "Body exposure" if level == "explicit_nude" else "Outfit"
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)
WOMAN_LOWER_ACCESS_TERMS = (
"penetrat",
"thrust",
"vaginal",
"anal",
"rear-entry",
"rear entry",
"front-and-back",
"front and back",
"double",
"doggy",
"missionary",
"cowgirl",
"straddles",
"hips aligned",
"penis into",
"penis inside",
"penis entering",
"mouth on her pussy",
"mouth pressed to her pussy",
"pussy licking",
"cunnilingus",
"thighs spread",
"thighs open",
"legs spread",
"legs open",
"cum on pussy",
"cum across her pussy",
"cum dripping from pussy",
"cum dripping from ass",
"cum on belly",
"cum on thighs",
"cum across her ass",
"cum across her lower back",
"toy aligned",
"second penetration point",
)
WOMAN_UPPER_ACCESS_TERMS = (
"boobjob",
"titjob",
"breast sex",
"breasts around",
"breasts tightly",
"hands pressing both breasts",
"breasts together",
"cum on breasts",
"cum across her breasts",
"cum on chest",
)
MAN_LOWER_ACCESS_TERMS = (
"penis",
"glans",
"testicle",
"balls",
"cumshot",
"ejaculat",
"semen",
"boobjob",
"titjob",
"breast sex",
"footjob",
"handjob",
"hand job",
"hand wrapped",
"hand stroking",
"blowjob",
"fellatio",
"penis sucking",
"penis in mouth",
"mouth on penis",
"penis licking",
)
LOWER_BODY_CLOTHING_TERMS = (
"panty",
"panties",
"brief",
"briefs",
"thong",
"bottom",
"bottoms",
"bodysuit",
"teddy",
"dress",
"skirt",
"shorts",
"jeans",
"trousers",
"pants",
"bikini",
"towel",
"sheet",
"blanket",
)
UPPER_BODY_CLOTHING_TERMS = (
"bra",
"cup",
"cups",
"corset",
"bodysuit",
"bustier",
"top",
"camisole",
"shirt",
"blouse",
"bodice",
"dress",
"robe",
"jacket",
"sweater",
"harness",
"chest",
"cleavage",
"panel",
"panels",
)
INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS = [
"wears an open button shirt with jeans lowered below the hips for genital access",
"wears a fitted tee pushed up with trousers lowered below the hips",
"keeps a dark shirt on while pants and underwear are pulled down below the hips",
"wears an open overshirt with jeans pushed down at the thighs",
"wears a hoodie lifted at the waist with sweatpants lowered below the hips",
"wears gym shorts pulled down below the hips with his shirt still on",
"keeps a casual shirt on with belt open and pants lowered below the hips",
"wears a half-open shirt with lower garments pushed down below the hips",
]
INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE = [
"wears an open button shirt with jeans unfastened",
"wears a fitted tee with pants opened at the waist",
"keeps a dark shirt on with trousers loosened",
"wears an open overshirt with jeans partly lowered",
"wears gym shorts loose at the waist with a towel nearby",
"wears a hoodie lifted at the waist with sweatpants loosened",
"wears a casual shirt with belt open and pants partly lowered",
"wears a half-open shirt with dark trousers",
]
def _hardcore_row_access_flags(row: dict[str, Any]) -> dict[str, bool]:
axis_values = row.get("item_axis_values")
axis_text = " ".join(str(value) for value in axis_values.values()) if isinstance(axis_values, dict) else ""
role_text = " ".join(
str(part or "")
for part in (
row.get("source_role_graph"),
row.get("role_graph"),
)
).lower()
detail_text = " ".join(
str(part or "")
for part in (
row.get("item"),
row.get("source_composition"),
row.get("composition"),
axis_text,
)
).lower()
full_text = f"{role_text} {detail_text}"
return {
"woman_lower": any(term in role_text for term in WOMAN_LOWER_ACCESS_TERMS),
"woman_upper": any(term in full_text for term in WOMAN_UPPER_ACCESS_TERMS),
"man_lower": any(term in role_text for term in MAN_LOWER_ACCESS_TERMS),
}
def _outfit_without_lower_body_blockers(outfit: str) -> str:
text = str(outfit or "").strip()
if not text:
return ""
text = re.sub(r"\blingerie set\b", "lingerie top details", text, flags=re.IGNORECASE)
text = re.sub(r"\bbrief set\b", "bra set", text, flags=re.IGNORECASE)
text = re.sub(r"\bbodysuit with\b", "upper bodysuit detail with", text, flags=re.IGNORECASE)
fragments = re.split(r"\s*,\s*|\s+\band\b\s+|\s+\bwith\b\s+|\s+\bunder\b\s+|\s+\bover\b\s+", text)
kept = []
for fragment in fragments:
fragment = fragment.strip(" ,.;")
fragment = re.sub(r"^(?:and|with|under|over)\s+", "", fragment, flags=re.IGNORECASE)
if not fragment:
continue
lower = fragment.lower()
if any(term in lower for term in LOWER_BODY_CLOTHING_TERMS):
continue
kept.append(fragment)
if not kept:
return ""
deduped = []
seen = set()
for fragment in kept:
key = re.sub(r"\W+", " ", fragment.lower()).strip()
if key and key not in seen:
deduped.append(fragment)
seen.add(key)
return ", ".join(deduped)
def _outfit_without_upper_body_blockers(outfit: str) -> str:
text = str(outfit or "").strip()
if not text:
return ""
text = re.sub(r"\blingerie set\b", "lingerie styling", text, flags=re.IGNORECASE)
text = re.sub(r"\bbalconette bra and brief set\b", "briefs and garter styling", text, flags=re.IGNORECASE)
fragments = re.split(r"\s*,\s*|\s+\band\s+|\s+\bwith\s+|\s+\bunder\s+|\s+\bover\s+", text)
kept = []
for fragment in fragments:
fragment = fragment.strip(" ,.;")
fragment = re.sub(r"^(?:and|with|under|over)\s+", "", fragment, flags=re.IGNORECASE)
if not fragment:
continue
lower = fragment.lower()
if any(term in lower for term in UPPER_BODY_CLOTHING_TERMS):
continue
kept.append(fragment)
if not kept:
return ""
deduped = []
seen = set()
for fragment in kept:
key = re.sub(r"\W+", " ", fragment.lower()).strip()
if key and key not in seen:
deduped.append(fragment)
seen.add(key)
return ", ".join(deduped)
def _insta_of_hardcore_clothing_state(mode: str, softcore_outfit: str, woman_access: str = "") -> str:
mode = mode if mode in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY else "none"
outfit = str(softcore_outfit or "").strip()
if mode == "none" or not outfit:
return ""
base = INSTA_OF_HARDCORE_CLOTHING_CONTINUITY[mode]
if mode == "explicit_nude":
return f"Body exposure: {base}."
if mode == "implied_nude":
return f"Body exposure: {base}."
if mode == "partially_removed" and woman_access == "lower":
detail = _outfit_without_lower_body_blockers(outfit)
base = (
"Woman A's lower body is clear; any lower garment is pulled aside or removed below the hips"
)
if detail:
return f"Clothing state: {base}; visible remaining styling: {detail}."
return f"Clothing state: {base}."
if mode == "partially_removed" and woman_access == "upper":
detail = _outfit_without_upper_body_blockers(outfit)
base = (
"Woman A's breasts and upper body are clear; any bra cup, bodice, or top panel is pulled aside or removed"
)
if detail:
return f"Clothing state: {base}; visible remaining styling: {detail}."
return f"Clothing state: {base}."
if mode == "partially_removed":
return f"Clothing state: Woman A keeps the outfit mostly on; teaser outfit detail: {outfit}."
return f"Clothing state: {base}; teaser outfit detail: {outfit}."
def _default_man_hardcore_clothing_entries(
men_count: int,
pov_labels: list[str] | None,
configured_entries: list[str],
rng: random.Random,
needs_lower_access: bool,
) -> list[str]:
pov_set = set(pov_labels or [])
configured_labels = {
match.group(1)
for entry in configured_entries
for match in [re.match(r"^\s*(Man [A-Z])\b", str(entry or ""))]
if match
}
pool = INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS if needs_lower_access else INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE
entries = []
for index in range(max(0, int(men_count))):
label = f"Man {chr(ord('A') + index)}"
if label in pov_set or label in configured_labels:
continue
entries.append(_hardcore_clothing_sentence(label, g.choose(rng, pool)))
return entries
def _insta_of_partner_styling(
seed_config: dict[str, int],
seed: int,
row_number: int,
women_count: int,
men_count: int,
pov_labels: list[str] | None = None,
label_map: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
content_rng = _axis_rng(seed_config, "content", seed, row_number + 421)
pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421)
pov_set = set(pov_labels or [])
outfits: list[str] = []
for index in range(max(0, women_count - 1)):
label = chr(ord("B") + index)
full_label = f"Woman {label}"
outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)
sentence = _softcore_outfit_sentence(full_label, outfit)
if sentence:
outfits.append(sentence)
for index in range(max(0, men_count)):
label = chr(ord("A") + index)
full_label = f"Man {label}"
if full_label in pov_set:
continue
outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)
sentence = _softcore_outfit_sentence(full_label, outfit)
if sentence:
outfits.append(sentence)
return {
"outfits": outfits,
"pose": g.choose(pose_rng, SOFTCORE_CAST_POSES),
}
def _insta_of_active_trigger(prompt: str, trigger: str, enabled: bool) -> str:
return _prepend_trigger(prompt, trigger, enabled)
def build_insta_of_pair(
row_number: int,
start_index: int,
seed: int,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
trigger: str,
prepend_trigger_to_prompt: bool,
seed_config: str | dict[str, Any] | None = None,
options_json: str | dict[str, Any] | None = None,
filter_config: str | dict[str, Any] | None = None,
camera_config: str | dict[str, Any] | None = None,
softcore_camera_config: str | dict[str, Any] | None = None,
hardcore_camera_config: str | dict[str, Any] | None = None,
character_profile: str | dict[str, Any] | None = "",
character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
options = _parse_insta_of_options(options_json)
if filter_config:
filters = _parse_filter_config(filter_config)
ethnicity = filters["ethnicity"]
figure = filters["figure"]
no_plus_women = filters["no_plus_women"]
no_black = filters["no_black"]
hard_women_count, hard_men_count = _insta_of_hardcore_counts(options)
active_trigger = trigger.strip() or g.TRIGGER
parsed_seed_config = _parse_seed_config(seed_config)
character_slots = _parse_character_cast(character_cast)
character_slot_map = _character_slot_label_map(character_slots)
pov_character_labels = _pov_character_labels(character_slot_map, hard_men_count)
softcore_level_key = str(options["softcore_level"])
soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key)
soft_content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number + 311)
hard_content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number + 317)
soft_person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
soft_expression_women_count = hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1
soft_expression_men_count = hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0
soft_expression_enabled = bool(options["softcore_expression_enabled"])
soft_expression_intensity = options["softcore_expression_intensity"]
soft_expression_intensity_source = "input"
if soft_expression_enabled:
soft_expression_intensity, soft_expression_intensity_source = _cast_expression_intensity_override(
options["softcore_expression_intensity"],
character_slot_map,
soft_expression_women_count,
soft_expression_men_count,
"softcore",
)
if soft_expression_intensity is None:
soft_expression_enabled = False
else:
soft_expression_intensity_source = "disabled"
primary_slot_context = None
primary_slot = character_slot_map.get("Woman A")
if primary_slot:
primary_slot_context = _context_from_character_slot(
soft_person_rng,
primary_slot,
"woman",
ethnicity,
figure,
no_plus_women,
no_black,
)
soft_row = build_prompt(
category=soft_category,
subcategory=soft_subcategory,
row_number=row_number,
start_index=start_index,
seed=seed,
clothing="minimal",
ethnicity=ethnicity,
poses="evocative",
backside_bias=0.0,
figure=figure,
no_plus_women=no_plus_women,
no_black=no_black,
minimal_clothing_ratio=-1,
standard_pose_ratio=-1,
trigger=active_trigger,
prepend_trigger_to_prompt=False,
extra_positive="",
extra_negative="",
seed_config=parsed_seed_config,
women_count=1,
men_count=0,
expression_enabled=soft_expression_enabled,
expression_intensity=soft_expression_intensity,
character_profile="" if primary_slot else character_profile or "",
character_cast="",
location_config=location_config or "",
composition_config=composition_config or "",
)
soft_row["expression_intensity_source"] = soft_expression_intensity_source
if primary_slot_context:
soft_row = _apply_character_context_to_row(soft_row, primary_slot_context)
soft_row["character_slot"] = primary_slot
soft_row["character_slot_status"] = "applied:Woman A"
if not soft_expression_enabled:
soft_row = _disable_row_expression(soft_row, soft_expression_intensity_source)
primary_softcore_outfit = _slot_softcore_outfit(primary_slot, soft_content_rng)
soft_row["item"] = primary_softcore_outfit or _insta_of_softcore_outfit(soft_content_rng, softcore_level_key)
soft_row["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key)
soft_row["item_label"] = "Insta/OF softcore body exposure" if softcore_level_key == "explicit_nude" else "Insta/OF softcore outfit"
soft_row["softcore_item_prompt_label"] = _insta_of_softcore_item_prompt_label(softcore_level_key)
soft_row["custom_item"] = "insta_of_softcore_outfit"
soft_row["softcore_outfit_policy"] = "character_slot:Woman A" if primary_softcore_outfit else "insta_of_safe_softcore"
if softcore_level_key == "explicit_nude":
soft_row["source_scene_text"] = soft_row.get("source_scene_text") or soft_row.get("scene_text", "")
soft_row["scene_text"] = _body_exposure_scene_text(soft_row.get("scene_text", ""))
soft_row["pov_character_labels"] = (
pov_character_labels
if options["softcore_cast"] == "same_as_hardcore"
else []
)
soft_row["pov_prompt_directive"] = _pov_prompt_directive(soft_row["pov_character_labels"])
if soft_row["pov_character_labels"]:
soft_row["source_composition"] = soft_row.get("source_composition") or soft_row.get("composition", "")
soft_row["composition"] = _pov_composition_prompt(
soft_row["source_composition"],
soft_row["pov_character_labels"],
)
hard_row = build_prompt(
category="Hardcore sexual poses",
subcategory=RANDOM_SUBCATEGORY,
row_number=row_number,
start_index=start_index,
seed=seed,
clothing="minimal",
ethnicity=ethnicity,
poses="evocative",
backside_bias=0.0,
figure=figure,
no_plus_women=no_plus_women,
no_black=no_black,
minimal_clothing_ratio=-1,
standard_pose_ratio=-1,
trigger=active_trigger,
prepend_trigger_to_prompt=False,
extra_positive="",
extra_negative="",
seed_config=parsed_seed_config,
women_count=hard_women_count,
men_count=hard_men_count,
expression_enabled=options["hardcore_expression_enabled"],
expression_intensity=options["hardcore_expression_intensity"],
character_cast=character_cast or "",
expression_phase="hardcore",
hardcore_position_config=hardcore_position_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
)
hard_row["hardcore_detail_density"] = options["hardcore_detail_density"]
hard_row["pov_character_labels"] = pov_character_labels
hard_row["pov_prompt_directive"] = _pov_prompt_directive(pov_character_labels)
descriptor = _insta_of_descriptor(soft_row)
cast_descriptors = _insta_of_cast_descriptors(
descriptor,
parsed_seed_config,
seed,
row_number,
ethnicity,
figure,
no_plus_women,
no_black,
hard_women_count,
hard_men_count,
character_slots,
)
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
soft_cast_descriptor_text = (
cast_descriptor_text
if options["softcore_cast"] == "same_as_hardcore"
else f"Woman A: {descriptor}"
)
soft_partner_styling = _insta_of_partner_styling(
parsed_seed_config,
seed,
row_number,
hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1,
hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0,
pov_character_labels if options["softcore_cast"] == "same_as_hardcore" else [],
character_slot_map,
)
if options["softcore_cast"] != "same_as_hardcore":
soft_partner_styling = {"outfits": [], "pose": ""}
soft_partner_outfit_text = "; ".join(soft_partner_styling["outfits"])
platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]]
soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]]
hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]]
hard_camera_mode = options["hardcore_camera_mode"]
soft_camera_source = softcore_camera_config or camera_config
hard_camera_source = hardcore_camera_config or camera_config
if hard_camera_mode == "same_as_softcore":
hard_camera_mode = options["softcore_camera_mode"]
hard_camera_source = soft_camera_source
soft_camera_config = _camera_config_with_mode(soft_camera_source, options["softcore_camera_mode"])
hard_camera_config = _camera_config_with_mode(hard_camera_source, hard_camera_mode)
soft_camera_config = _insta_camera_config_with_detail(soft_camera_config, options["camera_detail"])
hard_camera_config = _insta_camera_config_with_detail(hard_camera_config, options["camera_detail"])
soft_camera_directive, soft_camera_config = _camera_directive(soft_camera_config)
hard_camera_directive, hard_camera_config = _camera_directive(hard_camera_config)
soft_subject_kind = "woman" if options["softcore_cast"] == "solo" else "subjects"
hard_subject_kind = "couple" if hard_women_count + hard_men_count == 2 else "subjects"
soft_row = _apply_coworking_composition(soft_row, soft_subject_kind)
hard_row = _apply_coworking_composition(hard_row, hard_subject_kind)
hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"]
if hard_scene != hard_row.get("scene_text"):
hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "")
hard_row["scene_text"] = hard_scene
hard_composition = _coworking_composition_prompt(hard_scene, hard_row["composition"], hard_subject_kind)
if hard_composition != hard_row["composition"]:
hard_row["source_composition"] = hard_row.get("source_composition") or hard_row["composition"]
hard_row["composition"] = hard_composition
hard_row["composition_prompt"] = _composition_prompt(hard_composition)
soft_pov_camera_labels = (
pov_character_labels
if options["softcore_cast"] == "same_as_hardcore"
else []
)
soft_camera_scene_directive, soft_camera_config = _camera_scene_directive_for_context(
soft_row.get("scene_text"),
soft_row.get("composition"),
soft_camera_config,
soft_pov_camera_labels,
soft_subject_kind,
)
hard_camera_scene_directive, hard_camera_config = _camera_scene_directive_for_context(
hard_scene,
hard_composition,
hard_camera_config,
pov_character_labels,
hard_subject_kind,
)
if soft_pov_camera_labels:
soft_camera_directive = ""
if pov_character_labels:
hard_camera_directive = ""
soft_row["camera_config"] = soft_camera_config
soft_row["camera_directive"] = soft_camera_directive
soft_row["camera_scene_directive"] = soft_camera_scene_directive
hard_row["camera_config"] = hard_camera_config
hard_row["camera_directive"] = hard_camera_directive
hard_row["camera_scene_directive"] = hard_camera_scene_directive
soft_camera_scene_sentence = f"{soft_camera_scene_directive} " if soft_camera_scene_directive else ""
hard_camera_scene_sentence = f"{hard_camera_scene_directive} " if hard_camera_scene_directive else ""
soft_camera_sentence = f"Camera control: {soft_camera_directive} " if soft_camera_directive else ""
hard_camera_sentence = f"Camera control: {hard_camera_directive} " if hard_camera_directive else ""
soft_cast = (
"solo creator setup with Woman A alone"
if options["softcore_cast"] == "solo"
else f"soft creator-teaser setup with {_insta_of_cast_phrase(hard_women_count, hard_men_count)}"
)
soft_cast_presence = (
(
"Frame Woman A from the POV participant's first-person camera in a soft creator-teaser setup; "
"keep the POV participant off-camera as the viewpoint and implied by camera perspective or foreground cues. "
)
if options["softcore_cast"] == "same_as_hardcore" and pov_character_labels
else (
"Place Woman A and the listed partners together in a soft creator-teaser pose. "
if options["softcore_cast"] == "same_as_hardcore"
else "Keep the softcore version focused on Woman A alone. "
)
)
soft_cast_styling_sentence = (
f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. "
if options["softcore_cast"] == "same_as_hardcore" and soft_partner_outfit_text
else ""
)
hard_cast = _insta_of_cast_phrase(hard_women_count, hard_men_count)
character_hardcore_clothing_entries = _character_hardcore_clothing_entries(
character_slot_map,
hard_women_count,
hard_men_count,
pov_character_labels,
hard_content_rng,
)
access_flags = _hardcore_row_access_flags(hard_row)
woman_access = "lower" if access_flags["woman_lower"] else "upper" if access_flags["woman_upper"] else ""
default_man_hardcore_clothing_entries = _default_man_hardcore_clothing_entries(
hard_men_count,
pov_character_labels,
character_hardcore_clothing_entries,
hard_content_rng,
access_flags["man_lower"],
)
has_primary_hardcore_clothing = any(entry.startswith("Woman A") for entry in character_hardcore_clothing_entries)
fallback_hard_clothing_state = "" if has_primary_hardcore_clothing else _insta_of_hardcore_clothing_state(
options["hardcore_clothing_continuity"],
soft_row["item"],
woman_access=woman_access,
)
hard_clothing_parts = [
part.strip().rstrip(".")
for part in (
fallback_hard_clothing_state,
*character_hardcore_clothing_entries,
*default_man_hardcore_clothing_entries,
)
if str(part or "").strip()
]
hard_clothing_state = "; ".join(hard_clothing_parts)
hard_clothing_sentence = f"{hard_clothing_state}. " if hard_clothing_state else ""
if "body is fully exposed" in hard_clothing_state.lower() or "bare skin unobstructed" in hard_clothing_state.lower():
hard_scene = _body_exposure_scene_text(hard_scene)
hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "")
hard_row["scene_text"] = hard_scene
hard_detail_density = options["hardcore_detail_density"]
hard_detail_directive = {
"compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ",
"balanced": "",
"dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ",
}[hard_detail_density]
pov_directive = _pov_prompt_directive(pov_character_labels)
soft_descriptor_sentence = (
f"Cast descriptors: {soft_cast_descriptor_text}. "
if options["softcore_cast"] == "same_as_hardcore"
else f"Woman A: {descriptor}. "
)
soft_prompt = (
f"Insta/OF softcore mode: {platform_style}. "
f"{soft_descriptor_sentence}"
f"Softcore setup: {soft_level}. Cast: {soft_cast}. "
f"{soft_cast_presence}"
f"{soft_cast_styling_sentence}"
f"{soft_row['softcore_item_prompt_label']}: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. "
f"{soft_camera_scene_sentence}"
f"{_labeled_expression_sentence('Facial expression', soft_row.get('expression'))}"
f"Composition: {soft_row['composition']}. "
f"{soft_camera_sentence}"
"Keep the softcore version seductive, creator-shot, and styled as a soft teaser. "
f"{soft_row['positive_suffix']}."
)
hard_prompt = (
f"Insta/OF hardcore mode: {platform_style}. "
f"Hardcore setup: {hard_level}. Cast: {hard_cast}. "
f"Cast descriptors: {cast_descriptor_text}. "
f"{pov_directive + ' ' if pov_directive else ''}"
f"{'Keep Woman A visually central from the POV camera. ' if pov_character_labels else 'Keep Woman A visually central. '}"
f"{hard_clothing_sentence}"
f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. "
f"Setting: {hard_scene}. "
f"{hard_camera_scene_sentence}"
f"{_labeled_expression_sentence('Facial expressions', hard_row.get('expression'))}"
f"Composition: {hard_composition}. "
f"{hard_detail_directive}"
f"{hard_camera_sentence}"
f"{hard_row['positive_suffix']}."
)
if extra_positive.strip():
soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}"
hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}"
soft_prompt = _insta_of_active_trigger(soft_prompt, active_trigger, bool(prepend_trigger_to_prompt))
hard_prompt = _insta_of_active_trigger(hard_prompt, active_trigger, bool(prepend_trigger_to_prompt))
soft_prompt = sanitize_prompt_text(soft_prompt, triggers=(active_trigger,))
hard_prompt = sanitize_prompt_text(hard_prompt, triggers=(active_trigger,))
soft_negative = sanitize_negative_text(_combined_negative(INSTA_OF_SOFT_NEGATIVE, extra_negative))
hard_negative = sanitize_negative_text(_combined_negative(INSTA_OF_NEGATIVE, extra_negative))
soft_caption_parts = [
active_trigger,
"Insta/OF softcore mode",
descriptor,
soft_level,
soft_row["item"],
soft_row["pose"],
soft_partner_outfit_text,
soft_partner_styling["pose"],
soft_row["scene_text"],
soft_camera_scene_directive,
soft_row["composition"],
_camera_caption_text(soft_camera_config) if soft_camera_directive else "",
]
soft_caption = sanitize_caption_text(
", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip()),
triggers=(active_trigger,),
)
hard_caption_parts = [
active_trigger,
"Insta/OF hardcore mode",
"Woman A",
descriptor,
hard_cast,
hard_row["role_graph"],
hard_row["item"],
hard_scene,
hard_camera_scene_directive,
hard_composition,
_camera_caption_text(hard_camera_config) if hard_camera_directive else "",
]
hard_caption = sanitize_caption_text(
", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip()),
triggers=(active_trigger,),
)
metadata = {
"mode": "Insta/OF",
"options": options,
"shared_descriptor": descriptor,
"shared_cast_descriptors": cast_descriptors,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": pov_directive,
"softcore_partner_styling": soft_partner_styling,
"character_hardcore_clothing": character_hardcore_clothing_entries,
"default_man_hardcore_clothing": default_man_hardcore_clothing_entries,
"hardcore_clothing_state": hard_clothing_state,
"hardcore_detail_density": hard_detail_density,
"hardcore_position_config": hard_row.get("hardcore_position_config", {}),
"softcore_prompt": soft_prompt,
"hardcore_prompt": hard_prompt,
"softcore_negative_prompt": soft_negative,
"hardcore_negative_prompt": hard_negative,
"softcore_caption": soft_caption,
"hardcore_caption": hard_caption,
"softcore_row": soft_row,
"hardcore_row": hard_row,
"hardcore_women_count": hard_women_count,
"hardcore_men_count": hard_men_count,
"character_cast_slots": character_slots,
"character_slot_labels": sorted(character_slot_map),
"softcore_camera_config": soft_camera_config,
"hardcore_camera_config": hard_camera_config,
"softcore_camera_directive": soft_camera_directive,
"hardcore_camera_directive": hard_camera_directive,
"softcore_camera_scene_directive": soft_camera_scene_directive,
"hardcore_camera_scene_directive": hard_camera_scene_directive,
}
return metadata