Files
ComfyUI-Ethanfel-Prompt-Bui…/prompt_builder.py
T

5913 lines
215 KiB
Python

from __future__ import annotations
import json
import random
import re
from pathlib import Path
from string import Formatter
from typing import Any, Callable
try:
from .category_library import (
category_json_files as _json_files,
compatible_entries as _compatible_entries,
compatible_entry as _compatible_entry,
configured_pool as _configured_pool,
find_subcategory as _find_subcategory,
load_category_library,
load_composition_pool_library,
load_expression_pool_library,
load_scene_pool_library,
merged_axes as _merged_axes,
merged_field as _merged_field,
read_category_json as _read_json,
template_list as _template_list,
)
from . import camera_config as camera_policy
from . import category_cast_config as category_cast_policy
from . import generate_prompt_batches as g
from . import generation_profile_config as generation_profile_policy
from . import location_config as location_policy
from . import pair_clothing
from . import pair_camera
from . import pair_cast
from . import pair_output
from . import pair_rows
from . import pair_options
from . import scene_camera_adapters
from . import seed_config as seed_policy
from .hardcore_text_cleanup import (
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
)
from .hardcore_action_metadata import source_hardcore_action_family
from .hardcore_role_graphs import build_hardcore_role_graph
from .prompt_hygiene import (
sanitize_caption_text,
sanitize_negative_text,
sanitize_prompt_text,
)
except ImportError: # Allows local smoke tests with `python -c`.
from category_library import (
category_json_files as _json_files,
compatible_entries as _compatible_entries,
compatible_entry as _compatible_entry,
configured_pool as _configured_pool,
find_subcategory as _find_subcategory,
load_category_library,
load_composition_pool_library,
load_expression_pool_library,
load_scene_pool_library,
merged_axes as _merged_axes,
merged_field as _merged_field,
read_category_json as _read_json,
template_list as _template_list,
)
import camera_config as camera_policy
import category_cast_config as category_cast_policy
import generate_prompt_batches as g
import generation_profile_config as generation_profile_policy
import location_config as location_policy
import pair_clothing
import pair_camera
import pair_cast
import pair_output
import pair_rows
import pair_options
import scene_camera_adapters
import seed_config as seed_policy
from hardcore_text_cleanup import (
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
)
from hardcore_action_metadata import source_hardcore_action_family
from hardcore_role_graphs import build_hardcore_role_graph
from prompt_hygiene import (
sanitize_caption_text,
sanitize_negative_text,
sanitize_prompt_text,
)
ROOT_DIR = Path(__file__).resolve().parent
PROFILE_DIR = ROOT_DIR / "profiles"
BUILTIN_CATEGORIES = [
"auto_weighted",
"auto_full",
"woman",
"man",
"couple",
"group_or_layout",
"custom_random",
]
RANDOM_SUBCATEGORY = "random"
SEED_AXIS_SALTS = seed_policy.SEED_AXIS_SALTS
SEED_AXIS_ALIASES = seed_policy.SEED_AXIS_ALIASES
SEED_LOCK_AXES = seed_policy.SEED_LOCK_AXES
SEED_MODE_CHOICES = seed_policy.SEED_MODE_CHOICES
ETHNICITY_FILTER_CHOICES = [
"any",
"european",
"mediterranean_mena",
"latina",
"east_asian",
"southeast_asian",
"south_asian",
"black_african",
"indigenous",
"mixed",
"asian",
"white_asian",
"western_european",
"french_european",
"germanic_european",
"nordic_european",
"celtic_european",
"slavic_european",
"baltic_european",
"alpine_european",
"balkan_european",
"greek_mediterranean",
"italian_mediterranean",
"iberian_mediterranean",
]
ETHNICITY_LIST_KEYS = tuple(choice for choice in ETHNICITY_FILTER_CHOICES if choice != "any")
ETHNICITY_BASE_LIST_KEYS = (
"european",
"mediterranean_mena",
"latina",
"east_asian",
"southeast_asian",
"south_asian",
"black_african",
"indigenous",
"mixed",
)
EUROPEAN_REGIONAL_LIST_KEYS = (
"western_european",
"french_european",
"germanic_european",
"nordic_european",
"celtic_european",
"slavic_european",
"baltic_european",
"alpine_european",
"balkan_european",
)
MEDITERRANEAN_REGIONAL_LIST_KEYS = (
"greek_mediterranean",
"italian_mediterranean",
"iberian_mediterranean",
)
CHARACTER_LABEL_CHOICES = [
"auto_chain",
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
]
CHARACTER_AGE_CHOICES = (
["random", "manual"]
+ [f"{age}-year-old adult" for age in range(21, 86)]
+ [
"late 20s adult",
"early 30s adult",
"mid 30s adult",
"late 30s adult",
"early 40s adult",
"mid 40s adult",
"late 40s adult",
"early 50s adult",
"mid 50s adult",
"late 50s adult",
"early 60s adult",
"mid 60s adult",
"late 60s adult",
"early 70s adult",
"mid 70s adult",
"late 70s adult",
"early 80s adult",
]
)
CHARACTER_BODY_CHOICES = [
"random",
"manual",
"slim",
"petite adult",
"toned",
"athletic",
"average",
"curvy",
"soft curvy",
"curvy athletic",
"hourglass",
"slim busty",
"busty",
"busty curvy",
"voluptuous",
"plus-size",
"heavyset",
"fat",
"stocky",
"broad",
"muscular",
]
CHARACTER_WOMAN_BODY_CHOICES = [
"random",
"manual",
"slim",
"petite adult",
"toned",
"athletic",
"average",
"curvy",
"soft curvy",
"curvy athletic",
"hourglass",
"slim busty",
"busty",
"busty curvy",
"voluptuous",
"plus-size",
"heavyset",
"fat",
]
CHARACTER_MAN_BODY_CHOICES = [
"random",
"manual",
"slim",
"lean",
"lean athletic",
"toned",
"average",
"athletic",
"muscular",
"broad",
"broad-shouldered",
"stocky",
"heavyset",
"fat",
]
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
CHARACTER_SLOT_SEED_MAX = 0xFFFFFFFF
CHARACTER_HAIR_COLOR_CHOICES = [
"random",
"black",
"brown",
"dark_brown",
"chestnut",
"auburn",
"copper",
"red",
"blonde",
"platinum_blonde",
"ash_blonde",
"honey_blonde",
"strawberry_blonde",
"dark_blonde",
"silver_gray",
"white",
]
CHARACTER_HAIR_LENGTH_CHOICES = [
"random",
"very_short",
"short",
"bob_lob",
"shoulder_length",
"medium",
"long",
"very_long",
"updo",
]
CHARACTER_HAIR_STYLE_CHOICES = [
"random",
"straight",
"waves",
"loose_waves",
"curls",
"tight_curls",
"pixie_cut",
"bob",
"lob",
"shag",
"ponytail",
"braid",
"braids",
"bun",
"messy_bun",
"locs",
"twists",
"afro",
"natural_curls",
"wet_hair",
"slicked_back",
]
CHARACTER_EYE_COLOR_CHOICES = [
"random",
"blue",
"pale_blue",
"ice_blue",
"blue_gray",
"green",
"emerald_green",
"hazel",
"light_hazel",
"green_hazel",
"amber",
"amber_brown",
"honey_brown",
"brown",
"deep_brown",
"dark_brown",
"dark",
"gray",
"gray_brown",
]
CAMERA_DETAIL_CHOICES = camera_policy.CAMERA_DETAIL_CHOICES
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
HARDCORE_POSITION_FAMILY_CHOICES = [
"any",
"penetrative",
"foreplay",
"interaction",
"manual",
"oral",
"outercourse",
"anal",
"climax",
"threesome",
"group",
]
HARDCORE_POSITION_FOCUS_CHOICES = [
"keep_pool",
"penetration_only",
"foreplay_only",
"interaction_only",
"manual_only",
"oral_only",
"outercourse_only",
"anal_only",
"climax_only",
"threesome_only",
"group_only",
]
HARDCORE_POSITION_KEY_CHOICES = [
"missionary",
"cowgirl",
"reverse_cowgirl",
"doggy",
"bent_over",
"face_down_ass_up",
"standing",
"side_lying",
"edge_supported",
"kneeling",
"lotus_lap",
"face_sitting",
"sixty_nine",
"reclining_oral",
"straddled_oral",
"spread_leg_oral",
"chair_oral",
"kissing",
"caressing",
"breast_touch",
"face_touch",
"undressing",
"body_worship",
"nipple_play",
"ass_grab",
"thigh_kissing",
"hair_holding",
"wrist_pinning",
"dirty_talk",
"position_transition",
"guided_positioning",
"camera_showing",
"watching",
"aftercare",
"cleanup",
"fingering",
"clit_rubbing",
"mutual_masturbation",
"boobjob",
"testicle_sucking",
"penis_licking",
"handjob",
"footjob",
"open_thighs",
"front_back",
]
HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
"any": [
"penetrative_sex",
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"manual_stimulation",
"oral_sex",
"outercourse_sex",
"anal_double_penetration",
"threesomes",
"group_coordination",
"group_sex_orgy",
"cumshot_climax",
"aftercare_cleanup",
],
"penetrative": ["penetrative_sex"],
"foreplay": ["foreplay_teasing"],
"interaction": [
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"group_coordination",
"aftercare_cleanup",
],
"manual": ["manual_stimulation"],
"oral": ["oral_sex"],
"outercourse": ["outercourse_sex", "manual_stimulation"],
"anal": ["anal_double_penetration"],
"climax": ["cumshot_climax"],
"threesome": ["threesomes"],
"group": ["group_sex_orgy"],
}
HARDCORE_POSITION_KEY_MATCHES = {
"missionary": ("missionary", "above her", "under her"),
"cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
"reverse_cowgirl": ("reverse cowgirl", "facing away"),
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
"bent_over": ("bent-over", "bent over", "hips raised"),
"face_down_ass_up": ("face-down", "ass-up"),
"standing": ("standing", "stands", "braced standing"),
"side_lying": ("side-lying", "side lying", "spooning", "on the side", "on her side"),
"edge_supported": ("edge-of-bed", "edge of bed", "bed edge", "raised edge", "edge-supported"),
"kneeling": ("kneeling", "kneels", "kneeling center"),
"lotus_lap": ("lotus", "lap", "seated in a partner's lap"),
"face_sitting": ("face-sitting", "face sitting"),
"sixty_nine": ("sixty-nine", "69"),
"reclining_oral": ("reclining cunnilingus",),
"straddled_oral": ("straddled oral",),
"spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"),
"chair_oral": ("chair oral",),
"kissing": ("kiss", "kissing", "mouth-to-mouth", "mouth to mouth", "lips pressed"),
"caressing": ("caress", "caressing", "hands roaming", "stroking skin", "hands sliding"),
"breast_touch": ("breast", "breasts", "nipple", "cupping breasts", "touching breasts"),
"face_touch": ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin"),
"undressing": ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning"),
"body_worship": ("body worship", "worship", "kissing down", "mouth on skin", "kissing the body"),
"nipple_play": ("nipple", "nipples", "licking nipples", "sucking nipples", "nipple play"),
"ass_grab": ("ass grab", "ass-grab", "ass grabbing", "hand on the ass", "squeezing the ass"),
"thigh_kissing": ("thigh kiss", "thigh kissing", "kissing thighs", "mouth on inner thighs"),
"hair_holding": ("hair holding", "hair held", "holding hair", "hair pulled back"),
"wrist_pinning": ("wrist", "wrists", "pinning wrists", "wrists pinned", "hands pinned"),
"dirty_talk": ("dirty talk", "whispering", "mouth near the ear", "telling", "verbal teasing"),
"position_transition": ("transition", "turning around", "pulling onto the bed", "moving into position", "position change"),
"guided_positioning": ("guiding", "guided", "guides", "lifting legs", "spreading thighs", "pulling hips", "turning the body"),
"camera_showing": ("camera", "showing to camera", "presenting to camera", "spread open for camera", "creator-shot"),
"watching": ("watching", "voyeur", "waiting turn", "partner watches", "onlooker"),
"aftercare": ("aftercare", "cuddling", "kissing after", "holding close", "post-sex"),
"cleanup": ("cleanup", "wiping", "cleaning", "towel", "wet cloth"),
"fingering": ("fingering", "fingers inside", "fingers in pussy", "finger stimulation"),
"clit_rubbing": ("clit", "clitoris", "clit rubbing", "rubbing the clit", "fingers on clit"),
"mutual_masturbation": ("mutual masturbation", "both touching themselves", "masturbating together", "hands on their own bodies"),
"boobjob": ("boobjob", "titjob", "breast-sex", "breast sex"),
"testicle_sucking": ("testicle", "balls-licking", "balls licking", "balls and mouth"),
"penis_licking": ("penis-licking", "penis licking", "tongue along", "tongue licking"),
"handjob": ("handjob", "hand job", "stroking the penis", "hand stroking", "manual stimulation"),
"footjob": ("footjob", "soles", "toes curled", "feet stroking"),
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
}
HARDCORE_POSITION_AXIS_KEYS = {
"position",
"body_position",
"body_arrangement",
"arrangement",
"tease_act",
"touch_detail",
"manual_act",
"manual_detail",
"worship_act",
"transition_act",
"control_act",
"performance_act",
"coordination_act",
"aftercare_act",
"cleanup_detail",
}
HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY = {
"penetrative_sex": "penetrative",
"foreplay_teasing": "foreplay",
"body_worship_touching": "interaction",
"clothing_position_transitions": "interaction",
"dominant_guidance": "interaction",
"camera_performance": "interaction",
"manual_stimulation": "manual",
"oral_sex": "oral",
"outercourse_sex": "outercourse",
"anal_double_penetration": "anal",
"threesomes": "threesome",
"group_coordination": "interaction",
"group_sex_orgy": "group",
"cumshot_climax": "climax",
"aftercare_cleanup": "interaction",
}
def _hardcore_source_position_family(subcategory: dict[str, Any], config: dict[str, Any] | None = None) -> str:
slug = str(subcategory.get("slug") or subcategory.get("name") or "").strip().lower()
family = HARDCORE_SOURCE_FAMILY_BY_SUBCATEGORY.get(slug, "")
if family:
return family
config_family = _normalize_hardcore_position_family((config or {}).get("family"), "")
return "" if config_family == "any" else config_family
def _hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = None) -> list[str]:
text_parts = [str(part or "") for part in parts if str(part or "").strip()]
if isinstance(axis_values, dict):
text_parts.extend(str(value or "") for value in axis_values.values() if str(value or "").strip())
text = " ".join(text_parts).lower()
if not text:
return []
keys: list[str] = []
for key, tokens in HARDCORE_POSITION_KEY_MATCHES.items():
if any(token in text for token in tokens):
keys.append(key)
return keys
CAMERA_ORBIT_FRAMING_CHOICES = camera_policy.CAMERA_ORBIT_FRAMING_CHOICES
CAMERA_ORBIT_FOCUS_CHOICES = camera_policy.CAMERA_ORBIT_FOCUS_CHOICES
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 = camera_policy.CAMERA_MODE_PROMPTS
CAMERA_COMPACT_LABELS = camera_policy.CAMERA_COMPACT_LABELS
CAMERA_SHOT_PROMPTS = camera_policy.CAMERA_SHOT_PROMPTS
CAMERA_ANGLE_PROMPTS = camera_policy.CAMERA_ANGLE_PROMPTS
CAMERA_LENS_PROMPTS = camera_policy.CAMERA_LENS_PROMPTS
CAMERA_DISTANCE_PROMPTS = camera_policy.CAMERA_DISTANCE_PROMPTS
CAMERA_ORIENTATION_PROMPTS = camera_policy.CAMERA_ORIENTATION_PROMPTS
CAMERA_PHONE_PROMPTS = camera_policy.CAMERA_PHONE_PROMPTS
CAMERA_PRIORITY_PROMPTS = camera_policy.CAMERA_PRIORITY_PROMPTS
_EXTENSIONS_APPLIED = False
class SafeFormatDict(dict):
def __missing__(self, key: str) -> str:
return "{" + key + "}"
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 _oral_acts_for_position(values: list[Any], position: str) -> list[Any]:
position_text = str(position or "").lower()
if not position_text:
return values
def act_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(predicate: Callable[[str], bool]) -> list[Any]:
matches = [value for value in values if predicate(act_text(value))]
return matches or values
penis_terms = ("fellatio", "blowjob", "deepthroat", "penis sucking", "penis in mouth")
cunnilingus_terms = ("cunnilingus", "pussy licking", "tongue on pussy", "oral sex with tongue and fingers", "mouth on genitals")
if "sixty-nine" in position_text:
return filtered(lambda text: "sixty-nine" in text)
if "face-sitting" in position_text:
return filtered(lambda text: "face-sitting" in text or any(term in text for term in cunnilingus_terms))
if "kneeling oral" in position_text:
return filtered(lambda text: any(term in text for term in penis_terms))
if "straddled oral" in position_text or "reclining cunnilingus" in position_text:
return filtered(lambda text: "sixty-nine" not in text and not any(term in text for term in penis_terms))
if "spread-leg oral" in position_text:
return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text)
if any(term in position_text for term in ("standing oral", "kneeling oral", "edge-of-bed oral", "chair oral", "side-lying oral")):
return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text)
return values
def _oral_axis_values_for_context(values: list[Any], position: str, oral_act: str, axis_name: str) -> list[Any]:
axis_name = str(axis_name or "").lower()
if axis_name not in {"body_contact", "hand_detail", "mouth_detail", "saliva_detail", "climax_hint", "visibility"}:
return values
position_text = str(position or "").lower()
act_text = str(oral_act or "").lower()
woman_gives = any(
term in act_text
for term in ("fellatio", "blowjob", "deepthroat", "penis sucking", "penis in mouth")
)
man_gives = any(
term in act_text
for term in ("cunnilingus", "pussy licking", "tongue on pussy")
)
if not (woman_gives or man_gives):
return values
def value_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(terms: tuple[str, ...], excluded_terms: tuple[str, ...] = ()) -> list[Any]:
matches = [
value
for value in values
if any(term in value_text(value) for term in terms)
and not any(term in value_text(value) for term in excluded_terms)
]
return matches or values
if woman_gives:
by_axis = {
"body_contact": ("hips pushed", "fingers tangled", "bodies stacked", "hands on thighs"),
"hand_detail": ("hips", "penis", "head", "hair"),
"mouth_detail": ("lips", "mouth", "deep mouth", "saliva"),
"saliva_detail": ("saliva", "wet lips", "slick wet mouth", "drool", "mouth"),
"climax_hint": ("mouth", "lips", "tongue", "breasts", "belly", "sexual fluids"),
"visibility": ("mouth", "penis", "oral"),
}
excluded = {
"body_contact": ("legs held open", "spread legs", "ass lifted", "chest pressed to thighs"),
"hand_detail": ("spreading thighs", "sheets", "cupping breasts", "pressing into thighs", "holding the ass"),
}
return filtered(by_axis.get(axis_name, ("mouth", "penis")), excluded.get(axis_name, ()))
if man_gives and ("kneeling oral" in position_text or "standing oral" in position_text):
by_axis = {
"body_contact": ("legs held open", "one body kneeling", "chest pressed", "ass lifted", "hands on thighs"),
"hand_detail": ("thigh", "hips", "head", "ass"),
"mouth_detail": ("tongue", "wet lips", "deep mouth", "genitals"),
"saliva_detail": ("saliva", "wet lips", "tongue", "drool"),
"climax_hint": ("sexual fluids", "orgasmic tension"),
"visibility": ("mouth", "pussy", "oral", "genital"),
}
return filtered(by_axis.get(axis_name, ("mouth", "pussy", "tongue")), ("penis", "breasts"))
return values
def _outercourse_acts_for_position(values: list[Any], position: str) -> list[Any]:
position_text = str(position or "").lower()
if not position_text:
return values
def act_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(predicate: Callable[[str], bool]) -> list[Any]:
matches = [value for value in values if predicate(act_text(value))]
return matches or values
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
return filtered(lambda text: any(term in text for term in ("boobjob", "titjob", "breast sex", "breasts")))
if any(term in position_text for term in ("testicle", "balls")):
return filtered(lambda text: any(term in text for term in ("testicle", "balls")))
if "penis-licking" in position_text or "penis licking" in position_text:
return filtered(lambda text: "licking" in text or "tongue" in text)
if "handjob" in position_text or "hand job" in position_text:
return filtered(lambda text: any(term in text for term in ("handjob", "hand job", "hand wrapped", "two-handed")))
if "footjob" in position_text:
return filtered(lambda text: any(term in text for term in ("footjob", "feet", "soles", "toes")))
return values
def _outercourse_axis_values_for_position(values: list[Any], position: str, axis_name: str) -> list[Any]:
position_text = str(position or "").lower()
if not position_text:
return values
axis_name = str(axis_name or "").lower()
if axis_name not in {"contact_detail", "hand_detail", "texture_detail", "visibility", "body_contact"}:
return values
def value_text(value: Any) -> str:
return _entry_text(value).lower()
def filtered(terms: tuple[str, ...], excluded_terms: tuple[str, ...] = ()) -> list[Any]:
matches = [
value
for value in values
if any(term in value_text(value) for term in terms)
and not any(term in value_text(value) for term in excluded_terms)
]
return matches or values
if any(term in position_text for term in ("boobjob", "titjob", "breast-sex", "breast sex")):
by_axis = {
"contact_detail": ("compressed", "glans", "breast", "breasts", "soft tissue", "skin visibly"),
"hand_detail": ("breast", "breasts", "fingers"),
"texture_detail": ("compression", "soft flesh", "skin", "flesh", "asymmetry"),
"visibility": ("breast", "breasts", "glans", "shaft"),
"body_contact": ("torso", "body angled", "shoulders", "hips"),
}
excluded_by_axis = {
"contact_detail": ("hand wrapped", "fingers and palm", "soles", "toes", "balls", "tongue"),
"hand_detail": ("base of the penis", "penis shaft", "balls", "thigh", "ankles", "stroking"),
"texture_detail": ("toes", "soles", "tongue"),
"visibility": ("balls", "soles", "toes", "hand"),
"body_contact": ("head tucked", "face directly", "base of the penis"),
}
return filtered(
by_axis.get(axis_name, ("breast", "breasts", "shaft")),
excluded_by_axis.get(axis_name, ()),
)
if any(term in position_text for term in ("testicle", "balls")):
by_axis = {
"contact_detail": ("balls", "lips", "tongue", "wet"),
"hand_detail": ("balls", "base", "thigh"),
"texture_detail": ("wet", "saliva", "skin"),
"visibility": ("balls", "mouth"),
"body_contact": ("torso", "shoulders", "head tucked", "base of the penis", "knees", "thigh"),
}
return filtered(by_axis.get(axis_name, ("balls", "mouth", "tongue")))
if "penis-licking" in position_text or "penis licking" in position_text:
by_axis = {
"contact_detail": ("tongue", "lips", "glans", "shaft", "wet"),
"hand_detail": ("base", "penis", "thigh"),
"texture_detail": ("wet", "saliva", "skin"),
"visibility": ("tongue", "penis"),
"body_contact": ("head low", "face directly", "torso", "pelvis", "base of the penis", "hips", "body angled"),
}
return filtered(by_axis.get(axis_name, ("tongue", "glans", "shaft")))
if "handjob" in position_text or "hand job" in position_text:
by_axis = {
"contact_detail": ("hand", "fingers", "palm", "shaft", "glans"),
"hand_detail": ("hand", "hands", "shaft", "penis"),
"texture_detail": ("fingers", "pressure", "skin", "shaft"),
"visibility": ("hand", "penis", "shaft", "glans"),
"body_contact": ("hips", "knees", "body angle"),
}
return filtered(by_axis.get(axis_name, ("hand", "penis", "shaft")))
if "footjob" in position_text:
by_axis = {
"contact_detail": ("soles", "toes"),
"hand_detail": ("ankles", "thighs"),
"texture_detail": ("toes", "soles", "pressure"),
"visibility": ("feet", "soles"),
"body_contact": ("legs", "knees", "body angled"),
}
excluded_by_axis = {
"contact_detail": ("hand", "finger", "palm", "balls", "tongue", "breast"),
"texture_detail": ("fingers", "tongue", "breast"),
"visibility": ("hand", "balls", "breast"),
}
return filtered(
by_axis.get(axis_name, ("feet", "soles", "toes")),
excluded_by_axis.get(axis_name, ()),
)
return values
def _compose_item(
rng: random.Random,
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
women_count: int = 1,
men_count: int = 1,
) -> tuple[str, str, dict[str, str]]:
templates = _template_list(category, subcategory, item, "item_templates")
axes = _merged_axes(category, subcategory, item)
if templates and axes:
template = _entry_text(_weighted_choice(rng, _compatible_entries(templates, women_count, men_count)))
fields = [key for _, key, _, _ in Formatter().parse(template) if key]
unique_fields = list(dict.fromkeys(fields))
axis_values: dict[str, str] = {}
subcategory_slug = str(subcategory.get("slug") or "").lower()
if subcategory_slug in ("oral_sex", "outercourse_sex") and "position" in unique_fields and axes.get("position"):
position_values = _compatible_entries(axes["position"], women_count, men_count)
axis_values["position"] = _entry_text(_weighted_choice(rng, position_values))
for name in unique_fields:
if name in axis_values or name not in axes or not axes[name]:
continue
values = _compatible_entries(axes[name], women_count, men_count)
if subcategory_slug == "oral_sex" and name == "oral_act":
values = _oral_acts_for_position(values, axis_values.get("position", ""))
elif subcategory_slug == "oral_sex":
values = _oral_axis_values_for_context(
values,
axis_values.get("position", ""),
axis_values.get("oral_act", ""),
name,
)
if subcategory_slug == "outercourse_sex" and name == "outer_act":
values = _outercourse_acts_for_position(values, axis_values.get("position", ""))
if subcategory_slug == "outercourse_sex":
values = _outercourse_axis_values_for_position(values, axis_values.get("position", ""), name)
axis_values[name] = _entry_text(_weighted_choice(rng, values))
item_text = _format(template, axis_values).strip()
item_name = _item_name(item) or subcategory["name"]
return item_text, item_name, axis_values
return _item_text(item), _item_name(item), {}
def _choose_text(rng: random.Random, items: list[Any]) -> str:
item = _weighted_choice(rng, items)
return _item_text(item)
def _choose_distinct_text(rng: random.Random, items: list[Any], first_text: str) -> str:
first_text = _item_text(first_text).lower()
distinct = [item for item in items if _item_text(item).lower() != first_text]
if not distinct:
return ""
return _choose_text(rng, distinct)
def _choose_pair(rng: random.Random, items: list[Any]) -> tuple[str, str]:
return _pair_from(_weighted_choice(rng, items))
def _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 seed_policy.seed_mode_choices()
CATEGORY_PRESETS = category_cast_policy.CATEGORY_PRESETS
CAST_PRESETS = category_cast_policy.CAST_PRESETS
GENERATION_PROFILE_PRESETS = generation_profile_policy.GENERATION_PROFILE_PRESETS
def category_preset_choices() -> list[str]:
return category_cast_policy.category_preset_choices()
def cast_preset_choices() -> list[str]:
return category_cast_policy.cast_preset_choices()
def generation_profile_choices() -> list[str]:
return generation_profile_policy.generation_profile_choices()
def build_category_config_json(preset: str = "auto_weighted", subcategory: str = RANDOM_SUBCATEGORY) -> str:
return category_cast_policy.build_category_config_json(preset=preset, subcategory=subcategory)
def _parse_category_config(category_config: str | dict[str, Any] | None) -> tuple[str, str]:
return category_cast_policy.parse_category_config(category_config)
def build_cast_config_json(cast_mode: str = "mixed_couple", women_count: int = 1, men_count: int = 1) -> str:
return category_cast_policy.build_cast_config_json(cast_mode=cast_mode, women_count=women_count, men_count=men_count)
def _parse_cast_config(cast_config: str | dict[str, Any] | None) -> dict[str, int | str]:
return category_cast_policy.parse_cast_config(cast_config)
def build_generation_profile_json(
profile: str = "balanced",
clothing_override: str = "profile_default",
poses_override: str = "profile_default",
expression_intensity_mode: str = "profile_default",
expression_intensity: float = -1.0,
backside_bias: float = -1.0,
minimal_clothing_ratio: float = -1.0,
standard_pose_ratio: float = -1.0,
trigger_policy: str = "profile_default",
expression_enabled: bool = True,
) -> str:
return generation_profile_policy.build_generation_profile_json(
profile=profile,
clothing_override=clothing_override,
poses_override=poses_override,
expression_intensity_mode=expression_intensity_mode,
expression_intensity=expression_intensity,
backside_bias=backside_bias,
minimal_clothing_ratio=minimal_clothing_ratio,
standard_pose_ratio=standard_pose_ratio,
trigger_policy=trigger_policy,
expression_enabled=expression_enabled,
)
def _parse_generation_profile(profile_config: str | dict[str, Any] | None) -> dict[str, Any]:
return generation_profile_policy.parse_generation_profile(profile_config)
def build_filter_config_json(
ethnicity: str = "any",
figure: str = "curvy",
no_plus_women: bool = False,
no_black: bool = False,
include_european: bool = True,
include_mediterranean_mena: bool = True,
include_latina: bool = True,
include_east_asian: bool = True,
include_southeast_asian: bool = True,
include_south_asian: bool = True,
include_black_african: bool = True,
include_indigenous: bool = True,
include_mixed: bool = True,
include_plus_size: bool = True,
) -> str:
include_flags = {
"european": include_european,
"mediterranean_mena": include_mediterranean_mena,
"latina": include_latina,
"east_asian": include_east_asian,
"southeast_asian": include_southeast_asian,
"south_asian": include_south_asian,
"black_african": include_black_african,
"indigenous": include_indigenous,
"mixed": include_mixed,
}
selected_ethnicities = [key for key, enabled in include_flags.items() if enabled]
disabled_ethnicities = [key for key, enabled in include_flags.items() if not enabled]
enabled_ethnicities = list(selected_ethnicities)
if enabled_ethnicities:
enabled_ethnicities.extend(f"exclude_{key}" for key in disabled_ethnicities)
if 0 < len(selected_ethnicities) < len(include_flags):
ethnicity = "+".join(enabled_ethnicities)
elif not _is_valid_ethnicity_filter(ethnicity):
ethnicity = "any"
return json.dumps(
{
"ethnicity": ethnicity,
"ethnicity_includes": selected_ethnicities,
"figure": figure if figure in ("curvy", "balanced", "bombshell", "random") else "curvy",
"include_plus_size": bool(include_plus_size),
"include_black_african": bool(include_black_african),
"no_plus_women": not bool(include_plus_size) or bool(no_plus_women),
"no_black": not bool(include_black_african) or bool(no_black),
},
ensure_ascii=True,
sort_keys=True,
)
LOCATION_POOL_PRESETS = location_policy.LOCATION_POOL_PRESETS
COMPOSITION_POOL_PRESETS = location_policy.COMPOSITION_POOL_PRESETS
COMPOSITION_INLINE_PRESETS = location_policy.COMPOSITION_INLINE_PRESETS
THEMATIC_LOCATION_PRESETS = location_policy.THEMATIC_LOCATION_PRESETS
def location_pool_preset_choices() -> list[str]:
return location_policy.location_pool_preset_choices()
def composition_pool_preset_choices() -> list[str]:
return location_policy.composition_pool_preset_choices()
def location_theme_choices() -> list[str]:
return location_policy.location_theme_choices()
def _location_pool_names_for_preset(preset: str) -> list[str]:
return location_policy.location_pool_names_for_preset(preset)
def _custom_location_entries(custom_locations: str) -> list[dict[str, str]]:
return location_policy.custom_location_entries(custom_locations)
def _scene_entries_for_pool_names(pool_names: list[str]) -> list[Any]:
return location_policy.scene_entries_for_pool_names(pool_names)
def build_location_pool_json(
enabled: bool = True,
combine_mode: str = "replace",
preset: str = "custom_only",
custom_locations: str = "",
location_config: str | dict[str, Any] | None = "",
) -> str:
return location_policy.build_location_pool_json(
enabled=enabled,
combine_mode=combine_mode,
preset=preset,
custom_locations=custom_locations,
location_config=location_config,
)
def _parse_location_config(location_config: str | dict[str, Any] | None) -> dict[str, Any]:
return location_policy.parse_location_config(location_config)
def _location_config_active(location_config: dict[str, Any]) -> bool:
return location_policy.location_config_active(location_config)
def _composition_pool_names_for_preset(preset: str) -> list[str]:
return location_policy.composition_pool_names_for_preset(preset)
def _custom_composition_entries(custom_compositions: str) -> list[str]:
return location_policy.custom_composition_entries(custom_compositions)
def _composition_entries_for_pool_names(pool_names: list[str]) -> list[Any]:
return location_policy.composition_entries_for_pool_names(pool_names)
def build_composition_pool_json(
enabled: bool = True,
combine_mode: str = "replace",
preset: str = "custom_only",
custom_compositions: str = "",
composition_config: str | dict[str, Any] | None = "",
) -> str:
return location_policy.build_composition_pool_json(
enabled=enabled,
combine_mode=combine_mode,
preset=preset,
custom_compositions=custom_compositions,
composition_config=composition_config,
)
def _parse_composition_config(composition_config: str | dict[str, Any] | None) -> dict[str, Any]:
return location_policy.parse_composition_config(composition_config)
def _composition_config_active(composition_config: dict[str, Any]) -> bool:
return location_policy.composition_config_active(composition_config)
def build_thematic_location_json(
enabled: bool = True,
combine_mode: str = "replace",
theme: str = "semi_public_affair",
custom_locations: str = "",
custom_compositions: str = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
) -> tuple[str, str, str]:
return location_policy.build_thematic_location_json(
enabled=enabled,
combine_mode=combine_mode,
theme=theme,
custom_locations=custom_locations,
custom_compositions=custom_compositions,
location_config=location_config,
composition_config=composition_config,
)
def _ethnicity_text_from_value(value: Any) -> str:
if isinstance(value, dict):
return str(value.get("ethnicity") or "").strip()
text = str(value or "").strip()
if not text:
return ""
if text.startswith("{"):
try:
raw = json.loads(text)
except json.JSONDecodeError:
return text
if isinstance(raw, dict):
return str(raw.get("ethnicity") or "").strip()
return text
def _is_valid_ethnicity_filter(value: Any) -> bool:
text = _ethnicity_text_from_value(value)
return text == "any" or text in ETHNICITY_FILTER_CHOICES or "+" in text
def normalize_ethnicity_filter(value: Any, default: str = "any", allow_random: bool = False) -> str:
text = _ethnicity_text_from_value(value)
if text.lower() in CHARACTER_RANDOM_TOKENS:
return "random" if allow_random else default
return text if _is_valid_ethnicity_filter(text) else default
def build_ethnicity_list_json(
include_european: bool = False,
include_mediterranean_mena: bool = False,
include_latina: bool = False,
include_east_asian: bool = False,
include_southeast_asian: bool = False,
include_south_asian: bool = False,
include_black_african: bool = False,
include_indigenous: bool = False,
include_mixed: bool = False,
include_asian: bool = False,
include_white_asian: bool = False,
include_western_european: bool = False,
include_french_european: bool = False,
include_germanic_european: bool = False,
include_nordic_european: bool = False,
include_celtic_european: bool = False,
include_slavic_european: bool = False,
include_baltic_european: bool = False,
include_alpine_european: bool = False,
include_balkan_european: bool = False,
include_greek_mediterranean: bool = False,
include_italian_mediterranean: bool = False,
include_iberian_mediterranean: bool = False,
strict_excludes: bool = True,
) -> dict[str, str]:
include_flags = {
"european": include_european,
"mediterranean_mena": include_mediterranean_mena,
"latina": include_latina,
"east_asian": include_east_asian,
"southeast_asian": include_southeast_asian,
"south_asian": include_south_asian,
"black_african": include_black_african,
"indigenous": include_indigenous,
"mixed": include_mixed,
"asian": include_asian,
"white_asian": include_white_asian,
"western_european": include_western_european,
"french_european": include_french_european,
"germanic_european": include_germanic_european,
"nordic_european": include_nordic_european,
"celtic_european": include_celtic_european,
"slavic_european": include_slavic_european,
"baltic_european": include_baltic_european,
"alpine_european": include_alpine_european,
"balkan_european": include_balkan_european,
"greek_mediterranean": include_greek_mediterranean,
"italian_mediterranean": include_italian_mediterranean,
"iberian_mediterranean": include_iberian_mediterranean,
}
selected = [key for key in ETHNICITY_LIST_KEYS if include_flags.get(key)]
if not selected or set(selected) == set(ETHNICITY_LIST_KEYS):
ethnicity = "any"
else:
tokens = list(selected)
if strict_excludes:
protected: set[str] = set()
if "asian" in selected:
protected.update(("east_asian", "southeast_asian", "south_asian"))
if "white_asian" in selected:
protected.update(("european", "east_asian", "southeast_asian", "south_asian", "mixed"))
if any(key in selected for key in EUROPEAN_REGIONAL_LIST_KEYS):
protected.add("european")
if any(key in selected for key in MEDITERRANEAN_REGIONAL_LIST_KEYS):
protected.add("mediterranean_mena")
if "mixed" in selected:
protected.update(ETHNICITY_BASE_LIST_KEYS)
tokens.extend(
f"exclude_{key}"
for key in ETHNICITY_BASE_LIST_KEYS
if key not in selected and key not in protected
)
ethnicity = "+".join(tokens)
filter_config = {
"ethnicity": ethnicity,
"ethnicity_includes": selected,
}
summary = "any ethnicity" if ethnicity == "any" else "ethnicity list: " + ", ".join(selected)
return {
"ethnicity": ethnicity,
"filter_config": json.dumps(filter_config, ensure_ascii=True, sort_keys=True),
"summary": summary,
}
def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
defaults = {
"ethnicity": "any",
"figure": "curvy",
"no_plus_women": False,
"no_black": False,
"include_plus_size": True,
"include_black_african": True,
}
if not filter_config:
return defaults
if isinstance(filter_config, dict):
raw = filter_config
else:
text = str(filter_config).strip()
if not text.startswith("{"):
raw = {"ethnicity": text}
else:
try:
raw = json.loads(text)
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid filter_config JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError("filter_config must be a JSON object")
parsed = {**defaults, **raw}
parsed["ethnicity"] = normalize_ethnicity_filter(parsed.get("ethnicity"), "any")
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell", "random") else "curvy"
parsed["include_plus_size"] = bool(parsed.get("include_plus_size"))
parsed["include_black_african"] = bool(parsed.get("include_black_african"))
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
parsed["no_black"] = bool(parsed.get("no_black"))
return parsed
def _normalize_hardcore_position_family(value: Any, default: str = "any") -> str:
text = str(value or default).strip()
return text if text in HARDCORE_POSITION_FAMILY_CHOICES else default
def _normalize_hardcore_position_values(values: Any) -> list[str]:
raw_values = _list_from(values)
selected: list[str] = []
for value in raw_values:
text = str(value or "").strip()
if not text or text == "any":
continue
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
if normalized in HARDCORE_POSITION_KEY_CHOICES and normalized not in selected:
selected.append(normalized)
return selected
def _empty_hardcore_position_config() -> dict[str, Any]:
return {
"config_type": "hardcore_position",
"enabled": False,
"family": "any",
"positions": [],
"require_position": False,
"allow_toys": True,
"allow_double": True,
"allow_penetration": True,
"allow_foreplay": True,
"allow_interaction": True,
"allow_manual": True,
"allow_oral": True,
"allow_outercourse": True,
"allow_anal": True,
"allow_climax": True,
}
def _parse_hardcore_position_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return _empty_hardcore_position_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return _empty_hardcore_position_config()
if not isinstance(raw, dict):
return _empty_hardcore_position_config()
parsed = {**_empty_hardcore_position_config(), **raw}
parsed["enabled"] = bool(parsed.get("enabled", True))
parsed["family"] = _normalize_hardcore_position_family(parsed.get("family"))
parsed["positions"] = _normalize_hardcore_position_values(parsed.get("positions"))
parsed["require_position"] = not _is_false(parsed.get("require_position", False))
for key in (
"allow_toys",
"allow_double",
"allow_penetration",
"allow_foreplay",
"allow_interaction",
"allow_manual",
"allow_oral",
"allow_outercourse",
"allow_anal",
"allow_climax",
):
parsed[key] = not _is_false(parsed.get(key, True))
return parsed
def _hardcore_position_summary(config: dict[str, Any]) -> str:
if not config.get("enabled"):
return "hardcore position unrestricted"
parts = [f"family={config.get('family', 'any')}"]
positions = config.get("positions") or []
if positions:
parts.append("positions=" + ",".join(positions))
elif config.get("require_position"):
parts.append("position_templates=required")
disabled = [
label
for key, label in (
("allow_toys", "toys"),
("allow_double", "double"),
("allow_penetration", "penetration"),
("allow_foreplay", "foreplay"),
("allow_interaction", "interaction"),
("allow_manual", "manual"),
("allow_oral", "oral"),
("allow_outercourse", "outercourse"),
("allow_anal", "anal"),
("allow_climax", "climax"),
)
if not config.get(key, True)
]
if disabled:
parts.append("blocked=" + ",".join(disabled))
return "; ".join(parts)
def build_hardcore_position_pool_json(
hardcore_position_config: str | dict[str, Any] | None = "",
combine_mode: str = "replace",
family: str = "any",
selected_positions: list[str] | tuple[str, ...] | str | None = None,
) -> str:
base = _parse_hardcore_position_config(hardcore_position_config)
if combine_mode == "replace":
base = {**_empty_hardcore_position_config(), "enabled": True}
else:
base["enabled"] = True
base["family"] = _normalize_hardcore_position_family(family, base.get("family", "any"))
selected = _normalize_hardcore_position_values(selected_positions)
if combine_mode == "add":
existing = list(base.get("positions") or [])
for value in selected:
if value not in existing:
existing.append(value)
base["positions"] = existing
else:
base["positions"] = selected
base["require_position"] = bool(base.get("require_position")) or bool(base["positions"]) or base["family"] != "any"
base["summary"] = _hardcore_position_summary(base)
return json.dumps(base, ensure_ascii=True, sort_keys=True)
def build_hardcore_action_filter_json(
hardcore_position_config: str | dict[str, Any] | None = "",
focus: str = "keep_pool",
allow_toys: bool = False,
allow_double: bool = False,
allow_penetration: bool = True,
allow_foreplay: bool = True,
allow_interaction: bool = True,
allow_manual: bool = True,
allow_oral: bool = True,
allow_outercourse: bool = True,
allow_anal: bool = True,
allow_climax: bool = True,
) -> str:
config = _parse_hardcore_position_config(hardcore_position_config)
config["enabled"] = True
focus = str(focus or "keep_pool").strip()
focus_family = {
"penetration_only": "penetrative",
"foreplay_only": "foreplay",
"interaction_only": "interaction",
"manual_only": "manual",
"oral_only": "oral",
"outercourse_only": "outercourse",
"anal_only": "anal",
"climax_only": "climax",
"threesome_only": "threesome",
"group_only": "group",
}.get(focus)
if focus_family:
config["family"] = focus_family
config["allow_toys"] = bool(allow_toys)
config["allow_double"] = bool(allow_double)
config["allow_penetration"] = bool(allow_penetration)
config["allow_foreplay"] = bool(allow_foreplay)
config["allow_interaction"] = bool(allow_interaction)
config["allow_manual"] = bool(allow_manual)
config["allow_oral"] = bool(allow_oral)
config["allow_outercourse"] = bool(allow_outercourse)
config["allow_anal"] = bool(allow_anal)
config["allow_climax"] = bool(allow_climax)
if not focus_family and config["family"] != "any":
enabled_action_families = {
family
for enabled, family in (
(config["allow_penetration"], "penetrative"),
(config["allow_foreplay"], "foreplay"),
(config["allow_interaction"], "interaction"),
(config["allow_manual"], "manual"),
(config["allow_oral"], "oral"),
(config["allow_outercourse"], "outercourse"),
(config["allow_anal"], "anal"),
(config["allow_climax"], "climax"),
)
if enabled
}
if config["family"] in enabled_action_families and len(enabled_action_families) > 1:
config["family"] = "any"
if focus == "foreplay_only":
config["allow_foreplay"] = True
config["allow_interaction"] = True
elif focus == "interaction_only":
config["allow_interaction"] = True
config["allow_foreplay"] = True
elif focus == "manual_only":
config["allow_manual"] = True
elif focus == "oral_only":
config["allow_oral"] = True
config["allow_penetration"] = False
elif focus == "outercourse_only":
config["allow_outercourse"] = True
config["allow_oral"] = False
config["allow_penetration"] = False
elif focus == "anal_only":
config["allow_anal"] = True
config["allow_penetration"] = True
elif focus == "climax_only":
config["allow_climax"] = True
config["summary"] = _hardcore_position_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _hardcore_position_config_active(config: dict[str, Any]) -> bool:
return bool(config.get("enabled"))
def _hardcore_position_template_required(config: dict[str, Any]) -> bool:
if not _hardcore_position_config_active(config):
return False
return bool(config.get("require_position")) or bool(config.get("positions")) or _normalize_hardcore_position_family(config.get("family")) != "any"
def _is_hardcore_sexual_category(category: dict[str, Any]) -> bool:
return str(category.get("slug") or "").strip() == "hardcore_sexual_poses" or str(category.get("name") or "").strip().lower() == "hardcore sexual poses"
def _hardcore_allowed_subcategory_slugs(config: dict[str, Any]) -> set[str]:
family = _normalize_hardcore_position_family(config.get("family"))
allowed = set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES.get(family, HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"]))
if not config.get("allow_penetration", True):
allowed.difference_update({"penetrative_sex", "anal_double_penetration", "threesomes", "group_sex_orgy"})
if not config.get("allow_foreplay", True):
allowed.discard("foreplay_teasing")
if not config.get("allow_interaction", True):
allowed.difference_update(
{
"foreplay_teasing",
"body_worship_touching",
"clothing_position_transitions",
"dominant_guidance",
"camera_performance",
"group_coordination",
"aftercare_cleanup",
}
)
if not config.get("allow_manual", True):
allowed.discard("manual_stimulation")
if not config.get("allow_oral", True):
allowed.discard("oral_sex")
if not config.get("allow_outercourse", True):
allowed.discard("outercourse_sex")
if not config.get("allow_anal", True):
allowed.discard("anal_double_penetration")
if not config.get("allow_climax", True):
allowed.discard("cumshot_climax")
if not config.get("allow_double", True) and family == "anal":
allowed.add("anal_double_penetration")
return allowed or set(HARDCORE_POSITION_FAMILY_SUBCATEGORIES["any"])
def _filter_hardcore_categories_for_position(
categories: list[dict[str, Any]],
config: dict[str, Any],
women_count: int,
men_count: int,
) -> list[dict[str, Any]]:
if not _hardcore_position_config_active(config):
return categories
allowed = _hardcore_allowed_subcategory_slugs(config)
filtered_categories: list[dict[str, Any]] = []
for category in categories:
if not _is_hardcore_sexual_category(category):
filtered_categories.append(category)
continue
category_copy = dict(category)
subcategories = [
subcategory
for subcategory in category.get("subcategories", [])
if str(subcategory.get("slug") or "") in allowed and _compatible_entry(subcategory, women_count, men_count)
and _hardcore_subcategory_supports_positions(subcategory, config)
]
if subcategories:
category_copy["subcategories"] = subcategories
filtered_categories.append(category_copy)
return filtered_categories
def _hardcore_text_blocked_by_action(text: str, axis_name: str, config: dict[str, Any]) -> bool:
text = str(text or "").lower()
axis_name = str(axis_name or "").lower()
if not config.get("allow_toys", True) and any(term in text for term in ("toy", "dildo", "strap-on", "strap on")):
return True
if not config.get("allow_double", True) and (
axis_name == "double_act"
or any(term in text for term in ("double penetration", "double-penetration", "front-and-back", "front and back", "second penetration", "both sides", "two partners penetrating", "multiple penetrations"))
):
return True
if not config.get("allow_anal", True) and (
axis_name == "anal_act"
or any(term in text for term in (" anal", "anal sex", "anal penetration", "anus", "rear-entry anal", "penis entering ass", "thrusts into her ass", "thrusts into his ass"))
):
return True
if not config.get("allow_oral", True) and (
axis_name in ("oral_act", "oral_detail")
or any(term in text for term in ("oral sex", "mouth on genitals", "mouth on pussy", "blowjob", "cunnilingus", "tongue on pussy", "deepthroat", "fellatio"))
):
return True
if not config.get("allow_outercourse", True) and (
axis_name in ("outer_act", "contact_detail", "texture_detail")
or any(term in text for term in ("boobjob", "titjob", "breast sex", "breast-sex", "testicle", "balls", "penis licking", "penis-licking", "footjob", "soles", "toes"))
):
return True
if not config.get("allow_penetration", True) and (
axis_name in ("penetration_act", "penetration_detail", "anal_act", "double_act", "thrust_detail")
or any(term in text for term in ("penetration", "penetrative", "thrust", "penis entering", "vaginal sex", "anal sex"))
):
return True
if not config.get("allow_foreplay", True) and (
axis_name in ("tease_act", "touch_detail", "clothing_detail", "foreplay_detail", "face_detail", "body_contact", "mood_detail")
or any(
term in text
for term in (
"kiss",
"kissing",
"mouth-to-mouth",
"caress",
"caressing",
"stroking skin",
"hands roaming",
"touching breasts",
"cupping breasts",
"hand on the cheek",
"fingers under the chin",
"undressing",
"removing clothing",
"removing clothes",
"pulling clothing",
"sliding straps",
"unbuttoning",
)
)
):
return True
if not config.get("allow_interaction", True) and (
axis_name
in (
"tease_act",
"touch_detail",
"clothing_detail",
"foreplay_detail",
"face_detail",
"body_contact",
"mood_detail",
"worship_act",
"transition_act",
"control_act",
"performance_act",
"coordination_act",
"aftercare_act",
"cleanup_detail",
)
or any(
term in text
for term in (
"kiss",
"kissing",
"caress",
"body worship",
"nipple",
"ass grab",
"thigh",
"hair holding",
"wrists",
"dirty talk",
"whispering",
"undressing",
"position transition",
"guided",
"camera",
"watching",
"aftercare",
"cleanup",
"wiping",
)
)
):
return True
if not config.get("allow_manual", True) and (
axis_name in ("manual_act", "manual_detail")
or any(
term in text
for term in (
"fingering",
"fingers inside",
"clit",
"clitoris",
"manual stimulation",
"mutual masturbation",
"masturbating together",
"fingers on pussy",
"fingers on clit",
)
)
):
return True
if not config.get("allow_climax", True) and (
axis_name in ("climax_act", "climax_hint", "climax_detail", "fluid_detail", "fluid_location")
or any(term in text for term in ("climax", "cum", "semen", "ejaculat", "creampie", "post-orgasm", "post-penetration"))
):
return True
return False
def _hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
positions = config.get("positions") or []
if not positions:
return True
text = _entry_text(entry).lower()
for position in positions:
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
return True
return False
def _hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> bool:
selected = set(config.get("positions") or [])
if not selected:
return False
text = _entry_text(entry).lower()
matched = {
position
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
if any(term in text for term in terms)
}
return bool(matched) and not bool(matched & selected)
def _hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool:
if not _hardcore_position_template_required(config):
return True
axes = subcategory.get("item_axes")
if not isinstance(axes, dict):
return True
for axis_name, values in axes.items():
if str(axis_name) in HARDCORE_POSITION_AXIS_KEYS and any(
_hardcore_position_entry_matches(value, config)
for value in _list_from(values)
):
return True
return False
def _filter_hardcore_axis(axis_name: str, values: list[Any], config: dict[str, Any]) -> list[Any]:
if not _hardcore_position_config_active(config):
return values
filtered = [
value
for value in values
if not _hardcore_text_blocked_by_action(_entry_text(value), axis_name, config)
and not (axis_name not in HARDCORE_POSITION_AXIS_KEYS and _hardcore_position_entry_conflicts(value, config))
and (axis_name not in HARDCORE_POSITION_AXIS_KEYS or _hardcore_position_entry_matches(value, config))
]
return filtered or values
def _filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> list[Any]:
if not _hardcore_position_config_active(config):
return templates
filtered: list[Any] = []
for template in templates:
text = _entry_text(template)
fields = {key for _, key, _, _ in Formatter().parse(text) if key}
blocked = _hardcore_position_template_required(config) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS)
blocked = blocked or any(_hardcore_text_blocked_by_action(text, field, config) for field in fields | {""})
if not blocked:
filtered.append(template)
return filtered or templates
def _apply_hardcore_position_config_to_subcategory(
subcategory: dict[str, Any],
config: dict[str, Any],
) -> dict[str, Any]:
if not _hardcore_position_config_active(config):
return subcategory
subcategory_copy = dict(subcategory)
if "item_templates" in subcategory_copy:
subcategory_copy["item_templates"] = _filter_hardcore_templates(_list_from(subcategory_copy["item_templates"]), config)
raw_axes = subcategory_copy.get("item_axes")
if isinstance(raw_axes, dict):
axes = {}
for axis_name, values in raw_axes.items():
axes[axis_name] = _filter_hardcore_axis(str(axis_name), _list_from(values), config)
subcategory_copy["item_axes"] = axes
subcategory_copy["hardcore_position_config"] = config
return subcategory_copy
def _ratio_or_none(value: float) -> float | None:
try:
ratio = float(value)
except (TypeError, ValueError):
return None
if ratio < 0:
return None
return max(0.0, min(1.0, ratio))
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
try:
number = float(value)
except (TypeError, ValueError):
return default
return max(min_value, min(max_value, number))
def build_seed_config_json(
category_seed: int = -1,
subcategory_seed: int = -1,
content_seed: int = -1,
person_seed: int = -1,
scene_seed: int = -1,
pose_seed: int = -1,
role_seed: int = -1,
expression_seed: int = -1,
composition_seed: int = -1,
category_seed_mode: str = "auto",
subcategory_seed_mode: str = "auto",
content_seed_mode: str = "auto",
person_seed_mode: str = "auto",
scene_seed_mode: str = "auto",
pose_seed_mode: str = "auto",
role_seed_mode: str = "auto",
expression_seed_mode: str = "auto",
composition_seed_mode: str = "auto",
) -> str:
return seed_policy.build_seed_config_json(
category_seed=category_seed,
subcategory_seed=subcategory_seed,
content_seed=content_seed,
person_seed=person_seed,
scene_seed=scene_seed,
pose_seed=pose_seed,
role_seed=role_seed,
expression_seed=expression_seed,
composition_seed=composition_seed,
category_seed_mode=category_seed_mode,
subcategory_seed_mode=subcategory_seed_mode,
content_seed_mode=content_seed_mode,
person_seed_mode=person_seed_mode,
scene_seed_mode=scene_seed_mode,
pose_seed_mode=pose_seed_mode,
role_seed_mode=role_seed_mode,
expression_seed_mode=expression_seed_mode,
composition_seed_mode=composition_seed_mode,
)
def build_seed_lock_config_json(
base_seed: int = 20260614,
reroll_axis: str = "none",
reroll_seed: int = -1,
) -> str:
return seed_policy.build_seed_lock_config_json(
base_seed=base_seed,
reroll_axis=reroll_axis,
reroll_seed=reroll_seed,
)
def _parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
return seed_policy.parse_seed_config(seed_config)
def _configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
return seed_policy.configured_axis_seed(seed_config, axis)
def _axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
return seed_policy.axis_rng(seed_config, axis, base_seed, row_number)
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 _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 camera_policy.camera_mode_choices()
def ethnicity_choices() -> list[str]:
return list(ETHNICITY_FILTER_CHOICES)
def character_label_choices() -> list[str]:
return list(CHARACTER_LABEL_CHOICES)
def character_age_choices() -> list[str]:
return list(CHARACTER_AGE_CHOICES)
def character_body_choices() -> list[str]:
return list(CHARACTER_BODY_CHOICES)
def character_woman_body_choices() -> list[str]:
return list(CHARACTER_WOMAN_BODY_CHOICES)
def character_man_body_choices() -> list[str]:
return list(CHARACTER_MAN_BODY_CHOICES)
def character_descriptor_detail_choices() -> list[str]:
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
def character_presence_choices() -> list[str]:
return list(CHARACTER_PRESENCE_CHOICES)
def character_hair_color_choices() -> list[str]:
return list(CHARACTER_HAIR_COLOR_CHOICES)
def character_hair_length_choices() -> list[str]:
return list(CHARACTER_HAIR_LENGTH_CHOICES)
def character_hair_style_choices() -> list[str]:
return list(CHARACTER_HAIR_STYLE_CHOICES)
def character_eye_color_choices() -> list[str]:
return list(CHARACTER_EYE_COLOR_CHOICES)
def character_ethnicity_choices() -> list[str]:
return ["random"] + list(ETHNICITY_FILTER_CHOICES)
def character_figure_choices() -> list[str]:
return ["random", "curvy", "balanced", "bombshell"]
def camera_detail_choices() -> list[str]:
return camera_policy.camera_detail_choices()
def hardcore_detail_density_choices() -> list[str]:
return list(HARDCORE_DETAIL_DENSITY_CHOICES)
def hardcore_position_family_choices() -> list[str]:
return list(HARDCORE_POSITION_FAMILY_CHOICES)
def hardcore_position_focus_choices() -> list[str]:
return list(HARDCORE_POSITION_FOCUS_CHOICES)
def hardcore_position_key_choices() -> list[str]:
return list(HARDCORE_POSITION_KEY_CHOICES)
def character_softcore_outfit_source_choices() -> list[str]:
return [
"no_change",
"social_tease",
"lingerie_tease",
"implied_nude",
"explicit_tease",
"explicit_nude",
"partner_woman",
"partner_man",
"custom",
]
def character_hardcore_clothing_state_choices() -> list[str]:
return [
"no_change",
"fully_nude",
"partly_exposed",
"same_outfit",
"partially_removed",
"custom",
]
def camera_orbit_framing_choices() -> list[str]:
return camera_policy.camera_orbit_framing_choices()
def camera_orbit_focus_choices() -> list[str]:
return camera_policy.camera_orbit_focus_choices()
def camera_shot_choices() -> list[str]:
return camera_policy.camera_shot_choices()
def camera_angle_choices() -> list[str]:
return camera_policy.camera_angle_choices()
def camera_lens_choices() -> list[str]:
return camera_policy.camera_lens_choices()
def camera_distance_choices() -> list[str]:
return camera_policy.camera_distance_choices()
def camera_orientation_choices() -> list[str]:
return camera_policy.camera_orientation_choices()
def camera_phone_choices() -> list[str]:
return camera_policy.camera_phone_choices()
def camera_priority_choices() -> list[str]:
return camera_policy.camera_priority_choices()
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 camera_policy.build_camera_config_json(
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,
)
def _camera_orbit_direction(horizontal_angle: Any) -> str:
return camera_policy._camera_orbit_direction(horizontal_angle)
def _camera_orbit_elevation(vertical_angle: Any) -> str:
return camera_policy._camera_orbit_elevation(vertical_angle)
def _camera_orbit_distance(zoom: Any, framing: str = "from_zoom") -> str:
return camera_policy._camera_orbit_distance(zoom, framing)
def _camera_orbit_focus(subject_focus: str) -> str:
return camera_policy._camera_orbit_focus(subject_focus)
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]]:
return camera_policy.camera_orbit_prompt(
horizontal_angle,
vertical_angle,
zoom,
framing=framing,
subject_focus=subject_focus,
include_degrees=include_degrees,
)
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:
return camera_policy.build_camera_orbit_config_json(
enabled=enabled,
camera_mode=camera_mode,
horizontal_angle=horizontal_angle,
vertical_angle=vertical_angle,
zoom=zoom,
framing=framing,
subject_focus=subject_focus,
lens=lens,
orientation=orientation,
phone_visibility=phone_visibility,
priority=priority,
camera_detail=camera_detail,
include_degrees=include_degrees,
)
QWEN_CAMERA_DIRECTIONS = camera_policy.QWEN_CAMERA_DIRECTIONS
QWEN_CAMERA_ELEVATIONS = camera_policy.QWEN_CAMERA_ELEVATIONS
QWEN_CAMERA_ZOOMS = camera_policy.QWEN_CAMERA_ZOOMS
QWEN_CAMERA_SCENE_CENTER_Y = camera_policy.QWEN_CAMERA_SCENE_CENTER_Y
def _qwen_prompt_camera_values(qwen_prompt: Any) -> tuple[int, int, float]:
return camera_policy._qwen_prompt_camera_values(qwen_prompt)
def _camera_info_dict(camera_info: Any) -> dict[str, Any] | None:
return camera_policy._camera_info_dict(camera_info)
def _qwen_camera_info_values(camera_info: Any) -> tuple[int, int, float] | None:
return camera_policy._qwen_camera_info_values(camera_info)
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:
return camera_policy.build_qwen_camera_config_json(
qwen_prompt=qwen_prompt,
camera_info=camera_info,
prefer_camera_info=prefer_camera_info,
camera_mode=camera_mode,
subject_focus=subject_focus,
lens=lens,
orientation=orientation,
phone_visibility=phone_visibility,
priority=priority,
camera_detail=camera_detail,
include_degrees=include_degrees,
suppress_phone_visibility=suppress_phone_visibility,
)
def _choice(value: Any, choices: dict[str, str], default: str) -> str:
return camera_policy._choice(value, choices, default)
def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
return camera_policy.parse_camera_config(camera_config)
def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, Any]:
return camera_policy.camera_config_with_mode(camera_config, camera_mode)
def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, Any]]:
return camera_policy.camera_directive(camera_config)
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:
return camera_policy.camera_caption_text(parsed)
def _coworking_composition_prompt(scene_text: Any, composition: Any, subject_kind: str = "subjects") -> str:
return scene_camera_adapters.coworking_composition_prompt(scene_text, composition, subject_kind)
def _apply_coworking_composition(row: dict[str, Any], subject_kind: str) -> dict[str, Any]:
scene_text = row.get("scene_text") or row.get("source_scene_text") or row.get("scene")
old_composition = str(row.get("composition") or "").strip()
new_composition = _coworking_composition_prompt(scene_text, old_composition, subject_kind)
if not old_composition or new_composition == old_composition:
return row
row["source_composition"] = row.get("source_composition") or old_composition
row["composition"] = new_composition
row["composition_prompt"] = _composition_prompt(new_composition)
prompt = str(row.get("prompt") or "")
replacements = (
(f"Composition: vertical {old_composition}.", f"Composition: {_composition_prompt(new_composition)}."),
(f"Composition: {old_composition}.", f"Composition: {_composition_prompt(new_composition)}."),
(f"Framed as {old_composition}.", f"Framed as {new_composition}."),
)
for old_fragment, new_fragment in replacements:
if old_fragment in prompt:
row["prompt"] = prompt.replace(old_fragment, new_fragment)
break
row["caption"] = str(row.get("caption") or "").replace(f", {old_composition},", f", {new_composition},")
return row
def _camera_scene_directive_for_context(
scene_text: Any,
composition: Any,
camera_config: str | dict[str, Any] | None,
pov_labels: list[str] | None = None,
subject_kind: str = "subjects",
) -> tuple[str, dict[str, Any]]:
parsed = _parse_camera_config(camera_config)
directive = scene_camera_adapters.camera_scene_directive_for_context(
scene_text,
parsed,
pov_labels,
subject_kind,
CAMERA_COMPACT_LABELS,
)
return directive, parsed
def _row_camera_subject_kind(row: dict[str, Any]) -> str:
subject_type = str(row.get("subject_type") or row.get("primary_subject") or "").lower()
if subject_type in ("woman", "adult woman") or subject_type == "single_any":
return "woman"
if subject_type in ("man", "adult man"):
return "man"
try:
women_count = int(row.get("women_count") or 0)
men_count = int(row.get("men_count") or 0)
except (TypeError, ValueError):
women_count = men_count = 0
if women_count == 1 and men_count == 0:
return "woman"
if women_count == 0 and men_count == 1:
return "man"
if women_count + men_count == 2:
return "couple"
return "subjects"
def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
directive, parsed = _camera_directive(camera_config)
pov_labels = _pov_character_labels(
_character_slot_label_map(_parse_character_cast(row.get("character_cast_slots"))),
int(row.get("men_count") or 0) if str(row.get("men_count") or "").isdigit() else 0,
)
if not pov_labels:
pov_labels = [str(label) for label in _list_from(row.get("pov_character_labels")) if str(label).strip()]
subject_kind = _row_camera_subject_kind(row)
row = _apply_coworking_composition(row, subject_kind)
scene_directive, parsed = _camera_scene_directive_for_context(
row.get("scene_text") or row.get("source_scene_text") or row.get("scene"),
row.get("composition") or row.get("source_composition"),
parsed,
pov_labels,
subject_kind,
)
row["camera_config"] = parsed
row["camera_scene_directive"] = scene_directive
row["camera_directive"] = "" if pov_labels else directive
combined_directive = " ".join(part for part in (scene_directive, row["camera_directive"]) if part)
if not combined_directive:
return row
row["prompt"] = _insert_positive_directive(row["prompt"], combined_directive)
camera_caption = _camera_caption_text(parsed)
if camera_caption and not pov_labels:
row["caption"] = f"{row.get('caption', '').rstrip()}, {camera_caption}"
return row
def _row_seed(seed: int, row_number: int, salt: int = 0) -> int:
return seed_policy.row_seed(seed, row_number, salt)
def _pick_clothing_mode(rng: random.Random, clothing: str, minimal_ratio: float | None) -> str:
if clothing == "random":
return "minimal" if rng.random() < 0.5 else "full"
if minimal_ratio is None:
return clothing
return "minimal" if rng.random() < minimal_ratio else "full"
def _pick_pose_mode(rng: random.Random, poses: str, standard_ratio: float | None) -> str:
if poses == "random":
return "standard" if rng.random() < 0.5 else "evocative"
if standard_ratio is None:
return poses
return "standard" if rng.random() < standard_ratio else "evocative"
def _pick_figure_bias(rng: random.Random, figure: str) -> str:
if figure in ("curvy", "balanced", "bombshell"):
return figure
return g.choose(rng, ["curvy", "balanced", "bombshell"])
def _pick_expression_intensity(rng: random.Random, expression_intensity: Any) -> tuple[float, str]:
try:
value = float(expression_intensity)
except (TypeError, ValueError):
return 0.5, "default"
if value < 0:
return round(rng.random(), 2), "random"
return _clamped_float(value, 0.5), "input"
def _build_auto_weighted_row(
row_number: int,
start_index: int,
clothing: str,
ethnicity: str,
poses: str,
backside_bias: float,
figure: str,
no_plus_women: bool,
no_black: bool,
minimal_clothing_ratio: float | None,
standard_pose_ratio: float | None,
seed: int,
) -> dict[str, Any]:
batch_number = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
rows = g.build_rows(
batch_number * g.BATCH_SIZE,
start_index,
clothing,
ethnicity,
poses,
backside_bias,
figure,
no_plus_women,
no_black,
minimal_clothing_ratio,
standard_pose_ratio,
seed,
g.EXPRESSION_SEED + seed,
)
row = rows[row_number - 1]
row["main_category"] = "auto_weighted"
row["subcategory"] = row.get("primary_subject", "auto")
row["source"] = "built_in_generator"
return row
def _build_direct_builtin_row(
category: str,
row_number: int,
start_index: int,
clothing: str,
ethnicity: str,
poses: str,
backside_bias: float,
figure: str,
no_plus_women: bool,
no_black: bool,
minimal_clothing_ratio: float | None,
standard_pose_ratio: float | None,
seed: int,
) -> dict[str, Any]:
rng = random.Random(_row_seed(seed, row_number))
expr_deck = g.ExpressionDeck(g.EXPRESSIONS, random.Random(_row_seed(g.EXPRESSION_SEED + seed, row_number)))
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
index = start_index + row_number - 1
row_clothing = _pick_clothing_mode(rng, clothing, minimal_clothing_ratio)
row_poses = _pick_pose_mode(rng, poses, standard_pose_ratio)
if category == "woman":
row = g.make_single(
index,
batch,
rng,
"woman",
expr_deck,
row_clothing,
ethnicity,
row_poses,
backside_bias,
figure,
no_plus_women,
no_black,
)
elif category == "man":
row = g.make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_poses, backside_bias, figure)
elif category == "couple":
row = g.make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
elif category == "group_or_layout":
row = g.make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
else:
raise ValueError(f"Unknown built-in category: {category}")
row["main_category"] = category
row["subcategory"] = row.get("pose_mode", category)
row["source"] = "built_in_generator"
return row
def _auto_full_choice(seed_config: dict[str, int], seed: int, row_number: int) -> str:
categories = load_category_library()
if not categories:
return "auto_weighted"
category_rng = _axis_rng(seed_config, "category", seed, row_number)
choices: list[dict[str, Any]] = [{"category": "auto_weighted", "weight": 1.0}]
choices.extend(
{
"category": category["name"],
"weight": category.get("weight", 1.0),
}
for category in categories
)
choice = _weighted_choice(category_rng, choices)
return str(choice.get("category") or "auto_weighted")
def _body_phrase(body: Any, figure_note: Any = "") -> str:
body = str(body or "").strip()
figure_note = str(figure_note or "").strip()
if not body:
return figure_note
if not figure_note:
return f"{body} figure"
if "figure" in figure_note.lower():
return f"{body} build and {figure_note}"
return f"{body} figure with {figure_note}"
def _safe_profile_name(profile_name: str) -> str:
profile_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(profile_name or "").strip()).strip("_")
return profile_name[:64] or "profile"
def _profile_path(profile_name: str) -> Path:
return PROFILE_DIR / f"{_safe_profile_name(profile_name)}.json"
def character_profile_choices() -> list[str]:
if not PROFILE_DIR.exists():
return ["manual"]
names = sorted(path.stem for path in PROFILE_DIR.glob("*.json") if path.is_file())
return ["manual"] + names
def _load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[str, Any]:
if not value:
return {}
if isinstance(value, dict):
return value
try:
raw = json.loads(str(value))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid {label} JSON: {exc}") from exc
if not isinstance(raw, dict):
raise ValueError(f"{label} must be a JSON object")
return raw
CHARACTER_MANUAL_FIELDS = (
"manual_age",
"manual_body",
"body_phrase",
"skin",
"hair",
"eyes",
"softcore_outfit",
"hardcore_clothing",
)
def _parse_character_manual_config(value: str | dict[str, Any] | None) -> dict[str, str]:
if not value:
return {}
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return {}
if not isinstance(raw, dict):
return {}
return {
key: str(raw.get(key) or "").strip()
for key in CHARACTER_MANUAL_FIELDS
if str(raw.get(key) or "").strip()
}
def _character_manual_summary(config: dict[str, str]) -> str:
parts = [f"{key}={value}" for key, value in config.items() if value]
return "; ".join(parts) if parts else "manual unrestricted"
def build_character_manual_config_json(
manual: str | dict[str, Any] | None = "",
combine_mode: str = "merge_nonempty",
manual_age: str = "",
manual_body: str = "",
body_phrase: str = "",
skin: str = "",
hair: str = "",
eyes: str = "",
softcore_outfit: str = "",
hardcore_clothing: str = "",
) -> str:
base = {} if combine_mode == "replace_all" else _parse_character_manual_config(manual)
updates = {
"manual_age": manual_age,
"manual_body": manual_body,
"body_phrase": body_phrase,
"skin": skin,
"hair": hair,
"eyes": eyes,
"softcore_outfit": softcore_outfit,
"hardcore_clothing": hardcore_clothing,
}
for key, value in updates.items():
value = str(value or "").strip()
if value:
base[key] = value
result = {"config_type": "character_manual", **base}
result["summary"] = _character_manual_summary(base)
return json.dumps(result, ensure_ascii=True, sort_keys=True)
def _slot_value(value: Any) -> str:
text = str(value or "").strip()
if text.lower() in CHARACTER_RANDOM_TOKENS:
return ""
return text
CHARACTER_CHARACTERISTIC_AXES = {
"ages": CHARACTER_AGE_CHOICES,
"bodies": list(dict.fromkeys([*CHARACTER_BODY_CHOICES, *CHARACTER_WOMAN_BODY_CHOICES, *CHARACTER_MAN_BODY_CHOICES])),
"eyes": CHARACTER_EYE_COLOR_CHOICES,
}
def _empty_characteristics_config() -> dict[str, Any]:
return {
"config_type": "characteristics",
"ages": [],
"bodies": [],
"eyes": [],
"softcore_outfits": [],
"hardcore_clothing": [],
}
def _normalize_characteristic_choice(value: Any, choices: list[str] | tuple[str, ...]) -> str:
text = str(value or "").strip()
if not text:
return ""
normalized = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
for choice in choices:
if normalized == re.sub(r"[^a-z0-9]+", "_", str(choice).lower()).strip("_"):
return str(choice)
return ""
def _normalize_characteristic_values(
values: Any,
choices: list[str] | tuple[str, ...] | None = None,
*,
allow_free_text: bool = False,
) -> list[str]:
if isinstance(values, str):
raw_values = [part.strip() for part in re.split(r"[\n;]+", values) if part.strip()]
if len(raw_values) == 1 and "," in raw_values[0] and not allow_free_text:
raw_values = [part.strip() for part in raw_values[0].split(",") if part.strip()]
elif isinstance(values, (list, tuple, set)):
raw_values = list(values)
else:
raw_values = []
normalized: list[str] = []
for raw_value in raw_values:
value = str(raw_value or "").strip() if choices is None else _normalize_characteristic_choice(raw_value, choices)
if not value or value in ("random", "manual"):
continue
if value not in normalized:
normalized.append(value)
return normalized
def _parse_characteristics_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return _empty_characteristics_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return _empty_characteristics_config()
if not isinstance(raw, dict):
return _empty_characteristics_config()
return {
"config_type": "characteristics",
"ages": _normalize_characteristic_values(raw.get("ages"), CHARACTER_AGE_CHOICES),
"bodies": _normalize_characteristic_values(raw.get("bodies"), CHARACTER_CHARACTERISTIC_AXES["bodies"]),
"eyes": _normalize_characteristic_values(raw.get("eyes"), CHARACTER_EYE_COLOR_CHOICES),
"softcore_outfits": _normalize_characteristic_values(raw.get("softcore_outfits"), None, allow_free_text=True),
"hardcore_clothing": _normalize_characteristic_values(raw.get("hardcore_clothing"), None, allow_free_text=True),
}
def _characteristics_summary(config: dict[str, Any]) -> str:
parts = []
for key, label in (
("ages", "ages"),
("bodies", "bodies"),
("eyes", "eyes"),
("softcore_outfits", "soft_outfits"),
("hardcore_clothing", "hard_clothing"),
):
values = config.get(key) or []
if not values:
continue
if key in ("softcore_outfits", "hardcore_clothing"):
parts.append(f"{label}={len(values)}")
else:
parts.append(f"{label}={','.join(values)}")
return "; ".join(parts) if parts else "characteristics unrestricted"
def build_characteristics_config_json(
characteristics: str | dict[str, Any] | None = "",
axis: str = "ages",
selected_values: list[str] | tuple[str, ...] | str | None = None,
combine_mode: str = "replace_axis",
) -> str:
config = _parse_characteristics_config(characteristics)
axis_key = str(axis or "").strip().lower()
if axis_key not in config:
config["summary"] = _characteristics_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
choices = CHARACTER_CHARACTERISTIC_AXES.get(axis_key)
values = _normalize_characteristic_values(
selected_values,
choices,
allow_free_text=choices is None,
)
if combine_mode == "add_to_axis":
existing = list(config.get(axis_key) or [])
for value in values:
if value not in existing:
existing.append(value)
config[axis_key] = existing
else:
config[axis_key] = values
config["summary"] = _characteristics_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _characteristic_choice(config: dict[str, Any], key: str, rng: random.Random) -> str:
values = config.get(key) or []
return g.choose(rng, values) if values else ""
def _eye_phrase_from_key(key: str) -> str:
return {
"blue": "blue eyes",
"pale_blue": "pale blue eyes",
"ice_blue": "ice blue eyes",
"blue_gray": "blue-gray eyes",
"green": "green eyes",
"emerald_green": "emerald green eyes",
"hazel": "hazel eyes",
"light_hazel": "light hazel eyes",
"green_hazel": "green-hazel eyes",
"amber": "amber eyes",
"amber_brown": "amber-brown eyes",
"honey_brown": "honey-brown eyes",
"brown": "brown eyes",
"deep_brown": "deep brown eyes",
"dark_brown": "dark brown eyes",
"dark": "dark eyes",
"gray": "gray eyes",
"gray_brown": "gray-brown eyes",
}.get(key, "")
def _normalize_descriptor_detail(value: Any) -> str:
text = str(value or "auto").strip()
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
def _normalize_presence_mode(value: Any, subject_type: str) -> str:
text = str(value or "visible").strip().lower()
if text not in CHARACTER_PRESENCE_CHOICES:
text = "visible"
if subject_type != "man":
return "visible"
return text
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
if not slot:
return False
return slot.get("subject_type") == "man" and slot.get("presence_mode") == "pov"
def _normalize_slot_expression_intensity(value: Any) -> float:
try:
intensity = float(value)
except (TypeError, ValueError):
return -1.0
if intensity < 0:
return -1.0
return _clamped_float(intensity, 0.5)
def _slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
if not slot:
return True
return not _is_false(slot.get("expression_enabled", True))
def _slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
if not slot or not _slot_expression_enabled(slot):
return None
intensity = _normalize_slot_expression_intensity(slot.get("expression_intensity"))
return intensity if intensity >= 0 else None
def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
if not slot or not _slot_expression_enabled(slot):
return None
phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else ""
if phase_key:
intensity = _normalize_slot_expression_intensity(slot.get(phase_key))
if intensity >= 0:
return intensity
return _slot_expression_intensity(slot)
def _normalize_slot_seed(value: Any) -> int:
try:
seed = int(value)
except (TypeError, ValueError):
return -1
if seed < 0:
return -1
return min(seed, CHARACTER_SLOT_SEED_MAX)
def _slot_seed(slot: dict[str, Any] | None) -> int:
if not slot:
return -1
return _normalize_slot_seed(slot.get("slot_seed"))
def _slot_seeded_rng(slot: dict[str, Any] | None, salt: int) -> random.Random | None:
seed = _slot_seed(slot)
if seed < 0:
return None
return random.Random(_row_seed(seed, 1, salt))
def _slot_context_rng(slot: dict[str, Any], fallback_rng: random.Random) -> random.Random:
return _slot_seeded_rng(slot, 701) or fallback_rng
def _slot_effective_figure(
slot: dict[str, Any],
subject_type: str,
fallback_figure: str,
) -> str:
raw_figure = str(slot.get("figure") or "random").strip()
if raw_figure in ("curvy", "balanced", "bombshell"):
return raw_figure
seeded_rng = _slot_seeded_rng(slot, 709)
if subject_type == "woman" and seeded_rng is not None:
return g.choose(seeded_rng, ["curvy", "balanced", "bombshell"])
return fallback_figure
def _mean(values: list[float]) -> float:
return sum(values) / len(values)
def _cast_expression_intensity_override(
fallback: float,
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
expression_phase: str = "",
) -> tuple[float | None, str]:
groups: list[tuple[str, list[str]]] = [
("women", [f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))]),
("men", [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]),
]
all_values: list[float] = []
matching_slots: list[dict[str, Any]] = []
for group_name, labels in groups:
values: list[float] = []
value_labels: list[str] = []
for label in labels:
slot = label_map.get(label)
if _slot_is_pov(slot):
continue
if slot:
matching_slots.append(slot)
value = _slot_expression_intensity_for_phase(slot, expression_phase)
if value is not None:
values.append(value)
value_labels.append(label)
all_values.append(value)
if values:
if len(values) == 1:
return values[0], f"character_slot:{value_labels[0]}"
return _mean(values), f"character_slots:{group_name}"
if all_values:
return _mean(all_values), "character_slots:cast"
if matching_slots and all(not _slot_expression_enabled(slot) for slot in matching_slots):
return None, "character_slots:disabled"
return fallback, "input"
def _character_expression_entries(
rng: random.Random,
expression_pool: list[Any],
fallback_intensity: float,
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
expression_phase: str = "",
) -> list[str]:
labels = [
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
]
expressions: list[str] = []
used: set[str] = set()
for label in labels:
slot = label_map.get(label)
if not slot:
continue
if _slot_is_pov(slot):
continue
if not _slot_expression_enabled(slot):
continue
intensity = _slot_expression_intensity_for_phase(slot, expression_phase)
if intensity is None:
intensity = fallback_intensity
entries = _compatible_entries(
_expression_entries_for_intensity(expression_pool, intensity),
women_count,
men_count,
)
if not entries:
continue
choice = ""
for _attempt in range(5):
candidate = _choose_text(rng, entries)
if candidate not in used:
choice = candidate
break
if not choice:
choice = _choose_text(rng, entries)
used.add(choice)
expressions.append(f"{label} has {choice}")
return expressions
def _sanitize_character_expression_text_for_action(
expression_text: str,
role_graph: Any,
item: Any,
axis_values: Any = None,
) -> str:
text = str(expression_text or "").strip()
if not text:
return ""
context = " ".join(
str(part or "").lower()
for part in (
role_graph,
item,
*((axis_values or {}).values() if isinstance(axis_values, dict) else ()),
)
)
woman_active_outercourse = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"boobjob",
"titjob",
"breast sex",
"breasts tightly",
"testicle",
"balls-licking",
"balls licking",
"penis-licking",
"penis licking",
"handjob",
"hand job",
"footjob",
)
)
)
woman_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"takes man",
"penis in her mouth",
"mouth at penis level",
"fellatio",
"blowjob",
"deepthroat",
"penis sucking",
"lips wrapped",
)
)
)
man_gives_oral = (
re.search(r"\bwoman [a-z]\b", context)
and re.search(r"\bman [a-z]\b", context)
and any(
term in context
for term in (
"mouth on her pussy",
"mouth on woman",
"mouth pressed to her pussy",
"cunnilingus",
"pussy licking",
"tongue on pussy",
)
)
)
mouth_expression_terms = ("mouth", "oral", "tongue", "lips", "gagging", "saliva")
clauses = [clause.strip() for clause in text.split(";") if clause.strip()]
if woman_active_outercourse:
clauses = [clause for clause in clauses if not re.match(r"^Man [A-Z] has\b", clause)]
if woman_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Man [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
if man_gives_oral:
clauses = [
clause
for clause in clauses
if not (
re.match(r"^Woman [A-Z] has\b", clause)
and any(term in clause.lower() for term in mouth_expression_terms)
)
]
return "; ".join(clauses)
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
detail = _normalize_descriptor_detail(descriptor_detail)
if detail != "auto":
return detail
return "compact" if str(subject or "").strip().lower() == "man" else "full"
def _descriptor_from_parts(
subject: Any,
age: Any,
body_phrase: Any,
skin: Any,
hair: Any,
eyes: Any,
descriptor_detail: Any = "auto",
) -> str:
subject = str(subject or "person").strip() or "person"
age_text = " ".join(str(age or "").strip().split())
age_text = age_text.removesuffix(" adults").removesuffix(" adult").strip()
if age_text in ("adult", "adults"):
age_text = ""
subject_phrase = f"{age_text} adult {subject}".strip() if age_text else f"adult {subject}"
detail = _descriptor_detail_for_subject(subject, descriptor_detail)
detail_map = {
"minimal": (body_phrase,),
"compact": (body_phrase, skin),
"medium": (body_phrase, skin, hair),
"full": (body_phrase, skin, hair, eyes),
}
pieces = [subject_phrase, *detail_map.get(detail, detail_map["full"])]
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
choice = str(choice or "").strip()
manual_value = str(manual_value or "").strip()
if choice == "manual":
return manual_value or "random"
if choice.lower() in CHARACTER_RANDOM_TOKENS:
return "random"
return choice
def _normalize_slot_ethnicity(value: Any) -> str:
return normalize_ethnicity_filter(value, "random", allow_random=True)
def _normalize_hair_choice(value: Any, choices: list[str]) -> str:
text = str(value or "random").strip().lower().replace("-", "_").replace(" ", "_")
return text if text in choices else "random"
def _infer_hair_color_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("platinum_blonde", ("platinum-blonde", "platinum blonde", "platinum")),
("strawberry_blonde", ("strawberry-blonde", "strawberry blonde")),
("honey_blonde", ("honey-blonde", "honey blonde")),
("ash_blonde", ("ash-blonde", "ash blonde")),
("dark_blonde", ("dark-blonde", "dark blonde")),
(
"blonde",
(
"light-blonde",
"light blonde",
"blonde",
"flaxen",
"wheat-blonde",
"wheat blonde",
"beige-blonde",
"beige blonde",
"sandy-blonde",
"sandy blonde",
),
),
("silver_gray", ("silver-gray", "silver grey", "silver", "gray", "grey")),
("dark_brown", ("dark-brown", "dark brown", "espresso")),
("chestnut", ("chestnut",)),
("auburn", ("auburn",)),
("copper", ("copper",)),
("red", ("red hair", "redhead")),
("black", ("black",)),
("brown", ("brown", "brunette", "caramel")),
("white", ("white",)),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def _infer_hair_length_key(text: Any) -> str:
value = str(text or "").lower()
if any(token in value for token in ("very long", "waist-length", "hip-length")):
return "very_long"
if "long" in value:
return "long"
if "shoulder-length" in value or "shoulder length" in value:
return "shoulder_length"
if "medium-length" in value or "medium length" in value:
return "medium"
if any(token in value for token in ("bob", "lob")):
return "bob_lob"
if any(token in value for token in ("pixie", "short", "cropped", "tapered")):
return "short"
if any(token in value for token in ("bun", "updo")):
return "updo"
return "random"
def _infer_hair_style_key(text: Any) -> str:
value = str(text or "").lower()
checks = (
("pixie_cut", ("pixie",)),
("messy_bun", ("messy bun",)),
("bun", ("bun", "updo")),
("ponytail", ("ponytail",)),
("braids", ("braids", "box braids", "cornrow")),
("braid", ("braid",)),
("locs", ("locs", "dreadlocks")),
("twists", ("twists",)),
("afro", ("afro",)),
("natural_curls", ("natural curls", "natural coils", "coils")),
("tight_curls", ("tight curls", "tight coils")),
("curls", ("curls", "curly")),
("loose_waves", ("loose waves",)),
("waves", ("waves", "wavy")),
("lob", ("lob",)),
("bob", ("bob",)),
("shag", ("shag",)),
("wet_hair", ("wet hair", "damp hair")),
("slicked_back", ("slicked-back", "slicked back")),
("straight", ("straight", "sleek")),
)
for key, tokens in checks:
if any(token in value for token in tokens):
return key
return "random"
def _choose_hair_key(rng: random.Random, choices: list[str]) -> str:
pool = [choice for choice in choices if choice != "random"]
return g.choose(rng, pool) if pool else "random"
def _normalize_hair_values(values: Any, choices: list[str]) -> list[str]:
if isinstance(values, str):
raw_values = [part.strip() for part in re.split(r"[,;\n]+", values) if part.strip()]
elif isinstance(values, (list, tuple, set)):
raw_values = list(values)
else:
raw_values = []
normalized: list[str] = []
for value in raw_values:
key = _normalize_hair_choice(value, choices)
if key != "random" and key not in normalized:
normalized.append(key)
return normalized
def _empty_hair_config() -> dict[str, Any]:
return {"config_type": "hair_characteristics", "colors": [], "lengths": [], "styles": []}
def _parse_hair_config(value: str | dict[str, Any] | None) -> dict[str, Any]:
if not value:
return _empty_hair_config()
if isinstance(value, dict):
raw = value
else:
try:
raw = json.loads(str(value))
except json.JSONDecodeError:
return _empty_hair_config()
if not isinstance(raw, dict):
return _empty_hair_config()
return {
"config_type": "hair_characteristics",
"colors": _normalize_hair_values(raw.get("colors"), CHARACTER_HAIR_COLOR_CHOICES),
"lengths": _normalize_hair_values(raw.get("lengths"), CHARACTER_HAIR_LENGTH_CHOICES),
"styles": _normalize_hair_values(raw.get("styles"), CHARACTER_HAIR_STYLE_CHOICES),
}
def _hair_config_summary(config: dict[str, Any]) -> str:
parts = []
for label, key in (("colors", "colors"), ("lengths", "lengths"), ("styles", "styles")):
values = config.get(key) or []
if values:
parts.append(f"{label}={','.join(values)}")
return "; ".join(parts) if parts else "hair unrestricted"
def build_hair_config_json(
hair_config: str | dict[str, Any] | None = "",
axis: str = "color",
selected_values: list[str] | tuple[str, ...] | str | None = None,
combine_mode: str = "replace_axis",
) -> str:
config = _parse_hair_config(hair_config)
axis_key = {"color": "colors", "length": "lengths", "style": "styles"}.get(str(axis or "").strip().lower())
choice_map = {
"colors": CHARACTER_HAIR_COLOR_CHOICES,
"lengths": CHARACTER_HAIR_LENGTH_CHOICES,
"styles": CHARACTER_HAIR_STYLE_CHOICES,
}
if axis_key:
values = _normalize_hair_values(selected_values, choice_map[axis_key])
if combine_mode == "add_to_axis":
existing = list(config.get(axis_key) or [])
for value in values:
if value not in existing:
existing.append(value)
config[axis_key] = existing
else:
config[axis_key] = values
config["summary"] = _hair_config_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _hair_color_text(key: str) -> str:
return {
"black": "black",
"brown": "brown",
"dark_brown": "dark-brown",
"chestnut": "chestnut",
"auburn": "auburn",
"copper": "copper",
"red": "red",
"blonde": "blonde",
"platinum_blonde": "platinum-blonde",
"ash_blonde": "ash-blonde",
"honey_blonde": "honey-blonde",
"strawberry_blonde": "strawberry-blonde",
"dark_blonde": "dark-blonde",
"silver_gray": "silver-gray",
"white": "white",
}.get(key, "brown")
def _hair_length_text(key: str) -> str:
return {
"very_short": "very short",
"short": "short",
"bob_lob": "",
"shoulder_length": "shoulder-length",
"medium": "medium-length",
"long": "long",
"very_long": "very long",
"updo": "",
}.get(key, "")
def _hair_phrase_from_parts(color_key: str, length_key: str, style_key: str) -> str:
color = _hair_color_text(color_key)
length = _hair_length_text(length_key)
prefix = " ".join(part for part in (length, color) if part)
if style_key == "pixie_cut":
return f"short {color} pixie cut"
if style_key == "bob":
return f"{color} bob" if length_key in ("random", "bob_lob", "short") else f"{prefix} bob"
if style_key == "lob":
return f"shoulder-length {color} lob" if length_key in ("random", "bob_lob") else f"{prefix} lob"
if style_key == "shag":
return f"{prefix or color} shag"
if style_key == "ponytail":
return f"{prefix or color} ponytail"
if style_key == "braid":
return f"{prefix or color} braid"
if style_key == "braids":
return f"{prefix or color} braids"
if style_key == "bun":
return f"{prefix} hair in a bun" if length else f"{color} bun"
if style_key == "messy_bun":
return f"{prefix} hair in a messy bun" if length else f"messy {color} bun"
if style_key == "locs":
return f"{prefix or color} locs"
if style_key == "twists":
return f"{prefix or color} twists"
if style_key == "afro":
return f"{color} afro"
if style_key == "natural_curls":
return f"{prefix or color} natural curls"
if style_key == "wet_hair":
return f"{prefix or color} wet hair"
if style_key == "slicked_back":
return f"slicked-back {color} hair"
if style_key == "straight":
return f"{prefix or color} straight hair"
if style_key == "loose_waves":
return f"{prefix or color} loose waves"
if style_key == "tight_curls":
return f"{prefix or color} tight curls"
if style_key == "curls":
return f"{prefix or color} curls"
return f"{prefix or color} waves"
def _hair_descriptor_from_slot(base_hair: Any, slot: dict[str, Any], rng: random.Random) -> str:
hair_config = _parse_hair_config(slot.get("hair_config"))
color_choice = _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES)
length_choice = _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES)
style_choice = _normalize_hair_choice(slot.get("hair_style"), CHARACTER_HAIR_STYLE_CHOICES)
color_options = hair_config.get("colors") or []
length_options = hair_config.get("lengths") or []
style_options = hair_config.get("styles") or []
if (
color_choice == "random"
and length_choice == "random"
and style_choice == "random"
and not color_options
and not length_options
and not style_options
):
return ""
if color_choice != "random":
color_key = color_choice
elif color_options:
color_key = g.choose(rng, color_options)
else:
color_key = _infer_hair_color_key(base_hair)
if length_choice != "random":
length_key = length_choice
elif length_options:
length_key = g.choose(rng, length_options)
else:
length_key = _infer_hair_length_key(base_hair)
if style_choice != "random":
style_key = style_choice
elif style_options:
style_key = g.choose(rng, style_options)
else:
style_key = _infer_hair_style_key(base_hair)
if color_key == "random":
color_key = _choose_hair_key(rng, CHARACTER_HAIR_COLOR_CHOICES)
if length_key == "random":
length_key = _choose_hair_key(rng, CHARACTER_HAIR_LENGTH_CHOICES)
if style_key == "random":
style_key = _choose_hair_key(rng, CHARACTER_HAIR_STYLE_CHOICES)
if length_key == "updo" and style_key not in ("ponytail", "braid", "braids", "bun", "messy_bun", "locs", "twists"):
style_key = g.choose(rng, ["ponytail", "braid", "bun", "messy_bun"])
return _hair_phrase_from_parts(color_key, length_key, style_key)
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
if subject_type not in ("woman", "man"):
subject_type = "woman"
label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
if label == "AUTO_CHAIN":
label = "auto_chain"
if label not in CHARACTER_LABEL_CHOICES:
label = "auto_chain"
manual_config = _parse_character_manual_config(slot.get("manual") or slot.get("manual_config"))
raw_age = str(slot.get("age") or "random")
raw_manual_age = str(slot.get("manual_age") or "").strip()
if not raw_manual_age and manual_config.get("manual_age"):
raw_manual_age = manual_config["manual_age"]
if raw_age.lower() in CHARACTER_RANDOM_TOKENS:
raw_age = "manual"
age = _slot_manual_or_choice(raw_age, raw_manual_age)
raw_body = str(slot.get("body") or "random")
raw_manual_body = str(slot.get("manual_body") or "").strip()
if not raw_manual_body and manual_config.get("manual_body"):
raw_manual_body = manual_config["manual_body"]
if raw_body.lower() in CHARACTER_RANDOM_TOKENS:
raw_body = "manual"
body = _slot_manual_or_choice(raw_body, raw_manual_body)
figure = str(slot.get("figure") or "random").strip()
if figure not in character_figure_choices():
figure = "random"
def manual_fallback(field: str) -> str:
direct = _slot_value(slot.get(field))
return direct or manual_config.get(field, "")
normalized = {
"profile_type": "character_slot",
"subject_type": subject_type,
"label": label,
"slot_seed": _normalize_slot_seed(slot.get("slot_seed")),
"age": age,
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
"figure": figure,
"body": body,
"body_phrase": manual_fallback("body_phrase"),
"skin": manual_fallback("skin"),
"hair": manual_fallback("hair"),
"manual": manual_config,
"characteristics": (
slot.get("characteristics")
if isinstance(slot.get("characteristics"), dict)
else _slot_value(slot.get("characteristics") or slot.get("characteristics_config"))
),
"hair_config": (
slot.get("hair_config")
if isinstance(slot.get("hair_config"), dict)
else _slot_value(slot.get("hair_config"))
),
"hair_color": _normalize_hair_choice(slot.get("hair_color"), CHARACTER_HAIR_COLOR_CHOICES),
"hair_length": _normalize_hair_choice(slot.get("hair_length"), CHARACTER_HAIR_LENGTH_CHOICES),
"hair_style": _normalize_hair_choice(slot.get("hair_style"), CHARACTER_HAIR_STYLE_CHOICES),
"eyes": manual_fallback("eyes"),
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
"presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type),
"softcore_outfit": manual_fallback("softcore_outfit"),
"hardcore_clothing": (
_slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit"))
or manual_config.get("hardcore_clothing", "")
),
"expression_enabled": not _is_false(slot.get("expression_enabled", True)),
"expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")),
"softcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
"hardcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")),
}
normalized["summary"] = _character_slot_summary(normalized)
return normalized
def _parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
if not character_cast:
return []
if isinstance(character_cast, list):
raw = character_cast
elif isinstance(character_cast, dict):
raw = character_cast
else:
try:
raw = json.loads(str(character_cast))
except json.JSONDecodeError as exc:
raise ValueError(f"Invalid character_cast JSON: {exc}") from exc
if isinstance(raw, list):
slots = raw
elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
slots = raw["slots"]
elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
slots = [raw]
elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
slots = [raw]
else:
return []
return [_normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]
def _character_slot_summary(slot: dict[str, Any]) -> str:
subject = str(slot.get("subject_type") or "woman")
label = str(slot.get("label") or "auto_chain")
label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
parts = [
subject,
label_text,
f"seed={slot.get('slot_seed')}" if _slot_seed(slot) >= 0 else "",
f"age={slot.get('age', 'random')}",
f"ethnicity={slot.get('ethnicity', 'random')}",
f"figure={slot.get('figure', 'random')}",
f"body={slot.get('body', 'random')}",
f"detail={slot.get('descriptor_detail', 'auto')}",
]
parts = [part for part in parts if part]
if _slot_is_pov(slot):
parts.append("presence=pov")
if not _slot_expression_enabled(slot):
parts.append("expression=disabled")
else:
expression_intensity = _slot_expression_intensity(slot)
if expression_intensity is not None:
parts.append(f"expression={expression_intensity:.2f}")
softcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "softcore")
hardcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "hardcore")
if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity:
parts.append(f"soft_expr={softcore_expression_intensity:.2f}")
if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity:
parts.append(f"hard_expr={hardcore_expression_intensity:.2f}")
if slot.get("softcore_outfit"):
parts.append(f"soft_outfit={slot['softcore_outfit']}")
if slot.get("hardcore_clothing"):
parts.append(f"hard_clothing={slot['hardcore_clothing']}")
characteristics = _parse_characteristics_config(slot.get("characteristics"))
characteristics_summary = _characteristics_summary(characteristics)
if characteristics_summary != "characteristics unrestricted":
parts.append(f"characteristics={characteristics_summary}")
hair_config = _parse_hair_config(slot.get("hair_config"))
hair_config_summary = _hair_config_summary(hair_config)
if hair_config_summary != "hair unrestricted":
parts.append(f"hair={hair_config_summary}")
for key in ("hair_color", "hair_length", "hair_style"):
value = slot.get(key)
if value and value != "random":
parts.append(f"{key}={value}")
for key in ("body_phrase", "skin", "hair", "eyes"):
value = slot.get(key)
if value:
parts.append(f"{key}={value}")
return "; ".join(parts)
def build_character_slot_json(
subject_type: str = "woman",
label: str = "auto_chain",
slot_seed: int = -1,
age: str = "random",
manual_age: str = "",
manual: str | dict[str, Any] | None = "",
ethnicity: str = "random",
figure: str = "random",
body: str = "random",
manual_body: str = "",
body_phrase: str = "",
skin: str = "",
hair: str = "",
characteristics: str | dict[str, Any] | None = "",
hair_config: str | dict[str, Any] | None = "",
hair_color: str = "random",
hair_length: str = "random",
hair_style: str = "random",
eyes: str = "",
descriptor_detail: str = "auto",
expression_enabled: bool = True,
expression_intensity: float = -1.0,
enabled: bool = True,
character_cast: str | dict[str, Any] | list[Any] | None = "",
presence_mode: str = "visible",
softcore_expression_intensity: float = -1.0,
hardcore_expression_intensity: float = -1.0,
softcore_outfit: str = "",
hardcore_clothing: str = "",
) -> dict[str, str]:
existing_slots = _parse_character_cast(character_cast)
slot = _normalize_character_slot(
{
"subject_type": subject_type,
"label": label,
"slot_seed": slot_seed,
"age": age,
"manual_age": manual_age,
"manual": manual,
"ethnicity": ethnicity,
"figure": figure,
"body": body,
"manual_body": manual_body,
"body_phrase": body_phrase,
"skin": skin,
"hair": hair,
"characteristics": characteristics,
"hair_config": hair_config,
"hair_color": hair_color,
"hair_length": hair_length,
"hair_style": hair_style,
"eyes": eyes,
"descriptor_detail": descriptor_detail,
"presence_mode": presence_mode,
"softcore_outfit": softcore_outfit,
"hardcore_clothing": hardcore_clothing,
"expression_enabled": expression_enabled,
"expression_intensity": expression_intensity,
"softcore_expression_intensity": softcore_expression_intensity,
"hardcore_expression_intensity": hardcore_expression_intensity,
}
)
slots = existing_slots + ([slot] if enabled else [])
cast = {
"profile_type": "character_cast",
"version": 1,
"slots": slots,
}
return {
"character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
"character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
"summary": slot["summary"] if enabled else "disabled",
"status": f"{len(slots)} slot(s)",
}
def _slot_explicit_label(slot: dict[str, Any]) -> str:
label = str(slot.get("label") or "").strip().upper()
if label in CHARACTER_LABEL_CHOICES and label != "AUTO_CHAIN":
return label
return ""
def _character_slot_label_map(slots: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
label_map: dict[str, dict[str, Any]] = {}
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
for subject_type, prefix in (("woman", "Woman"), ("man", "Man")):
subject_slots = [slot for slot in slots if slot.get("subject_type") == subject_type]
auto_slots = [slot for slot in subject_slots if not _slot_explicit_label(slot)]
for index, slot in enumerate(reversed(auto_slots)):
if index >= len(letters):
break
label_map[f"{prefix} {letters[index]}"] = slot
for slot in subject_slots:
explicit = _slot_explicit_label(slot)
if explicit:
label_map[f"{prefix} {explicit}"] = slot
return label_map
def _pov_character_labels(
label_map: dict[str, dict[str, Any]],
men_count: int | None = None,
) -> list[str]:
if men_count is None:
labels = sorted(label for label in label_map if label.startswith("Man "))
else:
labels = [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]
return [label for label in labels if _slot_is_pov(label_map.get(label))]
def _pov_text_with_viewer(text: Any, pov_labels: list[str]) -> str:
rendered = str(text or "").strip()
if not rendered or not pov_labels:
return rendered
for label in sorted(pov_labels, key=len, reverse=True):
escaped = re.escape(label)
rendered = re.sub(rf"\b{escaped}'s\b", "the POV viewer's", rendered)
rendered = re.sub(rf"\b{escaped}\b", "the POV viewer", rendered)
rendered = re.sub(r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
return _clean_prompt_punctuation(rendered)
def _pov_role_graph_prompt(role_graph: Any, pov_labels: list[str]) -> str:
role_graph_text = str(role_graph or "").strip()
if not role_graph_text or not pov_labels:
return role_graph_text
viewer_text = _pov_text_with_viewer(role_graph_text, pov_labels)
label_text = ", ".join(pov_labels)
return f"First-person POV from {label_text}; {viewer_text}"
def _pov_prompt_directive(pov_labels: list[str]) -> str:
if not pov_labels:
return ""
label_text = ", ".join(pov_labels)
return (
f"POV participant: {label_text} is the first-person camera viewpoint; "
"he remains the off-camera viewpoint, represented by foreground hands, body position, or camera perspective cues when needed."
)
def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str:
text = str(composition or "").strip()
if not text or not pov_labels:
return text
text = re.sub(r"\ball participants visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball adult bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
text = re.sub(r"\ball three bodies readable\b", "visible partner bodies readable", text, flags=re.IGNORECASE)
text = re.sub(r"\bwide group-sex composition\b", "first-person group-sex POV composition", text, flags=re.IGNORECASE)
if "pov" not in text.lower() and "first-person" not in text.lower():
text = f"{text}, adapted for first-person POV with the POV participant kept off-camera"
return _clean_prompt_punctuation(text)
def _body_exposure_scene_text(scene: Any) -> str:
text = str(scene or "").strip()
if not text:
return ""
replacements = (
(r",?\s*\bscattered (?:clothes|clothing)\b", ""),
(r",?\s*\bfloor clothes\b", ""),
(r"\bclothes scattered\b", "soft floor shadows"),
(r",?\s*\bscattered lingerie\b", ""),
(r",?\s*\blingerie visible nearby\b", ""),
(r"\boutfit racks\b", "mirror shelves"),
(r"\bcostume racks\b", "mirror shelves"),
(r"\bhanging outfits\b", "hanging fabric"),
(r"\bclothing hooks\b", "wall hooks"),
(r"\boutfit-check\b", "creator-shot"),
(r"\boutfit framing\b", "body framing"),
(r"\bfull outfits\b", "full bodies"),
(r"\bcoordinated outfits\b", "coordinated posing"),
)
for pattern, replacement in replacements:
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
text = re.sub(r"\bwith,\s*", "with ", text, flags=re.IGNORECASE)
text = re.sub(r",\s*,", ",", text)
return _clean_prompt_punctuation(text)
def _slot_softcore_outfit(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
if not slot:
return ""
outfit = _slot_value(slot.get("softcore_outfit"))
if outfit:
return outfit
if rng is None:
return ""
return _characteristic_choice(_parse_characteristics_config(slot.get("characteristics")), "softcore_outfits", rng)
def _slot_hardcore_clothing(slot: dict[str, Any] | None, rng: random.Random | None = None) -> str:
if not slot:
return ""
clothing = _slot_value(slot.get("hardcore_clothing"))
if clothing:
return clothing
if rng is None:
return ""
return _characteristic_choice(_parse_characteristics_config(slot.get("characteristics")), "hardcore_clothing", rng)
def _softcore_outfit_sentence(label: str, outfit: str) -> str:
outfit = str(outfit or "").strip()
if not outfit:
return ""
lower = outfit.lower()
if lower.startswith(("wears ", "wearing ", "in ")):
return f"{label} {outfit}"
return f"{label} wears {outfit}"
def _hardcore_clothing_sentence(label: str, clothing: str) -> str:
clothing = str(clothing or "").strip().rstrip(".")
if not clothing:
return ""
lower = clothing.lower()
if lower.startswith(("fully nude", "nude")):
return f"{label}'s body is fully exposed, bare skin unobstructed"
if lower.startswith("partly nude"):
return f"{label}'s body is partly exposed"
if lower.startswith(("is ", "wears ", "wearing ", "keeps ", "has ", "with ")):
return f"{label} {clothing}"
return f"{label}'s clothing: {clothing}"
def _character_hardcore_clothing_entries(
label_map: dict[str, dict[str, Any]],
women_count: int,
men_count: int,
pov_labels: list[str] | None = None,
rng: random.Random | None = None,
) -> list[str]:
pov_set = set(pov_labels or [])
labels = [
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
]
entries: list[str] = []
for label in labels:
if label in pov_set:
continue
clothing = _slot_hardcore_clothing(label_map.get(label), rng)
sentence = _hardcore_clothing_sentence(label, clothing)
if sentence:
entries.append(sentence)
return entries
def _context_from_character_slot(
rng: random.Random,
slot: dict[str, Any],
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> dict[str, str]:
slot_ethnicity = _slot_value(slot.get("ethnicity"))
slot_body = _slot_value(slot.get("body"))
effective_ethnicity = slot_ethnicity or ethnicity
effective_figure = _slot_effective_figure(slot, subject_type, figure)
effective_no_plus = bool(no_plus_women) and not slot_body
effective_no_black = bool(no_black) and not slot_ethnicity
appearance_rng = _slot_context_rng(slot, rng)
context = _appearance_for_subject(
appearance_rng,
subject_type,
effective_ethnicity,
effective_figure,
effective_no_plus,
effective_no_black,
)
characteristics = _parse_characteristics_config(slot.get("characteristics"))
age = _slot_value(slot.get("age")) or _characteristic_choice(characteristics, "ages", appearance_rng)
body_phrase = _slot_value(slot.get("body_phrase"))
if not slot_body:
slot_body = _characteristic_choice(characteristics, "bodies", appearance_rng)
if age:
context["age"] = age
if slot_body:
context["body"] = slot_body
if subject_type == "woman":
context["body_phrase"] = _body_phrase(slot_body, context.get("figure", ""))
else:
context["body_phrase"] = f"{slot_body} figure"
if body_phrase:
context["body_phrase"] = body_phrase
skin_value = _slot_value(slot.get("skin"))
if skin_value:
context["skin"] = skin_value
eyes_value = _slot_value(slot.get("eyes"))
if not eyes_value:
eyes_value = _eye_phrase_from_key(_characteristic_choice(characteristics, "eyes", appearance_rng))
if eyes_value:
context["eyes"] = eyes_value
hair_value = _slot_value(slot.get("hair"))
if hair_value:
context["hair"] = hair_value
else:
hair_descriptor = _hair_descriptor_from_slot(context.get("hair"), slot, appearance_rng)
if hair_descriptor:
context["hair"] = hair_descriptor
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
context["presence_mode"] = _normalize_presence_mode(slot.get("presence_mode"), subject_type)
context["expression_enabled"] = _slot_expression_enabled(slot)
expression_intensity = _slot_expression_intensity(slot)
if expression_intensity is not None:
context["expression_intensity"] = expression_intensity
context["subject_type"] = subject_type
context["subject"] = subject_type
context["subject_phrase"] = subject_type
return context
def _character_context_for_label(
label: str,
label_map: dict[str, dict[str, Any]],
rng: random.Random,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> tuple[dict[str, str], dict[str, Any] | None]:
subject_type = "man" if label.startswith("Man ") else "woman"
slot = label_map.get(label)
if slot:
return _context_from_character_slot(rng, slot, subject_type, ethnicity, figure, no_plus_women, no_black), slot
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black), None
def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
for key in (
"subject_type",
"subject",
"subject_phrase",
"age",
"body",
"body_phrase",
"skin",
"hair",
"eyes",
"figure",
"descriptor_detail",
"presence_mode",
"expression_enabled",
"expression_intensity",
):
value = context.get(key)
if value is not None and value != "":
row[key] = value
if context.get("age"):
row["age_band"] = context["age"]
return row
def _cast_descriptor_entries(
seed_config: dict[str, int],
seed: int,
row_number: int,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
women_count: int,
men_count: int,
character_cast: str | dict[str, Any] | list[Any] | None = "",
primary_descriptor: str = "",
) -> tuple[list[str], list[dict[str, Any]]]:
slots = _parse_character_cast(character_cast)
label_map = _character_slot_label_map(slots)
rng = _axis_rng(seed_config, "person", seed, row_number + 997)
descriptors: list[str] = []
for index in range(max(0, women_count)):
label = f"Woman {chr(ord('A') + index)}"
if index == 0 and primary_descriptor:
descriptors.append(f"Woman A / primary creator: {primary_descriptor}")
continue
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
for index in range(max(0, men_count)):
label = f"Man {chr(ord('A') + index)}"
if _slot_is_pov(label_map.get(label)):
continue
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
return descriptors, slots
def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
row = _load_json_object(metadata_json, "metadata_json")
if isinstance(row.get("softcore_row"), dict):
return row["softcore_row"]
return row
def _row_from_character_slot(character_slot: str | dict[str, Any] | None) -> dict[str, Any]:
slots = _parse_character_cast(character_slot)
if not slots:
return {}
slot = slots[-1]
if _slot_seed(slot) >= 0:
subject_type = str(slot.get("subject_type") or "woman")
return _context_from_character_slot(
random.Random(_row_seed(_slot_seed(slot), 1, 719)),
slot,
subject_type,
"any",
"curvy",
False,
False,
)
return slot
def _character_profile_descriptor(profile: dict[str, Any]) -> str:
subject = str(profile.get("subject_type") or profile.get("subject") or "person").strip()
return _descriptor_from_parts(
subject,
profile.get("age"),
profile.get("body_phrase") or _body_phrase(profile.get("body"), profile.get("figure")),
profile.get("skin"),
profile.get("hair"),
profile.get("eyes"),
profile.get("descriptor_detail"),
)
def _normalize_character_profile(profile: dict[str, Any], profile_name: str = "") -> dict[str, Any]:
subject_type = str(profile.get("subject_type") or profile.get("primary_subject") or profile.get("subject") or "").strip()
if subject_type not in ("woman", "man"):
subject_type = "woman"
body = str(profile.get("body") or profile.get("body_type") or "").strip()
figure = str(profile.get("figure") or "").strip()
body_phrase = str(profile.get("body_phrase") or "").strip() or _body_phrase(body, figure)
normalized = {
"profile_type": "character",
"profile_name": _safe_profile_name(profile_name or str(profile.get("profile_name") or "")),
"subject_type": subject_type,
"subject": subject_type,
"subject_phrase": subject_type,
"age": str(profile.get("age") or profile.get("age_band") or "").strip(),
"body": body,
"body_phrase": body_phrase,
"skin": str(profile.get("skin") or "").strip(),
"hair": str(profile.get("hair") or "").strip(),
"eyes": str(profile.get("eyes") or "").strip(),
"figure": figure,
"descriptor_detail": _normalize_descriptor_detail(profile.get("descriptor_detail")),
}
normalized["descriptor"] = _character_profile_descriptor(normalized)
return normalized
def build_character_profile_json(
profile_name: str = "",
source: str = "metadata_json",
metadata_json: str | dict[str, Any] | None = "",
character_slot: str | dict[str, Any] | None = "",
subject_type: str = "woman",
age: str = "",
body: str = "",
body_phrase: str = "",
skin: str = "",
hair: str = "",
eyes: str = "",
figure: str = "",
save_now: bool = False,
) -> dict[str, str]:
if source == "character_slot":
row = _row_from_character_slot(character_slot or metadata_json)
raw_profile = {
"profile_name": profile_name,
"subject_type": row.get("subject_type") or subject_type,
"age": row.get("age") or age,
"body": row.get("body") or body,
"body_phrase": row.get("body_phrase") or body_phrase,
"skin": row.get("skin") or skin,
"hair": row.get("hair") or hair,
"eyes": row.get("eyes") or eyes,
"figure": row.get("figure") or figure,
"descriptor_detail": row.get("descriptor_detail") or "auto",
}
elif source == "metadata_json":
row = _row_from_profile_metadata(metadata_json)
raw_profile = {
"profile_name": profile_name,
"subject_type": row.get("subject_type") or row.get("primary_subject") or subject_type,
"age": row.get("age") or row.get("age_band") or age,
"body": row.get("body") or row.get("body_type") or body,
"body_phrase": row.get("body_phrase") or body_phrase,
"skin": row.get("skin") or skin,
"hair": row.get("hair") or hair,
"eyes": row.get("eyes") or eyes,
"figure": row.get("figure") or figure,
"descriptor_detail": row.get("descriptor_detail") or "auto",
}
else:
raw_profile = {
"profile_name": profile_name,
"subject_type": subject_type,
"age": age,
"body": body,
"body_phrase": body_phrase,
"skin": skin,
"hair": hair,
"eyes": eyes,
"figure": figure,
"descriptor_detail": "auto",
}
profile = _normalize_character_profile(raw_profile, profile_name)
saved_path = ""
status = "not_saved"
if save_now:
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
path = _profile_path(profile["profile_name"])
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
saved_path = str(path)
status = "saved"
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": saved_path,
"status": status,
}
def save_character_profile_payload(profile_name: str = "", profile_json: str | dict[str, Any] | None = "") -> dict[str, str]:
raw_profile = _load_json_object(profile_json, "profile_json")
if not raw_profile:
raise ValueError("No cached character profile is available to save.")
profile = _normalize_character_profile(raw_profile, profile_name or str(raw_profile.get("profile_name") or ""))
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
path = _profile_path(profile["profile_name"])
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": str(path),
"status": "saved",
}
def _empty_profile_result(status: str = "empty") -> dict[str, str]:
return {
"profile_json": "",
"profile_name": "",
"descriptor": "",
"saved_path": "",
"status": status,
}
def _apply_character_profile_overrides(
profile: dict[str, Any],
override_subject_type: str = "",
override_age: str = "",
override_body: str = "",
override_body_phrase: str = "",
override_skin: str = "",
override_hair: str = "",
override_eyes: str = "",
override_figure: str = "",
override_descriptor_detail: str = "",
) -> dict[str, Any]:
updated = dict(profile)
subject_type = str(override_subject_type or "").strip()
if subject_type in ("woman", "man"):
updated["subject_type"] = subject_type
updated["subject"] = subject_type
updated["subject_phrase"] = subject_type
for key, value in (
("age", override_age),
("body", override_body),
("body_phrase", override_body_phrase),
("skin", override_skin),
("hair", override_hair),
("eyes", override_eyes),
("figure", override_figure),
):
text = str(value or "").strip()
if text:
updated[key] = text
descriptor_detail = str(override_descriptor_detail or "").strip()
if descriptor_detail and descriptor_detail != "keep_profile":
updated["descriptor_detail"] = _normalize_descriptor_detail(descriptor_detail)
if not str(updated.get("body_phrase") or "").strip():
updated["body_phrase"] = _body_phrase(updated.get("body"), updated.get("figure"))
updated["descriptor"] = _character_profile_descriptor(updated)
return updated
def load_character_profile_json(
profile_name: str = "",
fallback_profile_json: str | dict[str, Any] | None = "",
enabled: bool = True,
delete_now: bool = False,
rename_now: bool = False,
rename_to: str = "",
override_subject_type: str = "",
override_age: str = "",
override_body: str = "",
override_body_phrase: str = "",
override_skin: str = "",
override_hair: str = "",
override_eyes: str = "",
override_figure: str = "",
override_descriptor_detail: str = "",
) -> dict[str, str]:
if not enabled:
return _empty_profile_result("disabled")
if delete_now and rename_now:
return _empty_profile_result("choose_delete_or_rename")
raw_profile = _load_json_object(fallback_profile_json, "fallback_profile_json")
saved_path = ""
if profile_name and profile_name != "manual":
path = _profile_path(profile_name)
if delete_now:
if path.exists():
path.unlink()
return _empty_profile_result(f"deleted:{path.stem}")
return _empty_profile_result(f"delete_missing:{_safe_profile_name(profile_name)}")
if rename_now:
new_name = _safe_profile_name(rename_to)
if not rename_to.strip():
return _empty_profile_result("rename_missing_name")
if not path.exists():
return _empty_profile_result(f"rename_missing:{_safe_profile_name(profile_name)}")
target = _profile_path(new_name)
if target.exists() and target != path:
return _empty_profile_result(f"rename_target_exists:{target.stem}")
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
profile = _normalize_character_profile(raw_profile, new_name)
target.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
if target != path:
path.unlink()
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": str(target),
"status": f"renamed:{path.stem}->{target.stem}",
}
if path.exists():
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
saved_path = str(path)
if not raw_profile:
return _empty_profile_result("empty")
profile = _normalize_character_profile(raw_profile, profile_name or raw_profile.get("profile_name", ""))
profile = _apply_character_profile_overrides(
profile,
override_subject_type=override_subject_type,
override_age=override_age,
override_body=override_body,
override_body_phrase=override_body_phrase,
override_skin=override_skin,
override_hair=override_hair,
override_eyes=override_eyes,
override_figure=override_figure,
override_descriptor_detail=override_descriptor_detail,
)
return {
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
"profile_name": profile["profile_name"],
"descriptor": profile["descriptor"],
"saved_path": saved_path,
"status": "loaded" if saved_path else "fallback",
}
def _parse_character_profile(character_profile: str | dict[str, Any] | None) -> dict[str, Any]:
raw = _load_json_object(character_profile, "character_profile")
if not raw:
return {}
if raw.get("profile_type") == "character" or any(key in raw for key in ("age", "age_band", "skin", "hair", "eyes")):
return _normalize_character_profile(raw, str(raw.get("profile_name") or ""))
return {}
def _apply_character_profile_to_context(
context: dict[str, Any],
character_profile: str | dict[str, Any] | None,
) -> tuple[dict[str, Any], dict[str, Any], str]:
profile = _parse_character_profile(character_profile)
if not profile:
return context, {}, "none"
if context.get("subject_type") not in ("woman", "man"):
return context, profile, "skipped_non_single_subject"
if profile["subject_type"] != context.get("subject_type"):
return context, profile, "skipped_subject_mismatch"
updated = dict(context)
for key in (
"subject_type",
"subject",
"subject_phrase",
"age",
"body",
"body_phrase",
"skin",
"hair",
"eyes",
"figure",
"descriptor_detail",
):
value = profile.get(key)
if value:
updated[key] = value
updated["subject"] = profile["subject_type"]
updated["subject_phrase"] = profile["subject_type"]
return updated, profile, "applied"
def _composition_prompt(composition: str) -> str:
composition = str(composition or "").strip()
if not composition:
return composition
lower = composition.lower()
if lower.startswith("vertical ") or " vertical " in lower or lower.endswith(" vertical"):
return composition
return f"vertical {composition}"
def _appearance_for_subject(
rng: random.Random,
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
) -> dict[str, str]:
if subject_type == "single_any":
subject_type = "woman" if rng.random() < 0.82 else "man"
if subject_type == "man":
men_ethnicity = ethnicity if ethnicity else "any"
subject, age, body, skin, hair, eyes = g.choose(rng, g.by_ethnicity(g.MEN, men_ethnicity))
return {
"subject_type": "man",
"subject": subject,
"subject_phrase": subject,
"age": age,
"body": body,
"skin": skin,
"hair": hair,
"eyes": eyes,
"body_phrase": f"{body} figure",
}
subject, age, body, skin, hair, eyes = g.choose_woman(rng, ethnicity, no_plus_women, no_black)
figure_note = g.choose(rng, g.figure_pool(figure))
return {
"subject_type": "woman",
"subject": subject,
"subject_phrase": subject,
"age": age,
"body": body,
"skin": skin,
"hair": hair,
"eyes": eyes,
"body_phrase": _body_phrase(body, figure_note),
"figure": figure_note,
}
def _count_phrase(count: int, singular: str, plural: str) -> str:
words = {
0: "no",
1: "one",
2: "two",
3: "three",
4: "four",
5: "five",
6: "six",
7: "seven",
8: "eight",
9: "nine",
10: "ten",
11: "eleven",
12: "twelve",
}
label = singular if count == 1 else plural
return f"{words.get(count, str(count))} {label}"
def _configured_cast_context(women_count: int, men_count: int) -> dict[str, str]:
women_count = max(0, int(women_count))
men_count = max(0, int(men_count))
if women_count + men_count == 0:
women_count = 1
parts = []
if women_count:
parts.append(_count_phrase(women_count, "adult woman", "adult women"))
if men_count:
parts.append(_count_phrase(men_count, "adult man", "adult men"))
if len(parts) == 1:
subject_phrase = parts[0]
else:
subject_phrase = f"{parts[0]} and {parts[1]}"
person_count = women_count + men_count
if person_count == 1:
scene_kind = "solo adult sexual pose"
elif person_count == 2:
scene_kind = "adult couple sex scene"
elif person_count == 3:
scene_kind = "adult threesome sex scene"
else:
scene_kind = "adult group sex scene"
women_label = "woman" if women_count == 1 else "women"
men_label = "man" if men_count == 1 else "men"
cast_summary = f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults"
return {
"subject_type": "configured_cast",
"subject": f"{women_count}w_{men_count}m_sex_scene",
"subject_phrase": subject_phrase,
"age": "21+ adults",
"body": "varied",
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "varied adult bodies",
"women_count": str(women_count),
"men_count": str(men_count),
"person_count": str(person_count),
"cast_summary": cast_summary,
"scene_kind": scene_kind,
}
def _couple_type_from_counts(
rng: random.Random,
women_count: int,
men_count: int,
) -> tuple[str, str, str, int, int]:
women_count = max(0, int(women_count))
men_count = max(0, int(men_count))
if women_count >= 2 and men_count == 0:
return "two women", "two women", "close affectionate couple pose", 2, 0
if men_count >= 2 and women_count == 0:
return "two men", "two men", "relaxed romantic couple pose", 0, 2
if women_count >= 1 and men_count >= 1:
return "woman and man", "a woman and a man", "playful date-night pose", 1, 1
primary_subject, subject_phrase, pose = g.choose(rng, g.COUPLE_TYPES)
if primary_subject == "two women":
return primary_subject, subject_phrase, pose, 2, 0
if primary_subject == "two men":
return primary_subject, subject_phrase, pose, 0, 2
return primary_subject, subject_phrase, pose, 1, 1
def _subject_context(
rng: random.Random,
subject_type: str,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
women_count: int = 1,
men_count: int = 1,
) -> dict[str, str]:
if subject_type in ("woman", "man", "single_any"):
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black)
if subject_type == "configured_cast":
return _configured_cast_context(women_count, men_count)
if subject_type == "couple":
primary_subject, subject_phrase, pose, effective_women_count, effective_men_count = _couple_type_from_counts(
rng,
women_count,
men_count,
)
return {
"subject_type": "couple",
"subject": primary_subject,
"subject_phrase": subject_phrase,
"age": g.choose(rng, g.COUPLE_AGES),
"body": g.choose(rng, ["slim and average", "curvy and broad", "stocky and curvy", "average and athletic"]),
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "",
"fallback_pose": pose,
"women_count": str(effective_women_count),
"men_count": str(effective_men_count),
"person_count": "2",
}
if subject_type == "group":
eth = "Asian " if ethnicity == "asian" else ""
return {
"subject_type": "group",
"subject": f"mixed {eth}adult group",
"subject_phrase": f"A mixed {eth}adult group of women and men",
"age": g.choose(rng, g.GROUP_AGES),
"body": "diverse",
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "diverse adult body types",
}
return {
"subject_type": subject_type,
"subject": "layout scene",
"subject_phrase": "Adult layout scene",
"age": "adult",
"body": "varied",
"skin": "",
"hair": "",
"eyes": "",
"body_phrase": "varied adult figures",
}
def _scene_pool(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
subject_type: str,
location_config: dict[str, Any] | None = None,
) -> list[Any]:
location_config = location_config or {}
location_entries = _list_from(location_config.get("scene_entries"))
if _location_config_active(location_config) and location_config.get("apply_mode") == "replace":
return location_entries
fallback = g.GROUP_SCENES if subject_type in ("group", "configured_cast") else g.SCENES
scene_entries: list[Any] = []
scene_pools = load_scene_pool_library()
item_source = item if isinstance(item, dict) else None
if item_source is not None and _is_false(item_source.get("inherit_scenes")):
sources = (item_source,)
elif _is_false(subcategory.get("inherit_scenes")):
sources = (subcategory, item_source)
else:
sources = (category, subcategory, item_source)
for source in sources:
if not isinstance(source, dict):
continue
if "scenes" in source:
_unique_extend(scene_entries, _list_from(source["scenes"]))
refs = _list_from(source.get("scene_pool")) + _list_from(source.get("scene_pools"))
for ref in refs:
ref_name = str(ref).strip()
if ref_name not in scene_pools:
raise ValueError(f"Unknown scene pool '{ref_name}'")
_unique_extend(scene_entries, scene_pools[ref_name])
if _location_config_active(location_config) and location_config.get("apply_mode") == "add":
_unique_extend(scene_entries, location_entries)
return scene_entries or fallback
def _legacy_scene_entries_for_row(row: dict[str, Any]) -> list[Any]:
subject = str(row.get("primary_subject") or "").lower()
if "group" in subject or "layout" in subject:
return list(g.GROUP_SCENES)
return list(g.SCENES)
def _legacy_scene_text_for_slug(slug: str) -> str:
for entry in list(g.SCENES) + list(g.GROUP_SCENES):
entry_slug, entry_text = _pair_from(entry)
if entry_slug == slug:
return entry_text
return ""
def _apply_location_config_to_legacy_row(
row: dict[str, Any],
location_config: dict[str, Any],
seed_config: dict[str, int],
seed: int,
row_number: int,
) -> dict[str, Any]:
if not _location_config_active(location_config):
return row
location_entries = _list_from(location_config.get("scene_entries"))
if location_config.get("apply_mode") == "add":
choices = _legacy_scene_entries_for_row(row)
_unique_extend(choices, location_entries)
else:
choices = location_entries
scene_rng = _axis_rng(seed_config, "scene", seed, row_number)
scene_slug, scene_text = _choose_pair(scene_rng, choices)
old_slug = str(row.get("scene") or "")
old_text = _legacy_scene_text_for_slug(old_slug)
row["source_scene"] = old_slug
row["source_scene_text"] = old_text
row["scene"] = scene_slug
row["scene_text"] = scene_text
row["location_config"] = location_config
if old_text:
row["prompt"] = str(row.get("prompt") or "").replace(f"Scene: {old_text}.", f"Scene: {scene_text}.")
row["caption"] = str(row.get("caption") or "").replace(f", {old_text},", f", {scene_text},")
else:
row["prompt"] = re.sub(
r"Scene:\s*.*?\.\s*Pose:",
f"Scene: {scene_text}. Pose:",
str(row.get("prompt") or ""),
count=1,
)
return row
def _legacy_composition_entries_for_row(row: dict[str, Any]) -> list[Any]:
subject = str(row.get("primary_subject") or "").lower()
if "group" in subject or "layout" in subject:
return list(g.GROUP_COMPOSITIONS)
return list(g.COMPOSITIONS)
def _apply_composition_config_to_legacy_row(
row: dict[str, Any],
composition_config: dict[str, Any],
seed_config: dict[str, int],
seed: int,
row_number: int,
) -> dict[str, Any]:
if not _composition_config_active(composition_config):
return row
composition_entries = _list_from(composition_config.get("composition_entries"))
if composition_config.get("apply_mode") == "add":
choices = _legacy_composition_entries_for_row(row)
_unique_extend(choices, composition_entries)
else:
choices = composition_entries
composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
new_composition = _choose_text(composition_rng, choices)
old_composition = str(row.get("composition") or "")
old_prompt_fragment = f"Composition: vertical {old_composition}."
new_prompt_fragment = f"Composition: {_composition_prompt(new_composition)}."
row["source_composition"] = old_composition
row["composition"] = new_composition
row["composition_prompt"] = _composition_prompt(new_composition)
row["composition_config"] = composition_config
if old_composition:
row["prompt"] = str(row.get("prompt") or "").replace(old_prompt_fragment, new_prompt_fragment)
row["caption"] = str(row.get("caption") or "").replace(f", {old_composition},", f", {new_composition},")
else:
row["prompt"] = re.sub(
r"Composition:\s*.*?\.\s*Use",
f"{new_prompt_fragment} Use",
str(row.get("prompt") or ""),
count=1,
)
return row
def _expression_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> list[Any]:
return _configured_pool(
category,
subcategory,
item,
"expressions",
"expression_pools",
load_expression_pool_library(),
"inherit_expressions",
) or g.EXPRESSIONS
def _expression_intensity_hint(entry: Any) -> float:
if isinstance(entry, dict):
for key in ("expression_intensity", "intensity"):
if key in entry:
return _clamped_float(entry[key], 0.5)
text = _entry_text(entry).lower()
high_terms = (
"ahegao",
"orgasm",
"climax",
"drool",
"drooling",
"tongue out",
"eyes rolled",
"fucked-out",
"cum-smeared",
"saliva",
"gagging",
"slack jaw",
"jaw slack",
"slack-jawed",
"sex-drunk",
"overwhelmed",
"strained",
"messy",
"panting",
"trembling",
"shaking",
"wide open mouth",
"raw ",
"wild ",
"dazed",
"spent",
)
if any(term in text for term in high_terms):
return 0.9
medium_terms = (
"seductive",
"teasing",
"lustful",
"aroused",
"bedroom",
"dominant",
"predatory",
"control",
"stern",
"strict",
"smirk",
"parted lips",
"open-mouthed",
"heated",
"hungry",
"inviting",
"sensual",
"fetish",
"commanding",
"flushed",
"moan",
)
if any(term in text for term in medium_terms):
return 0.62
low_terms = (
"neutral",
"quiet",
"calm",
"reserved",
"relaxed",
"candid",
"closed-mouth",
"thoughtful",
"controlled",
"focused",
"steady",
"bitten-lip",
"braced",
"held breath",
"concentrated",
"aloof",
"bored",
"tired",
"unfocused",
"contented",
"fashion",
"soft",
"sleepy",
"fresh-faced",
)
if any(term in text for term in low_terms):
return 0.25
return 0.5
def _expression_entries_for_intensity(entries: list[Any], expression_intensity: float) -> list[Any]:
target = _clamped_float(expression_intensity, 0.5)
weighted: list[Any] = []
for entry in entries:
entry_intensity = _expression_intensity_hint(entry)
distance = abs(target - entry_intensity)
if distance <= 0.18:
intensity_weight = 4.0
elif distance <= 0.35:
intensity_weight = 1.4
elif distance <= 0.55:
intensity_weight = 0.35
else:
intensity_weight = 0.05
if isinstance(entry, dict):
adjusted = dict(entry)
try:
base_weight = float(adjusted.get("weight", 1.0))
except (TypeError, ValueError):
base_weight = 1.0
adjusted["weight"] = max(0.0, base_weight) * intensity_weight
weighted.append(adjusted)
else:
weighted.append({"text": _entry_text(entry), "weight": intensity_weight})
return weighted or entries
def _pose_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str, poses: str) -> list[Any]:
configured = _merged_field(category, subcategory, item, "poses")
if configured:
return _list_from(configured)
if subject_type == "couple":
return [entry[2] for entry in g.COUPLE_TYPES]
if subject_type in ("layout", "scene"):
return ["clean designed layout"]
return g.EVOCATIVE_ALL if poses == "evocative" else g.POSES
def _composition_pool(
category: dict[str, Any],
subcategory: dict[str, Any],
item: Any,
subject_type: str,
composition_config: dict[str, Any] | None = None,
) -> list[Any]:
composition_config = composition_config or {}
composition_entries = _list_from(composition_config.get("composition_entries"))
if _composition_config_active(composition_config) and composition_config.get("apply_mode") == "replace":
return composition_entries
configured = _configured_pool(
category,
subcategory,
item,
"compositions",
"composition_pools",
load_composition_pool_library(),
"inherit_compositions",
)
if _composition_config_active(composition_config) and composition_config.get("apply_mode") == "add":
configured = list(configured or [])
_unique_extend(configured, composition_entries)
if configured:
return configured
if subject_type in ("group", "configured_cast"):
return g.GROUP_COMPOSITIONS
if subject_type in ("layout", "scene"):
return ["designed illustration layout"]
return g.COMPOSITIONS
def _build_custom_row(
category_choice: str,
subcategory_choice: str,
row_number: int,
start_index: int,
ethnicity: str,
poses: str,
figure: str,
no_plus_women: bool,
no_black: bool,
women_count: int,
men_count: int,
seed: int,
seed_config: dict[str, int],
expression_enabled: bool,
expression_intensity: float,
expression_intensity_source: str = "input",
character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
categories = load_category_library()
category_rng = _axis_rng(seed_config, "category", seed, row_number)
subcategory_rng = _axis_rng(seed_config, "subcategory", seed, row_number)
person_rng = _axis_rng(seed_config, "person", seed, row_number)
scene_rng = _axis_rng(seed_config, "scene", seed, row_number)
pose_rng = _axis_rng(seed_config, "pose", seed, row_number)
role_rng = _axis_rng(seed_config, "role", seed, row_number)
expression_rng = _axis_rng(seed_config, "expression", seed, row_number)
composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
parsed_hardcore_position_config = _parse_hardcore_position_config(hardcore_position_config)
parsed_location_config = _parse_location_config(location_config)
parsed_composition_config = _parse_composition_config(composition_config)
requested_women_count = women_count
requested_men_count = men_count
categories = _filter_hardcore_categories_for_position(
categories,
parsed_hardcore_position_config,
women_count,
men_count,
)
category, subcategory, women_count, men_count = _find_subcategory(
categories,
category_choice,
subcategory_choice,
category_rng,
subcategory_rng,
women_count,
men_count,
)
count_adjustment = {}
if women_count != requested_women_count or men_count != requested_men_count:
count_adjustment = {
"requested_women_count": requested_women_count,
"requested_men_count": requested_men_count,
"effective_women_count": women_count,
"effective_men_count": men_count,
}
if _is_hardcore_sexual_category(category):
subcategory = _apply_hardcore_position_config_to_subcategory(subcategory, parsed_hardcore_position_config)
content_axis = "pose" if _is_pose_content_category(category, subcategory) else "content"
content_rng = _axis_rng(seed_config, content_axis, seed, row_number)
items = _list_from(subcategory.get("items", [subcategory["name"]]))
item = _weighted_choice(content_rng, items)
item_text, item_name, item_axis_values = _compose_item(content_rng, category, subcategory, item, women_count, men_count)
is_pose_category = _is_pose_content_category(category, subcategory)
if is_pose_category:
item_text = _sanitize_hardcore_environment_anchors(item_text)
item_axis_values = _sanitize_hardcore_axis_values(item_axis_values)
subject_type = str(_merged_field(category, subcategory, item, "subject_type", "single_any"))
context = _subject_context(person_rng, subject_type, ethnicity, figure, no_plus_women, no_black, women_count, men_count)
character_slots = _parse_character_cast(character_cast)
character_slot_map = _character_slot_label_map(character_slots)
applied_slot: dict[str, Any] = {}
slot_status = "none"
if context.get("subject_type") in ("woman", "man"):
slot_label = "Woman A" if context["subject_type"] == "woman" else "Man A"
if slot_label in character_slot_map:
context, applied_slot = _character_context_for_label(
slot_label,
character_slot_map,
person_rng,
ethnicity,
figure,
no_plus_women,
no_black,
)
slot_status = f"applied:{slot_label}"
applied_profile, profile_status = {}, "skipped_character_slot"
else:
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
else:
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
subject_type = context["subject_type"]
pov_character_labels = (
_pov_character_labels(character_slot_map, men_count)
if subject_type == "configured_cast"
else []
)
source_role_graph = build_hardcore_role_graph(role_rng, subcategory, context, item_axis_values, pov_character_labels)
if is_pose_category:
source_role_graph = _sanitize_hardcore_environment_anchors(source_role_graph)
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
cast_descriptors: list[str] = []
cast_descriptor_text = ""
expression_intensity_source = expression_intensity_source or "input"
expression_disabled = not bool(expression_enabled)
if expression_disabled:
expression_intensity_source = "disabled"
elif subject_type in ("woman", "man") and applied_slot:
slot_label = "Woman A" if subject_type == "woman" else "Man A"
if not _slot_expression_enabled(applied_slot):
expression_disabled = True
expression_intensity_source = f"character_slot:{slot_label}:disabled"
else:
slot_expression_intensity = _slot_expression_intensity_for_phase(applied_slot, expression_phase)
if slot_expression_intensity is not None:
expression_intensity = slot_expression_intensity
expression_intensity_source = f"character_slot:{slot_label}"
elif subject_type == "configured_cast" and character_slots:
expression_intensity, expression_intensity_source = _cast_expression_intensity_override(
expression_intensity,
character_slot_map,
women_count,
men_count,
expression_phase,
)
if expression_intensity is None:
expression_disabled = True
if subject_type == "configured_cast" and character_slots:
cast_descriptors, _descriptor_slots = _cast_descriptor_entries(
seed_config,
seed,
row_number,
ethnicity,
figure,
no_plus_women,
no_black,
women_count,
men_count,
character_slots,
)
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
scene_slug, scene = _choose_pair(
scene_rng,
_compatible_entries(
_scene_pool(category, subcategory, item, subject_type, parsed_location_config),
women_count,
men_count,
),
)
pose = str(_merged_field(category, subcategory, item, "pose", "") or context.get("fallback_pose") or _choose_text(
pose_rng, _compatible_entries(_pose_pool(category, subcategory, item, subject_type, poses), women_count, men_count)
))
if is_pose_category:
pose = _sanitize_hardcore_environment_anchors(pose)
expression_pool = _expression_pool(category, subcategory, item)
if expression_disabled:
expression = ""
else:
expression_entries = _compatible_entries(
_expression_entries_for_intensity(expression_pool, expression_intensity),
women_count,
men_count,
)
expression = _choose_text(expression_rng, expression_entries)
if subject_type in ("couple", "group") and ";" not in expression:
secondary_expression = _choose_distinct_text(expression_rng, expression_entries, expression)
if secondary_expression:
expression = f"{expression}; {secondary_expression}"
shared_expression = expression
character_expressions: list[str] = []
character_expression_text = ""
if not expression_disabled and subject_type == "configured_cast" and character_slots:
character_expressions = _character_expression_entries(
expression_rng,
expression_pool,
expression_intensity,
character_slot_map,
women_count,
men_count,
expression_phase,
)
character_expression_text = "; ".join(character_expressions)
character_expression_text = _sanitize_character_expression_text_for_action(
character_expression_text,
source_role_graph,
item,
item_axis_values,
)
character_expressions = [part.strip() for part in character_expression_text.split(";") if part.strip()]
if character_expression_text:
expression = character_expression_text
source_composition = _choose_text(
composition_rng,
_compatible_entries(
_composition_pool(category, subcategory, item, subject_type, parsed_composition_config),
women_count,
men_count,
),
)
if is_pose_category:
source_composition = _sanitize_hardcore_environment_anchors(source_composition)
composition = _pov_composition_prompt(source_composition, pov_character_labels)
position_family = ""
position_keys: list[str] = []
position_key = ""
action_family = ""
if is_pose_category:
position_family = _hardcore_source_position_family(subcategory, parsed_hardcore_position_config)
position_keys = _hardcore_position_keys(
item_text,
source_role_graph,
source_composition,
pose,
axis_values=item_axis_values,
)
position_key = position_keys[0] if position_keys else ""
action_family = source_hardcore_action_family(
position_family,
source_role_graph,
item_text,
source_composition,
item_axis_values,
)
negative_prompt = str(_merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT))
positive_suffix = str(_merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX))
style = str(
_merged_field(
category,
subcategory,
item,
"style",
"sexy but tasteful adult pin-up coloured-pencil comic illustration",
)
)
item_label = str(_merged_field(category, subcategory, item, "item_label", category["name"]))
context.update(
{
"trigger": g.TRIGGER,
"main_category": category["name"],
"subcategory": subcategory["name"],
"category": category["name"],
"item": item_text,
"item_name": item_name,
"item_label": item_label,
"style": style,
"scene": scene,
"scene_slug": scene_slug,
"pose": pose,
"expression": expression,
"shared_expression": shared_expression,
"character_expressions": character_expressions,
"character_expression_text": character_expression_text,
"expression_enabled": not expression_disabled,
"expression_disabled": expression_disabled,
"expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source,
"composition": composition,
"source_composition": source_composition,
"composition_prompt": _composition_prompt(composition),
"composition_config": parsed_composition_config if _composition_config_active(parsed_composition_config) else {},
"role_graph": role_graph,
"source_role_graph": source_role_graph,
"action_family": action_family,
"position_family": position_family,
"position_key": position_key,
"position_keys": position_keys,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
"cast_descriptors": cast_descriptor_text,
"positive_suffix": positive_suffix,
"negative_prompt": negative_prompt,
}
)
if isinstance(item, dict) and "prompt_template" in item:
template = str(item["prompt_template"])
else:
template = str(subcategory.get("prompt_template") or category.get("prompt_template") or "")
if not template:
if subject_type in ("woman", "man"):
template = SINGLE_TEMPLATE
elif subject_type == "couple":
template = COUPLE_TEMPLATE
elif subject_type == "group":
template = GROUP_TEMPLATE
else:
template = LAYOUT_TEMPLATE
caption_template = str(
(item.get("caption_template") if isinstance(item, dict) else None)
or subcategory.get("caption_template")
or category.get("caption_template")
or "{trigger}, {subject_phrase}, {age}, {item}, {scene}, {composition}, coloured pencil comic illustration"
)
prompt = _format(template, context)
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in template:
prompt = _insert_positive_directive(prompt, f"Characters: {cast_descriptor_text}.")
if subject_type == "configured_cast" and pov_character_labels:
prompt = _insert_positive_directive(prompt, _pov_prompt_directive(pov_character_labels))
caption = _format(caption_template, context)
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in caption_template:
caption = f"{caption.rstrip()}, {cast_descriptor_text}"
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
index = start_index + row_number - 1
row = g.row_base(index, batch, context["subject"], context["age"], context["body"], scene_slug, composition)
row.update(
{
"prompt": prompt,
"caption": caption,
"negative_prompt": negative_prompt,
"expression": expression,
"main_category": category["name"],
"subcategory": subcategory["name"],
"category_slug": category["slug"],
"subcategory_slug": subcategory["slug"],
"subject_type": subject_type,
"subject_phrase": context.get("subject_phrase", ""),
"body_phrase": context.get("body_phrase", ""),
"skin": context.get("skin", ""),
"hair": context.get("hair", ""),
"eyes": context.get("eyes", ""),
"style": style,
"item": item_text,
"item_label": item_label,
"positive_suffix": positive_suffix,
"custom_item": item_name,
"item_axis_values": item_axis_values,
"scene_text": scene,
"location_config": parsed_location_config if _location_config_active(parsed_location_config) else {},
"pose": pose,
"seed_config": seed_config,
"hardcore_position_config": (
parsed_hardcore_position_config
if _hardcore_position_config_active(parsed_hardcore_position_config)
else {}
),
"content_seed_axis": content_axis,
"role_graph": role_graph,
"source_role_graph": source_role_graph,
"action_family": action_family,
"position_family": position_family,
"position_key": position_key,
"position_keys": position_keys,
"source_composition": source_composition,
"pov_character_labels": pov_character_labels,
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
"shared_expression": shared_expression,
"character_expressions": character_expressions,
"character_expression_text": character_expression_text,
"expression_enabled": not expression_disabled,
"expression_disabled": expression_disabled,
"cast_summary": context.get("cast_summary", ""),
"cast_descriptors": cast_descriptors,
"cast_descriptor_text": cast_descriptor_text,
"scene_kind": context.get("scene_kind", ""),
"women_count": context.get("women_count", ""),
"men_count": context.get("men_count", ""),
"person_count": context.get("person_count", ""),
"cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {},
"character_profile": applied_profile,
"character_profile_status": profile_status,
"character_slot": applied_slot,
"character_slot_status": slot_status,
"character_cast_slots": character_slots,
"expression_intensity": expression_intensity,
"expression_intensity_source": expression_intensity_source,
"source": "json_category",
}
)
if context.get("figure"):
row["figure"] = context["figure"]
if expression_disabled:
row = _disable_row_expression(row, expression_intensity_source)
return row
def build_prompt(
category: str,
subcategory: str,
row_number: int,
start_index: int,
seed: int,
clothing: str,
ethnicity: str,
poses: str,
backside_bias: float,
figure: str,
no_plus_women: bool,
no_black: bool,
minimal_clothing_ratio: float,
standard_pose_ratio: float,
trigger: str,
prepend_trigger_to_prompt: bool,
extra_positive: str,
extra_negative: str,
seed_config: str | dict[str, Any] | None = None,
women_count: int = 1,
men_count: int = 1,
camera_config: str | dict[str, Any] | None = None,
expression_intensity: float = 0.5,
character_profile: str | dict[str, Any] | None = None,
character_cast: str | dict[str, Any] | list[Any] | None = None,
expression_enabled: bool = True,
expression_phase: str = "",
hardcore_position_config: str | dict[str, Any] | None = None,
location_config: str | dict[str, Any] | None = None,
composition_config: str | dict[str, Any] | None = None,
) -> dict[str, Any]:
apply_pool_extensions()
row_number = max(1, int(row_number))
start_index = max(1, int(start_index))
seed = int(seed)
ethnicity = normalize_ethnicity_filter(ethnicity, "any")
expression_enabled = not _is_false(expression_enabled)
minimal_ratio = _ratio_or_none(minimal_clothing_ratio)
pose_ratio = _ratio_or_none(standard_pose_ratio)
parsed_seed_config = _parse_seed_config(seed_config)
parsed_location_config = _parse_location_config(location_config)
parsed_composition_config = _parse_composition_config(composition_config)
content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number)
pose_axis_rng = _axis_rng(parsed_seed_config, "pose", seed, row_number)
person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
expression_rng = _axis_rng(parsed_seed_config, "expression", seed, row_number)
clothing = clothing if clothing in ("full", "minimal", "random") else "full"
poses = poses if poses in ("standard", "evocative", "random") else "standard"
figure = figure if figure in ("curvy", "balanced", "bombshell", "random") else "curvy"
clothing = _pick_clothing_mode(content_rng, clothing, minimal_ratio)
poses = _pick_pose_mode(pose_axis_rng, poses, pose_ratio)
figure = _pick_figure_bias(person_rng, figure)
minimal_ratio = None
pose_ratio = None
expression_intensity, expression_intensity_source = _pick_expression_intensity(expression_rng, expression_intensity)
exact_custom_subcategory = bool(subcategory and subcategory != RANDOM_SUBCATEGORY and " / " in subcategory)
if category == "auto_full" and not exact_custom_subcategory:
category = _auto_full_choice(parsed_seed_config, seed, row_number)
if category == "auto_weighted" and not exact_custom_subcategory:
row = _build_auto_weighted_row(
row_number,
start_index,
clothing,
ethnicity,
poses,
float(backside_bias),
figure,
bool(no_plus_women),
bool(no_black),
minimal_ratio,
pose_ratio,
seed,
)
elif category in ("woman", "man", "couple", "group_or_layout") and not exact_custom_subcategory:
row = _build_direct_builtin_row(
category,
row_number,
start_index,
clothing,
ethnicity,
poses,
float(backside_bias),
figure,
bool(no_plus_women),
bool(no_black),
minimal_ratio,
pose_ratio,
seed,
)
else:
row = _build_custom_row(
category,
subcategory,
row_number,
start_index,
ethnicity,
poses,
figure,
bool(no_plus_women),
bool(no_black),
int(women_count),
int(men_count),
seed,
parsed_seed_config,
expression_enabled,
expression_intensity,
expression_intensity_source,
character_profile,
character_cast,
expression_phase,
hardcore_position_config,
parsed_location_config,
parsed_composition_config,
)
if row.get("source") == "built_in_generator":
row = _apply_location_config_to_legacy_row(
row,
parsed_location_config,
parsed_seed_config,
seed,
row_number,
)
row = _apply_composition_config_to_legacy_row(
row,
parsed_composition_config,
parsed_seed_config,
seed,
row_number,
)
if not expression_enabled:
row = _disable_row_expression(row, "disabled")
if extra_positive.strip():
row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}"
row = _apply_camera_config(row, camera_config)
active_trigger = trigger.strip() or g.TRIGGER
row["prompt"] = _prepend_trigger(row["prompt"], active_trigger, bool(prepend_trigger_to_prompt))
row["prompt"] = sanitize_prompt_text(row["prompt"], triggers=(active_trigger,))
row["caption"] = sanitize_caption_text(row.get("caption", ""), triggers=(active_trigger,))
row["negative_prompt"] = sanitize_negative_text(
_combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative)
)
row["trigger"] = active_trigger
row.setdefault("expression_intensity", expression_intensity)
row.setdefault("expression_intensity_source", expression_intensity_source)
return row
def build_prompt_from_configs(
row_number: int,
start_index: int,
seed: int,
category_config: str | dict[str, Any] | None = "",
cast_config: str | dict[str, Any] | None = "",
generation_profile: str | dict[str, Any] | None = "",
filter_config: str | dict[str, Any] | None = "",
seed_config: str | dict[str, Any] | None = "",
camera_config: str | dict[str, Any] | None = "",
character_profile: str | dict[str, Any] | None = "",
character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
category, subcategory = _parse_category_config(category_config)
cast = _parse_cast_config(cast_config)
profile = _parse_generation_profile(generation_profile)
filters = _parse_filter_config(filter_config)
return build_prompt(
category=category,
subcategory=subcategory,
row_number=row_number,
start_index=start_index,
seed=seed,
clothing=profile["clothing"],
ethnicity=filters["ethnicity"],
poses=profile["poses"],
expression_enabled=profile["expression_enabled"],
expression_intensity=profile["expression_intensity"],
backside_bias=profile["backside_bias"],
figure=filters["figure"],
no_plus_women=filters["no_plus_women"],
no_black=filters["no_black"],
women_count=int(cast["women_count"]),
men_count=int(cast["men_count"]),
minimal_clothing_ratio=profile["minimal_clothing_ratio"],
standard_pose_ratio=profile["standard_pose_ratio"],
trigger=profile["trigger"],
prepend_trigger_to_prompt=profile["prepend_trigger_to_prompt"],
extra_positive=extra_positive or "",
extra_negative=extra_negative or "",
seed_config=seed_config or "",
camera_config=camera_config or "",
character_profile=character_profile or "",
character_cast=character_cast or "",
hardcore_position_config=hardcore_position_config or "",
location_config=location_config or "",
composition_config=composition_config or "",
)
INSTA_OF_SOFT_LEVELS = pair_options.INSTA_OF_SOFT_LEVELS
INSTA_OF_HARDCORE_LEVELS = pair_options.INSTA_OF_HARDCORE_LEVELS
INSTA_OF_PLATFORM_STYLES = pair_options.INSTA_OF_PLATFORM_STYLES
INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = pair_options.INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
INSTA_OF_NEGATIVE = pair_options.INSTA_OF_NEGATIVE
INSTA_OF_SOFT_NEGATIVE = pair_options.INSTA_OF_SOFT_NEGATIVE
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL = pair_options.INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL
INSTA_OF_SOFTCORE_OUTFITS = pair_options.INSTA_OF_SOFTCORE_OUTFITS
INSTA_OF_SOFTCORE_POSES = pair_options.INSTA_OF_SOFTCORE_POSES
INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS = pair_options.INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS
INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS = pair_options.INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS
def character_softcore_outfit_values(source: str, custom_outfits: str = "") -> list[str]:
return pair_options.character_softcore_outfit_values(source, custom_outfits)
def character_hardcore_clothing_values(state: str, custom_clothing: str = "") -> list[str]:
return pair_options.character_hardcore_clothing_values(state, custom_clothing)
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:
return pair_options.build_insta_of_options_json(
softcore_cast=softcore_cast,
hardcore_cast=hardcore_cast,
hardcore_women_count=hardcore_women_count,
hardcore_men_count=hardcore_men_count,
softcore_level=softcore_level,
hardcore_level=hardcore_level,
platform_style=platform_style,
continuity=continuity,
hardcore_clothing_continuity=hardcore_clothing_continuity,
softcore_camera_mode=softcore_camera_mode,
hardcore_camera_mode=hardcore_camera_mode,
camera_detail=camera_detail,
softcore_expression_intensity=softcore_expression_intensity,
hardcore_expression_intensity=hardcore_expression_intensity,
softcore_expression_enabled=softcore_expression_enabled,
hardcore_expression_enabled=hardcore_expression_enabled,
hardcore_detail_density=hardcore_detail_density,
hardcore_detail_density_choices=HARDCORE_DETAIL_DENSITY_CHOICES,
)
def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[str, Any]:
return pair_options.parse_insta_of_options(
options_json,
camera_mode_choices=CAMERA_MODE_PROMPTS,
camera_detail_choices=CAMERA_DETAIL_CHOICES,
hardcore_detail_density_choices=HARDCORE_DETAIL_DENSITY_CHOICES,
)
def _insta_of_hardcore_counts(options: dict[str, Any]) -> tuple[int, int]:
return pair_options.hardcore_counts(options)
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 = pair_options.SOFTCORE_CAST_POSES
def _insta_of_softcore_category(level: str) -> tuple[str, str]:
return pair_options.softcore_category(level)
def _insta_of_softcore_outfit(rng: random.Random, level: str) -> str:
return g.choose(rng, pair_options.softcore_outfit_pool(level))
def _insta_of_softcore_item_prompt_label(level: str) -> str:
return pair_options.softcore_item_prompt_label(level)
def _insta_of_softcore_pose(rng: random.Random, level: str) -> str:
return g.choose(rng, pair_options.softcore_pose_pool(level))
def _insta_of_partner_styling(
seed_config: dict[str, int],
seed: int,
row_number: int,
women_count: int,
men_count: int,
pov_labels: list[str] | None = None,
label_map: dict[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
content_rng = _axis_rng(seed_config, "content", seed, row_number + 421)
pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421)
pov_set = set(pov_labels or [])
outfits: list[str] = []
for index in range(max(0, women_count - 1)):
label = chr(ord("B") + index)
full_label = f"Woman {label}"
outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)
sentence = _softcore_outfit_sentence(full_label, outfit)
if sentence:
outfits.append(sentence)
for index in range(max(0, men_count)):
label = chr(ord("A") + index)
full_label = f"Man {label}"
if full_label in pov_set:
continue
outfit = _slot_softcore_outfit((label_map or {}).get(full_label), content_rng) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)
sentence = _softcore_outfit_sentence(full_label, outfit)
if sentence:
outfits.append(sentence)
return {
"outfits": outfits,
"pose": g.choose(pose_rng, SOFTCORE_CAST_POSES),
}
def build_insta_of_pair(
row_number: int,
start_index: int,
seed: int,
ethnicity: str,
figure: str,
no_plus_women: bool,
no_black: bool,
trigger: str,
prepend_trigger_to_prompt: bool,
seed_config: str | dict[str, Any] | None = None,
options_json: str | dict[str, Any] | None = None,
filter_config: str | dict[str, Any] | None = None,
camera_config: str | dict[str, Any] | None = None,
softcore_camera_config: str | dict[str, Any] | None = None,
hardcore_camera_config: str | dict[str, Any] | None = None,
character_profile: str | dict[str, Any] | None = "",
character_cast: str | dict[str, Any] | list[Any] | None = "",
hardcore_position_config: str | dict[str, Any] | None = "",
location_config: str | dict[str, Any] | None = "",
composition_config: str | dict[str, Any] | None = "",
extra_positive: str = "",
extra_negative: str = "",
) -> dict[str, Any]:
options = _parse_insta_of_options(options_json)
if filter_config:
filters = _parse_filter_config(filter_config)
ethnicity = filters["ethnicity"]
figure = filters["figure"]
no_plus_women = filters["no_plus_women"]
no_black = filters["no_black"]
hard_women_count, hard_men_count = _insta_of_hardcore_counts(options)
active_trigger = trigger.strip() or g.TRIGGER
parsed_seed_config = _parse_seed_config(seed_config)
character_slots = _parse_character_cast(character_cast)
character_slot_map = _character_slot_label_map(character_slots)
pov_character_labels = _pov_character_labels(character_slot_map, hard_men_count)
softcore_level_key = str(options["softcore_level"])
soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key)
row_route = pair_rows.build_insta_pair_rows(
row_number=row_number,
start_index=start_index,
seed=seed,
active_trigger=active_trigger,
parsed_seed_config=parsed_seed_config,
options=options,
ethnicity=ethnicity,
figure=figure,
no_plus_women=no_plus_women,
no_black=no_black,
character_profile=character_profile,
character_cast=character_cast or "",
character_slot_map=character_slot_map,
pov_character_labels=pov_character_labels,
hard_women_count=hard_women_count,
hard_men_count=hard_men_count,
soft_category=soft_category,
soft_subcategory=soft_subcategory,
softcore_level_key=softcore_level_key,
hardcore_random_subcategory=RANDOM_SUBCATEGORY,
hardcore_position_config=hardcore_position_config,
location_config=location_config or "",
composition_config=composition_config or "",
build_prompt=build_prompt,
axis_rng=_axis_rng,
cast_expression_intensity_override=_cast_expression_intensity_override,
context_from_character_slot=_context_from_character_slot,
apply_character_context_to_row=_apply_character_context_to_row,
disable_row_expression=_disable_row_expression,
slot_softcore_outfit=_slot_softcore_outfit,
softcore_outfit=_insta_of_softcore_outfit,
softcore_pose=_insta_of_softcore_pose,
softcore_item_prompt_label=_insta_of_softcore_item_prompt_label,
body_exposure_scene_text=_body_exposure_scene_text,
pov_prompt_directive=_pov_prompt_directive,
pov_composition_prompt=_pov_composition_prompt,
)
soft_row = row_route["soft_row"]
hard_row = row_route["hard_row"]
hard_content_rng = row_route["hard_content_rng"]
cast_context = pair_cast.resolve_insta_pair_cast_context(
soft_row=soft_row,
options=options,
parsed_seed_config=parsed_seed_config,
seed=seed,
row_number=row_number,
ethnicity=ethnicity,
figure=figure,
no_plus_women=no_plus_women,
no_black=no_black,
hard_women_count=hard_women_count,
hard_men_count=hard_men_count,
character_slots=character_slots,
character_slot_map=character_slot_map,
pov_character_labels=pov_character_labels,
platform_styles=INSTA_OF_PLATFORM_STYLES,
soft_levels=INSTA_OF_SOFT_LEVELS,
hardcore_levels=INSTA_OF_HARDCORE_LEVELS,
descriptor_from_row=_insta_of_descriptor,
build_cast_descriptors=_insta_of_cast_descriptors,
prompt_cast_descriptors=_insta_of_prompt_cast_descriptors,
partner_styling=_insta_of_partner_styling,
cast_phrase=_insta_of_cast_phrase,
)
descriptor = cast_context["descriptor"]
cast_descriptors = cast_context["cast_descriptors"]
cast_descriptor_text = cast_context["cast_descriptor_text"]
soft_partner_styling = cast_context["soft_partner_styling"]
soft_partner_outfit_text = cast_context["soft_partner_outfit_text"]
platform_style = cast_context["platform_style"]
soft_level = cast_context["soft_level"]
hard_level = cast_context["hard_level"]
camera_route = pair_camera.resolve_insta_pair_camera(
soft_row=soft_row,
hard_row=hard_row,
options=options,
camera_config=camera_config,
softcore_camera_config=softcore_camera_config,
hardcore_camera_config=hardcore_camera_config,
hard_women_count=hard_women_count,
hard_men_count=hard_men_count,
pov_character_labels=pov_character_labels,
camera_detail_choices=CAMERA_DETAIL_CHOICES,
camera_config_with_mode=_camera_config_with_mode,
camera_directive=_camera_directive,
apply_contextual_composition=_apply_coworking_composition,
contextual_composition_prompt=_coworking_composition_prompt,
composition_prompt=_composition_prompt,
camera_scene_directive_for_context=_camera_scene_directive_for_context,
)
soft_row = camera_route["soft_row"]
hard_row = camera_route["hard_row"]
hard_scene = camera_route["hard_scene"]
hard_composition = camera_route["hard_composition"]
soft_camera_config = camera_route["soft_camera_config"]
hard_camera_config = camera_route["hard_camera_config"]
soft_camera_directive = camera_route["soft_camera_directive"]
hard_camera_directive = camera_route["hard_camera_directive"]
soft_camera_scene_directive = camera_route["soft_camera_scene_directive"]
hard_camera_scene_directive = camera_route["hard_camera_scene_directive"]
soft_camera_scene_sentence = camera_route["soft_camera_scene_sentence"]
hard_camera_scene_sentence = camera_route["hard_camera_scene_sentence"]
soft_camera_sentence = camera_route["soft_camera_sentence"]
hard_camera_sentence = camera_route["hard_camera_sentence"]
soft_cast = cast_context["soft_cast"]
soft_cast_presence = cast_context["soft_cast_presence"]
soft_cast_styling_sentence = cast_context["soft_cast_styling_sentence"]
hard_cast = cast_context["hard_cast"]
character_hardcore_clothing_entries = _character_hardcore_clothing_entries(
character_slot_map,
hard_women_count,
hard_men_count,
pov_character_labels,
hard_content_rng,
)
clothing_route = pair_clothing.resolve_hardcore_pair_clothing(
hard_row=hard_row,
mode=options["hardcore_clothing_continuity"],
softcore_outfit=soft_row["item"],
character_hardcore_clothing_entries=character_hardcore_clothing_entries,
men_count=hard_men_count,
pov_labels=pov_character_labels,
rng=hard_content_rng,
continuity_map=INSTA_OF_HARDCORE_CLOTHING_CONTINUITY,
choose=g.choose,
sentence_builder=_hardcore_clothing_sentence,
)
default_man_hardcore_clothing_entries = clothing_route["default_man_hardcore_clothing"]
hard_clothing_state = clothing_route["hardcore_clothing_state"]
hard_clothing_sentence = clothing_route["hardcore_clothing_sentence"]
if clothing_route["requires_body_exposure_scene"]:
hard_scene = _body_exposure_scene_text(hard_scene)
hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "")
hard_row["scene_text"] = hard_scene
hard_detail_density = options["hardcore_detail_density"]
hard_detail_directive = {
"compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ",
"balanced": "",
"dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ",
}[hard_detail_density]
pov_directive = _pov_prompt_directive(pov_character_labels)
soft_descriptor_sentence = cast_context["soft_descriptor_sentence"]
return pair_output.assemble_insta_pair_metadata(
active_trigger=active_trigger,
prepend_trigger_to_prompt=bool(prepend_trigger_to_prompt),
extra_positive=extra_positive,
extra_negative=extra_negative,
soft_negative_base=INSTA_OF_SOFT_NEGATIVE,
hard_negative_base=INSTA_OF_NEGATIVE,
options=options,
platform_style=platform_style,
soft_descriptor_sentence=soft_descriptor_sentence,
soft_level=soft_level,
soft_cast=soft_cast,
soft_cast_presence=soft_cast_presence,
soft_cast_styling_sentence=soft_cast_styling_sentence,
soft_row=soft_row,
soft_camera_scene_sentence=soft_camera_scene_sentence,
soft_camera_sentence=soft_camera_sentence,
hard_level=hard_level,
hard_cast=hard_cast,
cast_descriptor_text=cast_descriptor_text,
pov_directive=pov_directive,
pov_character_labels=pov_character_labels,
hard_clothing_sentence=hard_clothing_sentence,
hard_row=hard_row,
hard_scene=hard_scene,
hard_camera_scene_sentence=hard_camera_scene_sentence,
hard_composition=hard_composition,
hard_detail_directive=hard_detail_directive,
hard_camera_sentence=hard_camera_sentence,
descriptor=descriptor,
soft_partner_outfit_text=soft_partner_outfit_text,
soft_partner_styling=soft_partner_styling,
soft_camera_scene_directive=soft_camera_scene_directive,
soft_camera_config=soft_camera_config,
soft_camera_directive=soft_camera_directive,
hard_camera_scene_directive=hard_camera_scene_directive,
hard_camera_config=hard_camera_config,
hard_camera_directive=hard_camera_directive,
camera_caption_text=_camera_caption_text,
cast_descriptors=cast_descriptors,
character_hardcore_clothing_entries=character_hardcore_clothing_entries,
default_man_hardcore_clothing_entries=default_man_hardcore_clothing_entries,
hard_clothing_state=hard_clothing_state,
hard_detail_density=hard_detail_density,
hard_women_count=hard_women_count,
hard_men_count=hard_men_count,
character_slots=character_slots,
character_slot_map=character_slot_map,
)