5771 lines
210 KiB
Python
5771 lines
210 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 filter_config as filter_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 filter_config as filter_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 = filter_policy.ETHNICITY_FILTER_CHOICES
|
|
ETHNICITY_LIST_KEYS = filter_policy.ETHNICITY_LIST_KEYS
|
|
ETHNICITY_BASE_LIST_KEYS = filter_policy.ETHNICITY_BASE_LIST_KEYS
|
|
EUROPEAN_REGIONAL_LIST_KEYS = filter_policy.EUROPEAN_REGIONAL_LIST_KEYS
|
|
MEDITERRANEAN_REGIONAL_LIST_KEYS = filter_policy.MEDITERRANEAN_REGIONAL_LIST_KEYS
|
|
|
|
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:
|
|
return filter_policy.build_filter_config_json(
|
|
ethnicity=ethnicity,
|
|
figure=figure,
|
|
no_plus_women=no_plus_women,
|
|
no_black=no_black,
|
|
include_european=include_european,
|
|
include_mediterranean_mena=include_mediterranean_mena,
|
|
include_latina=include_latina,
|
|
include_east_asian=include_east_asian,
|
|
include_southeast_asian=include_southeast_asian,
|
|
include_south_asian=include_south_asian,
|
|
include_black_african=include_black_african,
|
|
include_indigenous=include_indigenous,
|
|
include_mixed=include_mixed,
|
|
include_plus_size=include_plus_size,
|
|
)
|
|
|
|
|
|
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:
|
|
return filter_policy.ethnicity_text_from_value(value)
|
|
|
|
|
|
def _is_valid_ethnicity_filter(value: Any) -> bool:
|
|
return filter_policy.is_valid_ethnicity_filter(value)
|
|
|
|
|
|
def normalize_ethnicity_filter(value: Any, default: str = "any", allow_random: bool = False) -> str:
|
|
return filter_policy.normalize_ethnicity_filter(value, default, allow_random)
|
|
|
|
|
|
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]:
|
|
return filter_policy.build_ethnicity_list_json(
|
|
include_european=include_european,
|
|
include_mediterranean_mena=include_mediterranean_mena,
|
|
include_latina=include_latina,
|
|
include_east_asian=include_east_asian,
|
|
include_southeast_asian=include_southeast_asian,
|
|
include_south_asian=include_south_asian,
|
|
include_black_african=include_black_african,
|
|
include_indigenous=include_indigenous,
|
|
include_mixed=include_mixed,
|
|
include_asian=include_asian,
|
|
include_white_asian=include_white_asian,
|
|
include_western_european=include_western_european,
|
|
include_french_european=include_french_european,
|
|
include_germanic_european=include_germanic_european,
|
|
include_nordic_european=include_nordic_european,
|
|
include_celtic_european=include_celtic_european,
|
|
include_slavic_european=include_slavic_european,
|
|
include_baltic_european=include_baltic_european,
|
|
include_alpine_european=include_alpine_european,
|
|
include_balkan_european=include_balkan_european,
|
|
include_greek_mediterranean=include_greek_mediterranean,
|
|
include_italian_mediterranean=include_italian_mediterranean,
|
|
include_iberian_mediterranean=include_iberian_mediterranean,
|
|
strict_excludes=strict_excludes,
|
|
)
|
|
|
|
|
|
def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
return filter_policy.parse_filter_config(filter_config)
|
|
|
|
|
|
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,
|
|
)
|