from __future__ import annotations import json import math import random import re from pathlib import Path from string import Formatter from typing import Any try: from . import generate_prompt_batches as g except ImportError: # Allows local smoke tests with `python -c`. import generate_prompt_batches as g ROOT_DIR = Path(__file__).resolve().parent CATEGORY_DIR = ROOT_DIR / "categories" PROFILE_DIR = ROOT_DIR / "profiles" BUILTIN_CATEGORIES = [ "auto_weighted", "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", ] 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"} CAMERA_DETAIL_CHOICES = ["off", "compact", "full"] HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"] 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 _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 == 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 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 _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} axis_values = { name: _entry_text(_weighted_choice(rng, _compatible_entries(axes[name], women_count, men_count))) for name in fields if name in axes and axes[name] } 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") 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") 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), "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: 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"): config["clothing"] = clothing_override if poses_override in ("standard", "evocative"): config["poses"] = poses_override config["expression_enabled"] = not _is_false(expression_enabled) if 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") else "full" parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative") else "standard" parsed["expression_enabled"] = not _is_false(parsed.get("expression_enabled", True)) parsed["expression_intensity"] = _clamped_float(parsed.get("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 ethnicity not in ETHNICITY_FILTER_CHOICES: ethnicity = "any" return json.dumps( { "ethnicity": ethnicity, "ethnicity_includes": selected_ethnicities, "figure": figure if figure in ("curvy", "balanced", "bombshell") 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 _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: try: raw = json.loads(str(filter_config)) 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} ethnicity = str(parsed.get("ethnicity") or "any") parsed["ethnicity"] = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in ethnicity else "any" parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell") 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 _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_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 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 _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any] | None) -> dict[str, Any]: directive, parsed = _camera_directive(camera_config) row["camera_config"] = parsed row["camera_directive"] = directive if not directive: return row row["prompt"] = _insert_positive_directive(row["prompt"], directive) camera_caption = _camera_caption_text(parsed) if camera_caption: 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 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 standard_ratio is None: return poses return "standard" if rng.random() < standard_ratio else "evocative" 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 _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 def _slot_value(value: Any) -> str: text = str(value or "").strip() if text.lower() in CHARACTER_RANDOM_TOKENS: return "" return text 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 _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 _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: text = str(value or "").strip() if text.lower() in CHARACTER_RANDOM_TOKENS: return "random" if text == "any" or text in ETHNICITY_FILTER_CHOICES or "+" in text: return text return "random" 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" age = _slot_manual_or_choice(str(slot.get("age") or "random"), str(slot.get("manual_age") or "")) body = _slot_manual_or_choice(str(slot.get("body") or "random"), str(slot.get("manual_body") or "")) figure = str(slot.get("figure") or "random").strip() if figure not in character_figure_choices(): figure = "random" normalized = { "profile_type": "character_slot", "subject_type": subject_type, "label": label, "age": age, "ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")), "figure": figure, "body": body, "body_phrase": _slot_value(slot.get("body_phrase")), "skin": _slot_value(slot.get("skin")), "hair": _slot_value(slot.get("hair")), "eyes": _slot_value(slot.get("eyes")), "descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")), "presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type), "softcore_outfit": _slot_value(slot.get("softcore_outfit")), "hardcore_clothing": _slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit")), "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"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')}", ] 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']}") 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", age: str = "random", manual_age: str = "", ethnicity: str = "random", figure: str = "random", body: str = "random", manual_body: str = "", body_phrase: str = "", skin: str = "", hair: str = "", 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, "age": age, "manual_age": manual_age, "ethnicity": ethnicity, "figure": figure, "body": body, "manual_body": manual_body, "body_phrase": body_phrase, "skin": skin, "hair": hair, "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) -> str: return _slot_value(slot.get("softcore_outfit")) if slot else "" def _slot_hardcore_clothing(slot: dict[str, Any] | None) -> str: return _slot_value(slot.get("hardcore_clothing")) if slot else "" 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, ) -> 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)) 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_figure = _slot_value(slot.get("figure")) slot_body = _slot_value(slot.get("body")) effective_ethnicity = slot_ethnicity or ethnicity effective_figure = slot_figure if slot_figure in ("curvy", "balanced", "bombshell") else figure effective_no_plus = bool(no_plus_women) and not slot_body effective_no_black = bool(no_black) and not slot_ethnicity context = _appearance_for_subject( rng, subject_type, effective_ethnicity, effective_figure, effective_no_plus, effective_no_black, ) age = _slot_value(slot.get("age")) body_phrase = _slot_value(slot.get("body_phrase")) 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 for key in ("skin", "hair", "eyes"): value = _slot_value(slot.get(key)) if value: context[key] = value 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 {} return slots[-1] 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 _empty_profile_result(status: str = "empty") -> dict[str, str]: return { "profile_json": "", "profile_name": "", "descriptor": "", "saved_path": "", "status": status, } 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 = "", ) -> 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", "")) 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, ) -> 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() 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 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 while {man} is above her and {man}'s penis thrusts into her." 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." 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." 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." 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." 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." if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text: return f"{woman} lies at the bed edge with hips near the edge while {man} kneels between her legs and {man}'s penis thrusts into her." 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." 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." return f"{woman} lies on her back with thighs open while {man} kneels between her legs and {man}'s penis thrusts into her." if people_count == 1: solo = people[0] if women_count == 1: 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 "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 "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 "oral" in slug: if "sixty-nine" in item_text or ("blowjob" in item_text and ("cunnilingus" in item_text or "pussy" in item_text)): graph = f"{woman} has {man}'s penis in her mouth while {man} uses his mouth on {woman}'s pussy, with both mouths pressed to genitals." elif any( term in item_text for term in ( "cunnilingus", "pussy licking", "tongue on pussy", "mouth on pussy", "pussy and tongue", "tongue contact", ) ) or ("pussy" in item_text and "penis" not in item_text): graph = f"{man} gives oral to {woman}, mouth on her pussy while {woman}'s thighs are held open for the camera." else: graph = f"{woman} takes {man}'s penis in her mouth while {man} holds her hair and hips." 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: graph = f"{man} thrusts his penis into {woman} while a toy adds a second penetration point." elif people_count >= 3: graph = f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front." else: graph = f"{man} thrusts his penis into {woman}'s ass while keeping her hips held open." 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) -> list[Any]: 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]) return scene_entries or fallback 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) -> list[Any]: configured = _configured_pool( category, subcategory, item, "compositions", "composition_pools", load_composition_pool_library(), "inherit_compositions", ) 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, character_profile: str | dict[str, Any] | None = None, character_cast: str | dict[str, Any] | list[Any] | None = None, expression_phase: str = "", ) -> 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) requested_women_count = women_count requested_men_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, } 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) 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) role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels) cast_descriptors: list[str] = [] cast_descriptor_text = "" expression_intensity_source = "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), 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) )) 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) if character_expression_text: expression = character_expression_text source_composition = _choose_text( composition_rng, _compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count), ) 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), "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, "pose": pose, "seed_config": seed_config, "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 = "", ) -> dict[str, Any]: apply_pool_extensions() row_number = max(1, int(row_number)) start_index = max(1, int(start_index)) seed = int(seed) clothing = clothing if clothing in ("full", "minimal") else "full" ethnicity = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in str(ethnicity) else "any" poses = poses if poses in ("standard", "evocative") else "standard" figure = figure if figure in ("curvy", "balanced", "bombshell") else "curvy" expression_enabled = not _is_false(expression_enabled) minimal_ratio = _ratio_or_none(minimal_clothing_ratio) pose_ratio = _ratio_or_none(standard_pose_ratio) expression_intensity = _clamped_float(expression_intensity, 0.5) parsed_seed_config = _parse_seed_config(seed_config) exact_custom_subcategory = bool(subcategory and subcategory != RANDOM_SUBCATEGORY and " / " in subcategory) 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, character_profile, character_cast, expression_phase, ) 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["negative_prompt"] = _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", "input") 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 = "", 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 "", ) INSTA_OF_SOFT_LEVELS = { "social_tease": "Instagram-style thirst-trap post, suggestive but non-explicit, 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 sexual contact still clearly visible", "partially_removed": "Woman A's teaser outfit is pushed aside and partly removed, exposing the sexual contact clearly", "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 non-explicit 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 sheets", "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 teaser without sex act", "lace teddy with strategic opaque embroidery, garter straps, and sheer stockings", "bare-shoulder robe opened around covered lingerie, explicit adult tease without partnered contact", "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 on soft sheets with direct eye contact", "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", "bedroom 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 on the edge of the bed 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 on the bed in a covered lingerie teaser pose with hands kept on fabric", "leaning against the vanity with the robe draped around covered lingerie", "standing in a three-quarter lingerie outfit-check pose with legs softly crossed", "sitting on the bed with stockings and garter details visible, non-explicit and posed", "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 under soft sheets with shoulders visible and the body strategically covered", "standing by the bathroom mirror with a towel wrapped around the body", "reclining under a satin sheet 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 no partnered contact", "kneeling on the bed with a sheer robe arranged around covered lingerie", "standing close to the mirror with the outfit framed boldly but non-explicitly", "leaning forward slightly with hands on the robe and intimate areas obscured", "sitting at the vanity 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 on the bed with body fully exposed and jewelry accents as styling", "standing at the vanity with body fully exposed in a premium creator-shot pose", "reclining on soft sheets 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 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 bodies close but no sexual contact", "posing shoulder-to-shoulder in a creator-shot group teaser", "leaning together on the bed in a non-explicit subscriber preview", "sitting close together with hands kept above clothing", "arranged around Woman A in a flirtatious non-explicit 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 with no sex act", ] 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) def _insta_of_hardcore_clothing_state(mode: str, softcore_outfit: 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}." return f"Clothing state: {base}; teaser outfit detail: {outfit}." 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)) 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)) 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 = "", 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) 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="", ) 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_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", ) 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_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 "" hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"] hard_composition = hard_row["composition"] soft_cast = ( "solo creator setup with Woman A alone" if options["softcore_cast"] == "solo" else f"non-explicit 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 non-explicit 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 non-explicit teaser pose with no sex act or genital contact. " 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, ) 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"], ) hard_clothing_parts = [part for part in (fallback_hard_clothing_state, *character_hardcore_clothing_entries) if part] hard_clothing_state = " ".join(hard_clothing_parts) 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"] = _body_exposure_scene_text(hard_row.get("scene_text", "")) 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"{_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 non-explicit. " 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_state} " f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. " f"Setting: {hard_scene}. " 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_negative = _combined_negative(INSTA_OF_SOFT_NEGATIVE, extra_negative) hard_negative = _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_row["composition"], _camera_caption_text(soft_camera_config) if soft_camera_directive else "", ] soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip()) hard_caption_parts = [ active_trigger, "Insta/OF hardcore mode", "Woman A", descriptor, hard_cast, hard_row["role_graph"], hard_row["item"], hard_scene, hard_composition, _camera_caption_text(hard_camera_config) if hard_camera_directive else "", ] hard_caption = ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip()) 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, "hardcore_clothing_state": hard_clothing_state, "hardcore_detail_density": hard_detail_density, "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, } return metadata