4994 lines
192 KiB
Python
4994 lines
192 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import random
|
|
import re
|
|
from pathlib import Path
|
|
from string import Formatter
|
|
from typing import Any
|
|
|
|
try:
|
|
from . import generate_prompt_batches as g
|
|
except ImportError: # Allows local smoke tests with `python -c`.
|
|
import generate_prompt_batches as g
|
|
|
|
|
|
ROOT_DIR = Path(__file__).resolve().parent
|
|
CATEGORY_DIR = ROOT_DIR / "categories"
|
|
PROFILE_DIR = ROOT_DIR / "profiles"
|
|
|
|
BUILTIN_CATEGORIES = [
|
|
"auto_weighted",
|
|
"woman",
|
|
"man",
|
|
"couple",
|
|
"group_or_layout",
|
|
"custom_random",
|
|
]
|
|
RANDOM_SUBCATEGORY = "random"
|
|
SEED_AXIS_SALTS = {
|
|
"category": 31,
|
|
"subcategory": 37,
|
|
"content": 41,
|
|
"person": 43,
|
|
"scene": 47,
|
|
"pose": 53,
|
|
"role": 57,
|
|
"expression": 59,
|
|
"composition": 61,
|
|
}
|
|
SEED_AXIS_ALIASES = {
|
|
"category": ("category_seed", "category"),
|
|
"subcategory": ("subcategory_seed", "subcategory"),
|
|
"content": ("content_seed", "item_seed", "outfit_seed", "sexual_pose_seed", "content"),
|
|
"person": ("person_seed", "appearance_seed", "cast_seed", "person"),
|
|
"scene": ("scene_seed", "scene"),
|
|
"pose": ("pose_seed", "sexual_pose_seed", "pose"),
|
|
"role": ("role_seed", "role", "pose_seed", "sexual_pose_seed"),
|
|
"expression": ("expression_seed", "face_seed", "expression"),
|
|
"composition": ("composition_seed", "camera_seed", "composition"),
|
|
}
|
|
|
|
SEED_LOCK_AXES = (
|
|
"category",
|
|
"subcategory",
|
|
"content",
|
|
"person",
|
|
"scene",
|
|
"pose",
|
|
"role",
|
|
"expression",
|
|
"composition",
|
|
)
|
|
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
|
|
|
|
ETHNICITY_FILTER_CHOICES = [
|
|
"any",
|
|
"european",
|
|
"mediterranean_mena",
|
|
"latina",
|
|
"east_asian",
|
|
"southeast_asian",
|
|
"south_asian",
|
|
"black_african",
|
|
"indigenous",
|
|
"mixed",
|
|
"asian",
|
|
"white_asian",
|
|
]
|
|
|
|
CHARACTER_LABEL_CHOICES = [
|
|
"auto_chain",
|
|
"A",
|
|
"B",
|
|
"C",
|
|
"D",
|
|
"E",
|
|
"F",
|
|
"G",
|
|
"H",
|
|
"I",
|
|
"J",
|
|
"K",
|
|
"L",
|
|
]
|
|
CHARACTER_AGE_CHOICES = (
|
|
["random", "manual"]
|
|
+ [f"{age}-year-old adult" for age in range(21, 86)]
|
|
+ [
|
|
"late 20s adult",
|
|
"early 30s adult",
|
|
"mid 30s adult",
|
|
"late 30s adult",
|
|
"early 40s adult",
|
|
"mid 40s adult",
|
|
"late 40s adult",
|
|
"early 50s adult",
|
|
"mid 50s adult",
|
|
"late 50s adult",
|
|
"early 60s adult",
|
|
"mid 60s adult",
|
|
"late 60s adult",
|
|
"early 70s adult",
|
|
"mid 70s adult",
|
|
"late 70s adult",
|
|
"early 80s adult",
|
|
]
|
|
)
|
|
CHARACTER_BODY_CHOICES = [
|
|
"random",
|
|
"manual",
|
|
"slim",
|
|
"petite adult",
|
|
"toned",
|
|
"athletic",
|
|
"average",
|
|
"curvy",
|
|
"soft curvy",
|
|
"curvy athletic",
|
|
"hourglass",
|
|
"slim busty",
|
|
"busty",
|
|
"busty curvy",
|
|
"voluptuous",
|
|
"plus-size",
|
|
"heavyset",
|
|
"fat",
|
|
"stocky",
|
|
"broad",
|
|
"muscular",
|
|
]
|
|
CHARACTER_WOMAN_BODY_CHOICES = [
|
|
"random",
|
|
"manual",
|
|
"slim",
|
|
"petite adult",
|
|
"toned",
|
|
"athletic",
|
|
"average",
|
|
"curvy",
|
|
"soft curvy",
|
|
"curvy athletic",
|
|
"hourglass",
|
|
"slim busty",
|
|
"busty",
|
|
"busty curvy",
|
|
"voluptuous",
|
|
"plus-size",
|
|
"heavyset",
|
|
"fat",
|
|
]
|
|
CHARACTER_MAN_BODY_CHOICES = [
|
|
"random",
|
|
"manual",
|
|
"slim",
|
|
"lean",
|
|
"lean athletic",
|
|
"toned",
|
|
"average",
|
|
"athletic",
|
|
"muscular",
|
|
"broad",
|
|
"broad-shouldered",
|
|
"stocky",
|
|
"heavyset",
|
|
"fat",
|
|
]
|
|
CHARACTER_DESCRIPTOR_DETAIL_CHOICES = ["auto", "full", "medium", "compact", "minimal"]
|
|
CHARACTER_PRESENCE_CHOICES = ["visible", "pov"]
|
|
CHARACTER_RANDOM_TOKENS = {"", "random", "auto", "global", "from_global", "default"}
|
|
|
|
CAMERA_DETAIL_CHOICES = ["off", "compact", "full"]
|
|
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
|
CAMERA_ORBIT_FRAMING_CHOICES = [
|
|
"from_zoom",
|
|
"wide",
|
|
"medium",
|
|
"full_body",
|
|
"three_quarter",
|
|
"close_up",
|
|
"extreme_close_up",
|
|
]
|
|
CAMERA_ORBIT_FOCUS_CHOICES = [
|
|
"auto",
|
|
"face",
|
|
"torso",
|
|
"hips",
|
|
"full_body",
|
|
"action",
|
|
"contact_points",
|
|
"environment",
|
|
]
|
|
|
|
GENERIC_POSITIVE_SUFFIX = (
|
|
"Use crisp clean comic linework, detailed hatching, soft blended shading, "
|
|
"pastel skin tones, muted blues and pinks, warm sensual lighting, and tactile textured paper."
|
|
)
|
|
|
|
SINGLE_TEMPLATE = (
|
|
"A {subject}: {style}, {age}, {body_phrase}, {skin}, {hair}, {eyes}. "
|
|
"{item_label}: {item}. Scene: {scene}. Pose: {pose}. Facial expression: {expression}. "
|
|
"Composition: {composition_prompt}. {positive_suffix} Avoid: {negative_prompt}."
|
|
)
|
|
|
|
COUPLE_TEMPLATE = (
|
|
"{subject_phrase}: {style}. Ages: {age}. Body types: {body}. {item_label}: {item}. "
|
|
"Scene: {scene}. Pose: {pose}. Facial expressions: {expression}. "
|
|
"Composition: {composition_prompt}. {positive_suffix} Avoid: {negative_prompt}."
|
|
)
|
|
|
|
GROUP_TEMPLATE = (
|
|
"{subject_phrase}: {style}, ages {age}, diverse adult body types. {item_label}: {item}. "
|
|
"Scene: {scene}. Facial expressions: {expression}. Composition: {composition_prompt}. "
|
|
"{positive_suffix} Avoid: {negative_prompt}."
|
|
)
|
|
|
|
LAYOUT_TEMPLATE = (
|
|
"{item}: {style}, adults only, clean designed composition. Scene: {scene}. "
|
|
"Facial expression: {expression}. Composition: {composition}. {positive_suffix} "
|
|
"Avoid: {negative_prompt}. Use no readable text unless the layout naturally needs small decorative placeholder marks."
|
|
)
|
|
|
|
CAMERA_MODE_PROMPTS = {
|
|
"disabled": "",
|
|
"standard": "",
|
|
"handheld_selfie": (
|
|
"Camera mode: handheld smartphone selfie, close arm-length framing, visible creator-shot perspective, "
|
|
"slight wide-angle intimacy, direct eye contact, natural phone-camera composition."
|
|
),
|
|
"mirror_selfie": (
|
|
"Camera mode: mirror selfie with the phone visible in one hand, reflective framing, creator looking at the screen, "
|
|
"body and environment visible through the mirror."
|
|
),
|
|
"phone_tripod": (
|
|
"Camera mode: phone on tripod or ring-light stand, creator-facing social-video framing, stable vertical composition, "
|
|
"hands-free self-recorded setup."
|
|
),
|
|
"creator_pov": (
|
|
"Camera mode: creator-held POV, intimate subscriber-view angle, the creator controls the camera, close foreground body framing."
|
|
),
|
|
"bed_selfie": (
|
|
"Camera mode: bed selfie shot from a phone held above or beside the body, intimate close framing, sheets visible around the subject."
|
|
),
|
|
"bathroom_mirror": (
|
|
"Camera mode: bathroom mirror selfie, phone visible, tiled private room, close vertical framing, candid creator-shot energy."
|
|
),
|
|
"phone_flash": (
|
|
"Camera mode: direct phone-flash selfie, crisp flash highlights, candid night-post feeling, hard-edged smartphone shadows."
|
|
),
|
|
"action_cam": (
|
|
"Camera mode: body-mounted or handheld action-camera intimacy, very close wide-angle perspective, dynamic creator-shot framing."
|
|
),
|
|
}
|
|
|
|
CAMERA_COMPACT_LABELS = {
|
|
"disabled": "",
|
|
"standard": "",
|
|
"handheld_selfie": "handheld smartphone selfie",
|
|
"mirror_selfie": "mirror selfie",
|
|
"phone_tripod": "phone tripod / ring-light setup",
|
|
"creator_pov": "creator-held POV",
|
|
"bed_selfie": "bed selfie",
|
|
"bathroom_mirror": "bathroom mirror selfie",
|
|
"phone_flash": "phone-flash selfie",
|
|
"action_cam": "handheld action-camera view",
|
|
"full_body": "full body",
|
|
"three_quarter": "three-quarter body",
|
|
"waist_up": "waist-up",
|
|
"close_up": "close-up",
|
|
"extreme_close_up": "extreme close-up",
|
|
"eye_level": "eye-level",
|
|
"high_angle": "high-angle",
|
|
"low_angle": "low-angle",
|
|
"overhead": "overhead",
|
|
"side_profile": "side-profile",
|
|
"rear_view": "rear-view",
|
|
"mirror_reflection": "mirror reflection",
|
|
"smartphone_wide": "smartphone wide-angle",
|
|
"ultra_wide": "ultra-wide",
|
|
"portrait_lens": "phone portrait lens",
|
|
"telephoto": "telephoto-style",
|
|
"macro_detail": "macro detail",
|
|
"arm_length": "arm-length",
|
|
"near_body": "near-body",
|
|
"bedside": "bedside phone",
|
|
"room_corner": "room-corner phone",
|
|
"vertical_story": "vertical 9:16",
|
|
"square_feed": "square feed",
|
|
"horizontal": "horizontal",
|
|
"phone_visible": "phone visible",
|
|
"phone_hidden": "phone hidden",
|
|
"screen_reflection": "screen reflection",
|
|
"ring_light_visible": "ring light visible",
|
|
}
|
|
|
|
CAMERA_SHOT_PROMPTS = {
|
|
"auto": "",
|
|
"full_body": "Shot size: full body visible, head-to-toe framing, no important body parts cropped out.",
|
|
"three_quarter": "Shot size: three-quarter body framing, face, torso, hips, and thighs clearly visible.",
|
|
"waist_up": "Shot size: waist-up creator framing with face and upper body as the focus.",
|
|
"close_up": "Shot size: close-up framing with face, expression, hands, and body contact emphasized.",
|
|
"extreme_close_up": "Shot size: extreme close-up detail shot, tightly framed and intimate.",
|
|
}
|
|
|
|
CAMERA_ANGLE_PROMPTS = {
|
|
"auto": "",
|
|
"eye_level": "Angle: eye-level camera angle with direct creator eye contact.",
|
|
"high_angle": "Angle: high-angle selfie looking down toward the body.",
|
|
"low_angle": "Angle: low-angle phone camera looking upward from near the body.",
|
|
"overhead": "Angle: overhead phone shot looking down at the full pose.",
|
|
"side_profile": "Angle: side-profile camera view emphasizing body silhouette and contact points.",
|
|
"rear_view": "Angle: rear-view camera framing with the body turned away from the lens.",
|
|
"mirror_reflection": "Angle: mirror-reflection composition with the phone and reflected body placement readable.",
|
|
}
|
|
|
|
CAMERA_LENS_PROMPTS = {
|
|
"auto": "",
|
|
"smartphone_wide": "Lens: smartphone wide-angle lens with slight edge distortion and close personal scale.",
|
|
"ultra_wide": "Lens: ultra-wide phone lens, exaggerated near-camera perspective, environmental context visible.",
|
|
"portrait_lens": "Lens: phone portrait mode, shallow depth of field, crisp subject separation.",
|
|
"telephoto": "Lens: compressed telephoto-style framing, flatter proportions, less distortion.",
|
|
"macro_detail": "Lens: macro-detail phone shot focused on texture, skin, fabric, and contact detail.",
|
|
}
|
|
|
|
CAMERA_DISTANCE_PROMPTS = {
|
|
"auto": "",
|
|
"arm_length": "Camera distance: arm-length selfie distance, close enough to feel handheld.",
|
|
"near_body": "Camera distance: near-body camera placement with intimate foreground framing.",
|
|
"bedside": "Camera distance: phone placed beside the body on the bed or floor.",
|
|
"room_corner": "Camera distance: phone set across the room, self-recorded but wider and more observational.",
|
|
}
|
|
|
|
CAMERA_ORIENTATION_PROMPTS = {
|
|
"auto": "",
|
|
"vertical_story": "Orientation: vertical 9:16 story/reel framing.",
|
|
"square_feed": "Orientation: square social-feed crop.",
|
|
"horizontal": "Orientation: horizontal phone-video crop.",
|
|
}
|
|
|
|
CAMERA_PHONE_PROMPTS = {
|
|
"auto": "",
|
|
"phone_visible": "Phone visibility: phone visible in hand or mirror, clearly creator-shot.",
|
|
"phone_hidden": "Phone visibility: phone is implied but not visible, preserving the selfie/creator-shot perspective.",
|
|
"screen_reflection": "Phone visibility: screen glow or reflection visible in the scene.",
|
|
"ring_light_visible": "Phone visibility: ring light or tripod visible enough to read as self-recorded content.",
|
|
}
|
|
|
|
CAMERA_PRIORITY_PROMPTS = {
|
|
"soft_hint": "Camera priority: treat the camera notes as style guidance.",
|
|
"strong": "Camera priority: strongly preserve the selected camera, lens, angle, crop, and phone-shot perspective.",
|
|
"locked": "Camera priority: locked camera constraint; do not replace this with a studio, third-person, cinematic, or unrelated camera view.",
|
|
}
|
|
|
|
|
|
_EXTENSIONS_APPLIED = False
|
|
|
|
|
|
class SafeFormatDict(dict):
|
|
def __missing__(self, key: str) -> str:
|
|
return "{" + key + "}"
|
|
|
|
|
|
def _json_files() -> list[Path]:
|
|
if not CATEGORY_DIR.exists():
|
|
return []
|
|
return sorted(path for path in CATEGORY_DIR.glob("*.json") if path.is_file())
|
|
|
|
|
|
def _read_json(path: Path) -> dict[str, Any]:
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid JSON in {path}: {exc}") from exc
|
|
if not isinstance(data, dict):
|
|
raise ValueError(f"{path} must contain a JSON object")
|
|
return data
|
|
|
|
|
|
def _slug(value: str) -> str:
|
|
return g.slugify(value) or "custom"
|
|
|
|
|
|
def _list_from(value: Any) -> list[Any]:
|
|
if value is None:
|
|
return []
|
|
if isinstance(value, list):
|
|
return value
|
|
return [value]
|
|
|
|
|
|
def _is_false(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return value is False
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in ("false", "0", "no", "off")
|
|
return False
|
|
|
|
|
|
def _unique_extend(target: list[Any], additions: list[Any]) -> None:
|
|
seen = set()
|
|
for item in target:
|
|
try:
|
|
seen.add(json.dumps(item, sort_keys=True))
|
|
except TypeError:
|
|
seen.add(repr(item))
|
|
for item in additions:
|
|
try:
|
|
marker = json.dumps(item, sort_keys=True)
|
|
except TypeError:
|
|
marker = repr(item)
|
|
if marker not in seen:
|
|
target.append(item)
|
|
seen.add(marker)
|
|
|
|
|
|
def _pair_from(value: Any) -> tuple[str, str]:
|
|
if isinstance(value, dict):
|
|
text = str(
|
|
value.get("prompt")
|
|
or value.get("description")
|
|
or value.get("text")
|
|
or value.get("name")
|
|
or ""
|
|
).strip()
|
|
slug = str(value.get("slug") or _slug(str(value.get("name") or text))).strip()
|
|
if not text:
|
|
raise ValueError(f"Pair extension is missing prompt text: {value!r}")
|
|
return slug, text
|
|
if isinstance(value, (list, tuple)) and len(value) == 2:
|
|
return str(value[0]), str(value[1])
|
|
text = str(value).strip()
|
|
if not text:
|
|
raise ValueError("Pair extension cannot be empty")
|
|
return _slug(text), text
|
|
|
|
|
|
def _weighted_choice(rng: random.Random, items: list[Any]) -> Any:
|
|
if not items:
|
|
raise ValueError("Cannot choose from an empty list")
|
|
weights: list[float] = []
|
|
for item in items:
|
|
weight = item.get("weight", 1.0) if isinstance(item, dict) else 1.0
|
|
try:
|
|
weights.append(max(0.0, float(weight)))
|
|
except (TypeError, ValueError):
|
|
weights.append(1.0)
|
|
total = sum(weights)
|
|
if total <= 0:
|
|
return items[rng.randrange(len(items))]
|
|
pick = rng.random() * total
|
|
running = 0.0
|
|
for item, weight in zip(items, weights):
|
|
running += weight
|
|
if pick <= running:
|
|
return item
|
|
return items[-1]
|
|
|
|
|
|
def _entry_text(item: Any) -> str:
|
|
if isinstance(item, dict):
|
|
return str(
|
|
item.get("template")
|
|
or item.get("prompt")
|
|
or item.get("text")
|
|
or item.get("description")
|
|
or item.get("name")
|
|
or ""
|
|
).strip()
|
|
return str(item).strip()
|
|
|
|
|
|
def _item_text(item: Any) -> str:
|
|
return _entry_text(item)
|
|
|
|
|
|
def _item_name(item: Any) -> str:
|
|
if isinstance(item, dict):
|
|
return str(item.get("name") or _item_text(item)).strip()
|
|
return _item_text(item)
|
|
|
|
|
|
def _template_list(category: dict[str, Any], subcategory: dict[str, Any], item: Any, key: str) -> list[Any]:
|
|
if isinstance(item, dict) and key in item:
|
|
return _list_from(item[key])
|
|
if key in subcategory:
|
|
return _list_from(subcategory[key])
|
|
if key in category:
|
|
return _list_from(category[key])
|
|
return []
|
|
|
|
|
|
def _constraint_int(entry: dict[str, Any], key: str) -> int | None:
|
|
if key not in entry:
|
|
return None
|
|
try:
|
|
return int(entry[key])
|
|
except (TypeError, ValueError):
|
|
return None
|
|
|
|
|
|
def _cast_requirement_matches(requirement: str, women_count: int, men_count: int) -> bool:
|
|
total = women_count + men_count
|
|
requirement = requirement.strip().lower()
|
|
if requirement in ("", "any"):
|
|
return True
|
|
if requirement == "women_only":
|
|
return women_count > 0 and men_count == 0
|
|
if requirement == "men_only":
|
|
return men_count > 0 and women_count == 0
|
|
if requirement == "mixed":
|
|
return women_count > 0 and men_count > 0
|
|
if requirement == "has_women":
|
|
return women_count > 0
|
|
if requirement == "has_men":
|
|
return men_count > 0
|
|
if requirement == "solo":
|
|
return total == 1
|
|
if requirement == "couple":
|
|
return total == 2
|
|
if requirement == "threesome":
|
|
return total == 3
|
|
if requirement == "group":
|
|
return total >= 4
|
|
return True
|
|
|
|
|
|
def _heuristic_cast_compatible(text: str, women_count: int, men_count: int) -> bool:
|
|
text = text.lower()
|
|
if not text:
|
|
return True
|
|
total = women_count + men_count
|
|
if total == 1:
|
|
solo_blocked_terms = (
|
|
"partner",
|
|
"partners",
|
|
"two bodies",
|
|
"three bodies",
|
|
"bodies still pressed",
|
|
"bodies pressed",
|
|
"bodies tangled",
|
|
"wet bodies",
|
|
"chests heaving together",
|
|
"straddling a partner",
|
|
"shared climax",
|
|
"between two",
|
|
"from both sides",
|
|
"front-and-back",
|
|
"body contact",
|
|
)
|
|
if any(term in text for term in solo_blocked_terms):
|
|
return False
|
|
solo_toy_terms = ("toy", "dildo", "finger", "fingers", "self")
|
|
if "penetration" in text and not any(term in text for term in solo_toy_terms):
|
|
return False
|
|
if total < 3 and "threesome" in text:
|
|
return False
|
|
if total != 3 and ("centered threesome" in text or "three-way" in text):
|
|
return False
|
|
if total < 3 and ("three bodies" in text or "center partner" in text or "center body" in text):
|
|
return False
|
|
if total < 4 and ("orgy" in text or "group sex" in text or "group-sex" in text or "group pile" in text):
|
|
return False
|
|
if total < 3 and (
|
|
"double penetration" in text
|
|
or "two partners penetrating" in text
|
|
or "front-and-back penetration" in text
|
|
or "one penis in pussy and one penis in ass" in text
|
|
or "pussy and ass filled" in text
|
|
or "vaginal and anal penetration at the same time" in text
|
|
or "front-and-back double penetration" in text
|
|
or "hardcore double penetration" in text
|
|
or "kneeling double penetration" in text
|
|
or "standing supported double penetration" in text
|
|
or "deep double penetration" in text
|
|
or "between two partners" in text
|
|
or "from both sides" in text
|
|
):
|
|
toy_terms = ("strap-on", "strap on", "dildo", "toy", "finger")
|
|
if not any(term in text for term in toy_terms):
|
|
return False
|
|
if men_count == 0:
|
|
toy_terms = ("strap-on", "strap on", "dildo", "toy", "finger", "fingers")
|
|
penetration_terms = (
|
|
"vaginal penetration",
|
|
"deep vaginal sex",
|
|
"penetrative sex",
|
|
"pussy penetration",
|
|
"pussy stretched",
|
|
"vaginal thrusting",
|
|
"full-body penetrative",
|
|
"close-contact vaginal",
|
|
"penetration clearly visible",
|
|
"explicit penetrative contact",
|
|
)
|
|
if any(term in text for term in penetration_terms) and not any(term in text for term in toy_terms):
|
|
return False
|
|
male_terms = (
|
|
" penis",
|
|
"penis ",
|
|
"penises",
|
|
"cum",
|
|
"creampie",
|
|
"facial",
|
|
"blowjob",
|
|
"fellatio",
|
|
"deepthroat",
|
|
"ejaculation",
|
|
"semen",
|
|
)
|
|
if any(term in text for term in male_terms) and not any(term in text for term in toy_terms):
|
|
return False
|
|
elif men_count < 2 and "penises" in text:
|
|
return False
|
|
if women_count == 0:
|
|
if "penetrative sex" in text and not any(term in text for term in ("anal", "ass", "male/male", "men")):
|
|
return False
|
|
female_terms = (
|
|
"pussy",
|
|
"vaginal",
|
|
"vagina",
|
|
"cunnilingus",
|
|
"clit",
|
|
"clitoris",
|
|
"breasts",
|
|
"breast ",
|
|
"nipples",
|
|
"nipple",
|
|
"underboob",
|
|
)
|
|
if any(term in text for term in female_terms):
|
|
return False
|
|
return True
|
|
|
|
|
|
def _compatible_entry(entry: Any, women_count: int, men_count: int) -> bool:
|
|
if not isinstance(entry, dict):
|
|
return _heuristic_cast_compatible(_entry_text(entry), women_count, men_count)
|
|
total = women_count + men_count
|
|
for key, value in (
|
|
("min_women", women_count),
|
|
("min_men", men_count),
|
|
("min_people", total),
|
|
):
|
|
minimum = _constraint_int(entry, key)
|
|
if minimum is not None and value < minimum:
|
|
return False
|
|
for key, value in (
|
|
("max_women", women_count),
|
|
("max_men", men_count),
|
|
("max_people", total),
|
|
):
|
|
maximum = _constraint_int(entry, key)
|
|
if maximum is not None and value > maximum:
|
|
return False
|
|
requirements = _list_from(entry.get("cast", [])) + _list_from(entry.get("requires", []))
|
|
if requirements and not all(_cast_requirement_matches(str(req), women_count, men_count) for req in requirements):
|
|
return False
|
|
if any(key in entry for key in ("subcategories", "item_templates", "item_axes")):
|
|
return True
|
|
return _heuristic_cast_compatible(_entry_text(entry), women_count, men_count)
|
|
|
|
|
|
def _compatible_entries(entries: list[Any], women_count: int, men_count: int) -> list[Any]:
|
|
filtered = [entry for entry in entries if _compatible_entry(entry, women_count, men_count)]
|
|
return filtered or entries
|
|
|
|
|
|
def _merged_axes(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> dict[str, list[Any]]:
|
|
axes: dict[str, list[Any]] = {}
|
|
for source in (category, subcategory, item if isinstance(item, dict) else None):
|
|
if not isinstance(source, dict):
|
|
continue
|
|
raw_axes = source.get("item_axes", {})
|
|
if raw_axes is None:
|
|
continue
|
|
if not isinstance(raw_axes, dict):
|
|
raise ValueError("item_axes must be a JSON object")
|
|
for key, values in raw_axes.items():
|
|
axes[str(key)] = _list_from(values)
|
|
return axes
|
|
|
|
|
|
def _compose_item(
|
|
rng: random.Random,
|
|
category: dict[str, Any],
|
|
subcategory: dict[str, Any],
|
|
item: Any,
|
|
women_count: int = 1,
|
|
men_count: int = 1,
|
|
) -> tuple[str, str, dict[str, str]]:
|
|
templates = _template_list(category, subcategory, item, "item_templates")
|
|
axes = _merged_axes(category, subcategory, item)
|
|
if templates and axes:
|
|
template = _entry_text(_weighted_choice(rng, _compatible_entries(templates, women_count, men_count)))
|
|
fields = {key for _, key, _, _ in Formatter().parse(template) if key}
|
|
axis_values = {
|
|
name: _entry_text(_weighted_choice(rng, _compatible_entries(axes[name], women_count, men_count)))
|
|
for name in fields
|
|
if name in axes and axes[name]
|
|
}
|
|
item_text = _format(template, axis_values).strip()
|
|
item_name = _item_name(item) or subcategory["name"]
|
|
return item_text, item_name, axis_values
|
|
return _item_text(item), _item_name(item), {}
|
|
|
|
|
|
def _choose_text(rng: random.Random, items: list[Any]) -> str:
|
|
item = _weighted_choice(rng, items)
|
|
return _item_text(item)
|
|
|
|
|
|
def _choose_distinct_text(rng: random.Random, items: list[Any], first_text: str) -> str:
|
|
first_text = _item_text(first_text).lower()
|
|
distinct = [item for item in items if _item_text(item).lower() != first_text]
|
|
if not distinct:
|
|
return ""
|
|
return _choose_text(rng, distinct)
|
|
|
|
|
|
def _choose_pair(rng: random.Random, items: list[Any]) -> tuple[str, str]:
|
|
return _pair_from(_weighted_choice(rng, items))
|
|
|
|
|
|
def _normalize_subcategories(category: dict[str, Any]) -> list[dict[str, Any]]:
|
|
raw = category.get("subcategories", [])
|
|
if isinstance(raw, dict):
|
|
raw = [
|
|
{"name": name, **(value if isinstance(value, dict) else {"items": value})}
|
|
for name, value in raw.items()
|
|
]
|
|
subcategories: list[dict[str, Any]] = []
|
|
for entry in _list_from(raw):
|
|
if isinstance(entry, str):
|
|
sub = {"name": entry, "items": [entry]}
|
|
elif isinstance(entry, dict):
|
|
sub = dict(entry)
|
|
else:
|
|
raise ValueError(f"Subcategory must be an object or string: {entry!r}")
|
|
name = str(sub.get("name") or sub.get("slug") or "General").strip()
|
|
sub["name"] = name
|
|
sub["slug"] = str(sub.get("slug") or _slug(name))
|
|
if "items" not in sub and "prompts" in sub:
|
|
sub["items"] = sub["prompts"]
|
|
if "items" not in sub:
|
|
sub["items"] = [name]
|
|
subcategories.append(sub)
|
|
if not subcategories:
|
|
name = str(category.get("name") or "General")
|
|
subcategories.append({"name": "General", "slug": "general", "items": [name]})
|
|
return subcategories
|
|
|
|
|
|
def _normalize_categories(raw_categories: Any) -> list[dict[str, Any]]:
|
|
if isinstance(raw_categories, dict):
|
|
iterable = [
|
|
{"name": name, **(value if isinstance(value, dict) else {"subcategories": value})}
|
|
for name, value in raw_categories.items()
|
|
]
|
|
else:
|
|
iterable = _list_from(raw_categories)
|
|
|
|
categories: list[dict[str, Any]] = []
|
|
for entry in iterable:
|
|
if not isinstance(entry, dict):
|
|
raise ValueError(f"Category must be an object: {entry!r}")
|
|
category = dict(entry)
|
|
name = str(category.get("name") or category.get("slug") or "Custom").strip()
|
|
category["name"] = name
|
|
category["slug"] = str(category.get("slug") or _slug(name))
|
|
category["subcategories"] = _normalize_subcategories(category)
|
|
categories.append(category)
|
|
return categories
|
|
|
|
|
|
def load_category_library() -> list[dict[str, Any]]:
|
|
categories: list[dict[str, Any]] = []
|
|
for path in _json_files():
|
|
data = _read_json(path)
|
|
categories.extend(_normalize_categories(data.get("categories", [])))
|
|
return categories
|
|
|
|
|
|
def _load_named_pool_library(key: str) -> dict[str, list[Any]]:
|
|
pools: dict[str, list[Any]] = {}
|
|
for path in _json_files():
|
|
data = _read_json(path)
|
|
raw_pools = data.get(key, {})
|
|
if not raw_pools:
|
|
continue
|
|
if not isinstance(raw_pools, dict):
|
|
raise ValueError(f"{key} in {path} must be an object")
|
|
for name, entries in raw_pools.items():
|
|
pool_name = str(name).strip()
|
|
if not pool_name:
|
|
continue
|
|
pools.setdefault(pool_name, [])
|
|
_unique_extend(pools[pool_name], _list_from(entries))
|
|
return pools
|
|
|
|
|
|
def load_scene_pool_library() -> dict[str, list[Any]]:
|
|
return _load_named_pool_library("scene_pools")
|
|
|
|
|
|
def load_expression_pool_library() -> dict[str, list[Any]]:
|
|
return _load_named_pool_library("expression_pools")
|
|
|
|
|
|
def load_composition_pool_library() -> dict[str, list[Any]]:
|
|
return _load_named_pool_library("composition_pools")
|
|
|
|
|
|
def _extension_targets() -> dict[str, tuple[list[Any], bool]]:
|
|
return {
|
|
"women_clothes": (g.WOMEN_CLOTHES, False),
|
|
"women_clothes_minimal": (g.WOMEN_CLOTHES_MINIMAL, False),
|
|
"men_clothes": (g.MEN_CLOTHES, False),
|
|
"men_clothes_minimal": (g.MEN_CLOTHES_MINIMAL, False),
|
|
"couple_outfits": (g.COUPLE_OUTFITS, False),
|
|
"couple_outfits_minimal": (g.COUPLE_OUTFITS_MINIMAL, False),
|
|
"poses": (g.POSES, False),
|
|
"evocative_poses": (g.EVOCATIVE_POSES, False),
|
|
"backside_poses": (g.BACKSIDE_POSES, False),
|
|
"expressions": (g.EXPRESSIONS, False),
|
|
"compositions": (g.COMPOSITIONS, False),
|
|
"props": (g.PROPS, False),
|
|
"figure_curvy": (g.FIGURE_CURVY, False),
|
|
"figure_athletic": (g.FIGURE_ATHLETIC, False),
|
|
"figure_bombshell": (g.FIGURE_BOMBSHELL, False),
|
|
"scenes": (g.SCENES, True),
|
|
"group_scenes": (g.GROUP_SCENES, True),
|
|
"layouts_full": (g.LAYOUTS_FULL, True),
|
|
"layouts_minimal": (g.LAYOUTS_MINIMAL, True),
|
|
"group_compositions": (g.GROUP_COMPOSITIONS, False),
|
|
"group_ages": (g.GROUP_AGES, False),
|
|
}
|
|
|
|
|
|
def apply_pool_extensions() -> None:
|
|
global _EXTENSIONS_APPLIED
|
|
if _EXTENSIONS_APPLIED:
|
|
return
|
|
targets = _extension_targets()
|
|
for path in _json_files():
|
|
data = _read_json(path)
|
|
extensions = data.get("pool_extensions", {})
|
|
if not isinstance(extensions, dict):
|
|
raise ValueError(f"pool_extensions in {path} must be an object")
|
|
for target_name, additions in extensions.items():
|
|
if target_name not in targets:
|
|
known = ", ".join(sorted(targets))
|
|
raise ValueError(f"Unknown pool extension '{target_name}' in {path}. Known: {known}")
|
|
target, expects_pair = targets[target_name]
|
|
normalized = [_pair_from(item) for item in _list_from(additions)] if expects_pair else [
|
|
_item_text(item) for item in _list_from(additions)
|
|
]
|
|
_unique_extend(target, normalized)
|
|
g.EVOCATIVE_ALL = g.EVOCATIVE_POSES + g.BACKSIDE_POSES
|
|
_EXTENSIONS_APPLIED = True
|
|
|
|
|
|
def category_choices() -> list[str]:
|
|
apply_pool_extensions()
|
|
custom = [category["name"] for category in load_category_library()]
|
|
return BUILTIN_CATEGORIES + [name for name in custom if name not in BUILTIN_CATEGORIES]
|
|
|
|
|
|
def subcategory_choices() -> list[str]:
|
|
apply_pool_extensions()
|
|
choices = [RANDOM_SUBCATEGORY]
|
|
for category in load_category_library():
|
|
for subcategory in category["subcategories"]:
|
|
choices.append(f"{category['name']} / {subcategory['name']}")
|
|
return choices
|
|
|
|
|
|
def seed_mode_choices() -> list[str]:
|
|
return list(SEED_MODE_CHOICES)
|
|
|
|
|
|
CATEGORY_PRESETS = {
|
|
"auto_weighted": ("auto_weighted", RANDOM_SUBCATEGORY),
|
|
"women_casual": ("Casual clothes", RANDOM_SUBCATEGORY),
|
|
"men_casual": ("Men casual clothes", RANDOM_SUBCATEGORY),
|
|
"couple_casual": ("Couple casual clothes", RANDOM_SUBCATEGORY),
|
|
"provocative_erotic": ("Provocative erotic clothes", RANDOM_SUBCATEGORY),
|
|
"hardcore_pose": ("Hardcore sexual poses", RANDOM_SUBCATEGORY),
|
|
"custom_random": ("custom_random", RANDOM_SUBCATEGORY),
|
|
}
|
|
|
|
CAST_PRESETS = {
|
|
"solo_woman": (1, 0),
|
|
"solo_man": (0, 1),
|
|
"mixed_couple": (1, 1),
|
|
"two_women": (2, 0),
|
|
"two_men": (0, 2),
|
|
"threesome_2w1m": (2, 1),
|
|
"small_group_3w2m": (3, 2),
|
|
}
|
|
|
|
GENERATION_PROFILE_PRESETS = {
|
|
"balanced": {
|
|
"clothing": "full",
|
|
"poses": "standard",
|
|
"expression_enabled": True,
|
|
"expression_intensity": 0.5,
|
|
"backside_bias": 0.0,
|
|
"minimal_clothing_ratio": -1.0,
|
|
"standard_pose_ratio": -1.0,
|
|
"trigger": "sxcpinup_coloredpencil",
|
|
"prepend_trigger_to_prompt": True,
|
|
},
|
|
"casual_clean": {
|
|
"clothing": "full",
|
|
"poses": "standard",
|
|
"expression_enabled": True,
|
|
"expression_intensity": 0.35,
|
|
"backside_bias": 0.0,
|
|
"minimal_clothing_ratio": -1.0,
|
|
"standard_pose_ratio": -1.0,
|
|
"trigger": "sxcpinup_coloredpencil",
|
|
"prepend_trigger_to_prompt": True,
|
|
},
|
|
"evocative_softcore": {
|
|
"clothing": "minimal",
|
|
"poses": "evocative",
|
|
"expression_enabled": True,
|
|
"expression_intensity": 0.65,
|
|
"backside_bias": 0.2,
|
|
"minimal_clothing_ratio": -1.0,
|
|
"standard_pose_ratio": -1.0,
|
|
"trigger": "sxcpinup_coloredpencil",
|
|
"prepend_trigger_to_prompt": True,
|
|
},
|
|
"hardcore_intense": {
|
|
"clothing": "minimal",
|
|
"poses": "evocative",
|
|
"expression_enabled": True,
|
|
"expression_intensity": 0.9,
|
|
"backside_bias": 0.0,
|
|
"minimal_clothing_ratio": -1.0,
|
|
"standard_pose_ratio": -1.0,
|
|
"trigger": "sxcpinup_coloredpencil",
|
|
"prepend_trigger_to_prompt": True,
|
|
},
|
|
"krea2_friendly": {
|
|
"clothing": "full",
|
|
"poses": "standard",
|
|
"expression_enabled": True,
|
|
"expression_intensity": 0.55,
|
|
"backside_bias": 0.0,
|
|
"minimal_clothing_ratio": -1.0,
|
|
"standard_pose_ratio": -1.0,
|
|
"trigger": "sxcpinup_coloredpencil",
|
|
"prepend_trigger_to_prompt": False,
|
|
},
|
|
"flux_original": {
|
|
"clothing": "full",
|
|
"poses": "standard",
|
|
"expression_enabled": True,
|
|
"expression_intensity": 0.5,
|
|
"backside_bias": 0.0,
|
|
"minimal_clothing_ratio": -1.0,
|
|
"standard_pose_ratio": -1.0,
|
|
"trigger": "sxcpinup_coloredpencil",
|
|
"prepend_trigger_to_prompt": True,
|
|
},
|
|
}
|
|
|
|
|
|
def category_preset_choices() -> list[str]:
|
|
return list(CATEGORY_PRESETS)
|
|
|
|
|
|
def cast_preset_choices() -> list[str]:
|
|
return list(CAST_PRESETS) + ["custom_counts"]
|
|
|
|
|
|
def generation_profile_choices() -> list[str]:
|
|
return list(GENERATION_PROFILE_PRESETS)
|
|
|
|
|
|
def build_category_config_json(preset: str = "auto_weighted", subcategory: str = RANDOM_SUBCATEGORY) -> str:
|
|
category, default_subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
|
chosen_subcategory = subcategory if subcategory and subcategory != RANDOM_SUBCATEGORY else default_subcategory
|
|
return json.dumps(
|
|
{
|
|
"preset": preset if preset in CATEGORY_PRESETS else "auto_weighted",
|
|
"category": category,
|
|
"subcategory": chosen_subcategory,
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def _parse_category_config(category_config: str | dict[str, Any] | None) -> tuple[str, str]:
|
|
if not category_config:
|
|
return CATEGORY_PRESETS["auto_weighted"]
|
|
if isinstance(category_config, dict):
|
|
raw = category_config
|
|
else:
|
|
try:
|
|
raw = json.loads(str(category_config))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid category_config JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("category_config must be a JSON object")
|
|
preset = str(raw.get("preset") or "auto_weighted")
|
|
category, subcategory = CATEGORY_PRESETS.get(preset, CATEGORY_PRESETS["auto_weighted"])
|
|
category = str(raw.get("category") or category)
|
|
subcategory = str(raw.get("subcategory") or subcategory or RANDOM_SUBCATEGORY)
|
|
return category, subcategory
|
|
|
|
|
|
def build_cast_config_json(cast_mode: str = "mixed_couple", women_count: int = 1, men_count: int = 1) -> str:
|
|
if cast_mode in CAST_PRESETS:
|
|
women_count, men_count = CAST_PRESETS[cast_mode]
|
|
else:
|
|
women_count = max(0, min(12, int(women_count)))
|
|
men_count = max(0, min(12, int(men_count)))
|
|
if women_count + men_count == 0:
|
|
women_count = 1
|
|
cast_mode = "custom_counts"
|
|
return json.dumps(
|
|
{
|
|
"cast_mode": cast_mode,
|
|
"women_count": int(women_count),
|
|
"men_count": int(men_count),
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def _parse_cast_config(cast_config: str | dict[str, Any] | None) -> dict[str, int | str]:
|
|
if not cast_config:
|
|
return {"cast_mode": "mixed_couple", "women_count": 1, "men_count": 1}
|
|
if isinstance(cast_config, dict):
|
|
raw = cast_config
|
|
else:
|
|
try:
|
|
raw = json.loads(str(cast_config))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid cast_config JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("cast_config must be a JSON object")
|
|
return json.loads(build_cast_config_json(str(raw.get("cast_mode") or "custom_counts"), raw.get("women_count", 1), raw.get("men_count", 1)))
|
|
|
|
|
|
def build_generation_profile_json(
|
|
profile: str = "balanced",
|
|
clothing_override: str = "profile_default",
|
|
poses_override: str = "profile_default",
|
|
expression_intensity: float = -1.0,
|
|
backside_bias: float = -1.0,
|
|
minimal_clothing_ratio: float = -1.0,
|
|
standard_pose_ratio: float = -1.0,
|
|
trigger_policy: str = "profile_default",
|
|
expression_enabled: bool = True,
|
|
) -> str:
|
|
profile = profile if profile in GENERATION_PROFILE_PRESETS else "balanced"
|
|
config = dict(GENERATION_PROFILE_PRESETS[profile])
|
|
if clothing_override in ("full", "minimal"):
|
|
config["clothing"] = clothing_override
|
|
if poses_override in ("standard", "evocative"):
|
|
config["poses"] = poses_override
|
|
config["expression_enabled"] = not _is_false(expression_enabled)
|
|
if float(expression_intensity) >= 0:
|
|
config["expression_intensity"] = _clamped_float(expression_intensity, config["expression_intensity"])
|
|
if float(backside_bias) >= 0:
|
|
config["backside_bias"] = _clamped_float(backside_bias, config["backside_bias"])
|
|
if float(minimal_clothing_ratio) >= 0:
|
|
config["minimal_clothing_ratio"] = _clamped_float(minimal_clothing_ratio, config["minimal_clothing_ratio"])
|
|
if float(standard_pose_ratio) >= 0:
|
|
config["standard_pose_ratio"] = _clamped_float(standard_pose_ratio, config["standard_pose_ratio"])
|
|
if trigger_policy == "prepend_trigger":
|
|
config["prepend_trigger_to_prompt"] = True
|
|
elif trigger_policy == "do_not_prepend":
|
|
config["prepend_trigger_to_prompt"] = False
|
|
config["profile"] = profile
|
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
|
|
|
|
|
def _parse_generation_profile(profile_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
if not profile_config:
|
|
return dict(GENERATION_PROFILE_PRESETS["balanced"])
|
|
if isinstance(profile_config, dict):
|
|
raw = profile_config
|
|
else:
|
|
try:
|
|
raw = json.loads(str(profile_config))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid generation_profile JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("generation_profile must be a JSON object")
|
|
profile = str(raw.get("profile") or "balanced")
|
|
parsed = dict(GENERATION_PROFILE_PRESETS.get(profile, GENERATION_PROFILE_PRESETS["balanced"]))
|
|
parsed.update(raw)
|
|
parsed["clothing"] = parsed["clothing"] if parsed.get("clothing") in ("full", "minimal") else "full"
|
|
parsed["poses"] = parsed["poses"] if parsed.get("poses") in ("standard", "evocative") else "standard"
|
|
parsed["expression_enabled"] = not _is_false(parsed.get("expression_enabled", True))
|
|
parsed["expression_intensity"] = _clamped_float(parsed.get("expression_intensity"), 0.5)
|
|
parsed["backside_bias"] = _clamped_float(parsed.get("backside_bias"), 0.0)
|
|
parsed["minimal_clothing_ratio"] = _clamped_float(parsed.get("minimal_clothing_ratio"), -1.0, -1.0, 1.0)
|
|
parsed["standard_pose_ratio"] = _clamped_float(parsed.get("standard_pose_ratio"), -1.0, -1.0, 1.0)
|
|
parsed["trigger"] = str(parsed.get("trigger") or "sxcpinup_coloredpencil")
|
|
parsed["prepend_trigger_to_prompt"] = bool(parsed.get("prepend_trigger_to_prompt"))
|
|
return parsed
|
|
|
|
|
|
def build_filter_config_json(
|
|
ethnicity: str = "any",
|
|
figure: str = "curvy",
|
|
no_plus_women: bool = False,
|
|
no_black: bool = False,
|
|
include_european: bool = True,
|
|
include_mediterranean_mena: bool = True,
|
|
include_latina: bool = True,
|
|
include_east_asian: bool = True,
|
|
include_southeast_asian: bool = True,
|
|
include_south_asian: bool = True,
|
|
include_black_african: bool = True,
|
|
include_indigenous: bool = True,
|
|
include_mixed: bool = True,
|
|
include_plus_size: bool = True,
|
|
) -> str:
|
|
include_flags = {
|
|
"european": include_european,
|
|
"mediterranean_mena": include_mediterranean_mena,
|
|
"latina": include_latina,
|
|
"east_asian": include_east_asian,
|
|
"southeast_asian": include_southeast_asian,
|
|
"south_asian": include_south_asian,
|
|
"black_african": include_black_african,
|
|
"indigenous": include_indigenous,
|
|
"mixed": include_mixed,
|
|
}
|
|
selected_ethnicities = [key for key, enabled in include_flags.items() if enabled]
|
|
disabled_ethnicities = [key for key, enabled in include_flags.items() if not enabled]
|
|
enabled_ethnicities = list(selected_ethnicities)
|
|
if enabled_ethnicities:
|
|
enabled_ethnicities.extend(f"exclude_{key}" for key in disabled_ethnicities)
|
|
if 0 < len(selected_ethnicities) < len(include_flags):
|
|
ethnicity = "+".join(enabled_ethnicities)
|
|
elif ethnicity not in ETHNICITY_FILTER_CHOICES:
|
|
ethnicity = "any"
|
|
return json.dumps(
|
|
{
|
|
"ethnicity": ethnicity,
|
|
"ethnicity_includes": selected_ethnicities,
|
|
"figure": figure if figure in ("curvy", "balanced", "bombshell") else "curvy",
|
|
"include_plus_size": bool(include_plus_size),
|
|
"include_black_african": bool(include_black_african),
|
|
"no_plus_women": not bool(include_plus_size) or bool(no_plus_women),
|
|
"no_black": not bool(include_black_african) or bool(no_black),
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def _parse_filter_config(filter_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
defaults = {
|
|
"ethnicity": "any",
|
|
"figure": "curvy",
|
|
"no_plus_women": False,
|
|
"no_black": False,
|
|
"include_plus_size": True,
|
|
"include_black_african": True,
|
|
}
|
|
if not filter_config:
|
|
return defaults
|
|
if isinstance(filter_config, dict):
|
|
raw = filter_config
|
|
else:
|
|
try:
|
|
raw = json.loads(str(filter_config))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid filter_config JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("filter_config must be a JSON object")
|
|
parsed = {**defaults, **raw}
|
|
ethnicity = str(parsed.get("ethnicity") or "any")
|
|
parsed["ethnicity"] = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in ethnicity else "any"
|
|
parsed["figure"] = parsed["figure"] if parsed.get("figure") in ("curvy", "balanced", "bombshell") else "curvy"
|
|
parsed["include_plus_size"] = bool(parsed.get("include_plus_size"))
|
|
parsed["include_black_african"] = bool(parsed.get("include_black_african"))
|
|
parsed["no_plus_women"] = bool(parsed.get("no_plus_women"))
|
|
parsed["no_black"] = bool(parsed.get("no_black"))
|
|
return parsed
|
|
|
|
|
|
def _ratio_or_none(value: float) -> float | None:
|
|
try:
|
|
ratio = float(value)
|
|
except (TypeError, ValueError):
|
|
return None
|
|
if ratio < 0:
|
|
return None
|
|
return max(0.0, min(1.0, ratio))
|
|
|
|
|
|
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
|
|
try:
|
|
number = float(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
return max(min_value, min(max_value, number))
|
|
|
|
|
|
def build_seed_config_json(
|
|
category_seed: int = -1,
|
|
subcategory_seed: int = -1,
|
|
content_seed: int = -1,
|
|
person_seed: int = -1,
|
|
scene_seed: int = -1,
|
|
pose_seed: int = -1,
|
|
role_seed: int = -1,
|
|
expression_seed: int = -1,
|
|
composition_seed: int = -1,
|
|
category_seed_mode: str = "auto",
|
|
subcategory_seed_mode: str = "auto",
|
|
content_seed_mode: str = "auto",
|
|
person_seed_mode: str = "auto",
|
|
scene_seed_mode: str = "auto",
|
|
pose_seed_mode: str = "auto",
|
|
role_seed_mode: str = "auto",
|
|
expression_seed_mode: str = "auto",
|
|
composition_seed_mode: str = "auto",
|
|
) -> str:
|
|
rng = random.SystemRandom()
|
|
|
|
def axis_seed(value: int, mode: str) -> int:
|
|
mode = mode if mode in SEED_MODE_CHOICES else "auto"
|
|
if mode == "auto":
|
|
return int(value)
|
|
if mode == "random":
|
|
return rng.randint(0, 0xFFFFFFFF)
|
|
if mode == "fixed":
|
|
return max(0, int(value))
|
|
return -1
|
|
|
|
return json.dumps(
|
|
{
|
|
"category_seed": axis_seed(category_seed, category_seed_mode),
|
|
"subcategory_seed": axis_seed(subcategory_seed, subcategory_seed_mode),
|
|
"content_seed": axis_seed(content_seed, content_seed_mode),
|
|
"person_seed": axis_seed(person_seed, person_seed_mode),
|
|
"scene_seed": axis_seed(scene_seed, scene_seed_mode),
|
|
"pose_seed": axis_seed(pose_seed, pose_seed_mode),
|
|
"role_seed": axis_seed(role_seed, role_seed_mode),
|
|
"expression_seed": axis_seed(expression_seed, expression_seed_mode),
|
|
"composition_seed": axis_seed(composition_seed, composition_seed_mode),
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def build_seed_lock_config_json(
|
|
base_seed: int = 20260614,
|
|
reroll_axis: str = "none",
|
|
reroll_seed: int = -1,
|
|
) -> str:
|
|
base_seed = int(base_seed)
|
|
reroll_seed = int(reroll_seed)
|
|
reroll_groups = {
|
|
"none": (),
|
|
"category": ("category",),
|
|
"subcategory": ("subcategory",),
|
|
"content": ("content",),
|
|
"person": ("person",),
|
|
"scene": ("scene",),
|
|
"pose": ("pose", "role"),
|
|
"role": ("role",),
|
|
"expression": ("expression",),
|
|
"composition": ("composition",),
|
|
"content_pose": ("content", "pose", "role"),
|
|
"scene_pose": ("scene", "pose", "role"),
|
|
}
|
|
reroll = set(reroll_groups.get(str(reroll_axis or "none"), ()))
|
|
config: dict[str, int] = {}
|
|
for axis in SEED_LOCK_AXES:
|
|
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
|
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
|
|
|
|
|
def _parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
|
|
if not seed_config:
|
|
return {}
|
|
if isinstance(seed_config, dict):
|
|
raw = seed_config
|
|
else:
|
|
try:
|
|
raw = json.loads(str(seed_config))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid seed_config JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("seed_config must be a JSON object")
|
|
parsed: dict[str, int] = {}
|
|
for key, value in raw.items():
|
|
try:
|
|
parsed[str(key)] = int(value)
|
|
except (TypeError, ValueError):
|
|
continue
|
|
return parsed
|
|
|
|
|
|
def _configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
|
|
for key in SEED_AXIS_ALIASES.get(axis, (axis,)):
|
|
value = seed_config.get(key)
|
|
if value is not None and value >= 0:
|
|
return value
|
|
return None
|
|
|
|
|
|
def _axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
|
|
configured = _configured_axis_seed(seed_config, axis)
|
|
salt = SEED_AXIS_SALTS.get(axis, 0)
|
|
if configured is None:
|
|
return random.Random(_row_seed(base_seed, row_number, salt))
|
|
return random.Random(_row_seed(configured, row_number, salt))
|
|
|
|
|
|
def _is_pose_content_category(category: dict[str, Any], subcategory: dict[str, Any]) -> bool:
|
|
haystack = " ".join(
|
|
str(value)
|
|
for value in (
|
|
category.get("name", ""),
|
|
category.get("slug", ""),
|
|
category.get("item_label", ""),
|
|
subcategory.get("name", ""),
|
|
subcategory.get("slug", ""),
|
|
subcategory.get("item_label", ""),
|
|
)
|
|
).lower()
|
|
return "pose" in haystack or "sex" in haystack
|
|
|
|
|
|
def _format(template: str, context: dict[str, Any]) -> str:
|
|
fields = {key for _, key, _, _ in Formatter().parse(template) if key}
|
|
safe_context = SafeFormatDict({key: str(value) for key, value in context.items()})
|
|
for field in fields:
|
|
safe_context.setdefault(field, "{" + field + "}")
|
|
return template.format_map(safe_context)
|
|
|
|
|
|
def _clean_prompt_punctuation(text: str) -> str:
|
|
text = re.sub(r"\s+", " ", str(text or "")).strip()
|
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
|
text = re.sub(r"(?:,\s*){2,}", ", ", text)
|
|
text = re.sub(r"\.\s*\.", ".", text)
|
|
text = re.sub(r":\s*\.", ".", text)
|
|
return text.strip()
|
|
|
|
|
|
def _strip_expression_text(text: str, expression: Any = "") -> str:
|
|
text = str(text or "")
|
|
if not text:
|
|
return ""
|
|
text = re.sub(r"\s*Facial expressions?:\s*[^.]*\.\s*", " ", text, flags=re.IGNORECASE)
|
|
text = re.sub(r",\s*one with [^,]+ and the other with [^,]+(?=,)", "", text, flags=re.IGNORECASE)
|
|
text = re.sub(r",\s*a lively mix of expressions from [^,]+(?=,)", "", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\s+with\s+(?:an?|the)\s+[^,]*expression(?=,)", "", text, flags=re.IGNORECASE)
|
|
expression_text = str(expression or "").strip()
|
|
if expression_text:
|
|
for part in [piece.strip() for piece in expression_text.split(";") if piece.strip()]:
|
|
escaped = re.escape(part)
|
|
text = re.sub(rf",\s*{escaped}(?=,)", "", text, flags=re.IGNORECASE)
|
|
text = re.sub(rf"\s+with\s+(?:an?|the)?\s*{escaped}", "", text, flags=re.IGNORECASE)
|
|
return _clean_prompt_punctuation(text)
|
|
|
|
|
|
def _disable_row_expression(row: dict[str, Any], source: str = "disabled") -> dict[str, Any]:
|
|
previous_expression = row.get("expression", "")
|
|
row["prompt"] = _strip_expression_text(row.get("prompt", ""), previous_expression)
|
|
row["caption"] = _strip_expression_text(row.get("caption", ""), previous_expression)
|
|
row["expression"] = ""
|
|
row["shared_expression"] = ""
|
|
row["character_expressions"] = []
|
|
row["character_expression_text"] = ""
|
|
row["expression_enabled"] = False
|
|
row["expression_disabled"] = True
|
|
row["expression_intensity"] = None
|
|
row["expression_intensity_source"] = source
|
|
return row
|
|
|
|
|
|
def _labeled_expression_sentence(label: str, expression: Any) -> str:
|
|
expression = str(expression or "").strip()
|
|
if not expression:
|
|
return ""
|
|
return f"{label}: {expression}. "
|
|
|
|
|
|
def _prepend_trigger(prompt: str, trigger: str, enabled: bool) -> str:
|
|
trigger = trigger.strip()
|
|
if not enabled or not trigger:
|
|
return prompt
|
|
if prompt.lower().startswith(trigger.lower()):
|
|
return prompt
|
|
return f"{trigger}, {prompt}"
|
|
|
|
|
|
def _combined_negative(base: str, extra: str) -> str:
|
|
parts = [part.strip() for part in (base, extra) if part and part.strip()]
|
|
return ", ".join(parts)
|
|
|
|
|
|
def camera_mode_choices() -> list[str]:
|
|
return list(CAMERA_MODE_PROMPTS)
|
|
|
|
|
|
def ethnicity_choices() -> list[str]:
|
|
return list(ETHNICITY_FILTER_CHOICES)
|
|
|
|
|
|
def character_label_choices() -> list[str]:
|
|
return list(CHARACTER_LABEL_CHOICES)
|
|
|
|
|
|
def character_age_choices() -> list[str]:
|
|
return list(CHARACTER_AGE_CHOICES)
|
|
|
|
|
|
def character_body_choices() -> list[str]:
|
|
return list(CHARACTER_BODY_CHOICES)
|
|
|
|
|
|
def character_woman_body_choices() -> list[str]:
|
|
return list(CHARACTER_WOMAN_BODY_CHOICES)
|
|
|
|
|
|
def character_man_body_choices() -> list[str]:
|
|
return list(CHARACTER_MAN_BODY_CHOICES)
|
|
|
|
|
|
def character_descriptor_detail_choices() -> list[str]:
|
|
return list(CHARACTER_DESCRIPTOR_DETAIL_CHOICES)
|
|
|
|
|
|
def character_presence_choices() -> list[str]:
|
|
return list(CHARACTER_PRESENCE_CHOICES)
|
|
|
|
|
|
def character_ethnicity_choices() -> list[str]:
|
|
return ["random"] + list(ETHNICITY_FILTER_CHOICES)
|
|
|
|
|
|
def character_figure_choices() -> list[str]:
|
|
return ["random", "curvy", "balanced", "bombshell"]
|
|
|
|
|
|
def camera_detail_choices() -> list[str]:
|
|
return list(CAMERA_DETAIL_CHOICES)
|
|
|
|
|
|
def hardcore_detail_density_choices() -> list[str]:
|
|
return list(HARDCORE_DETAIL_DENSITY_CHOICES)
|
|
|
|
|
|
def camera_orbit_framing_choices() -> list[str]:
|
|
return list(CAMERA_ORBIT_FRAMING_CHOICES)
|
|
|
|
|
|
def camera_orbit_focus_choices() -> list[str]:
|
|
return list(CAMERA_ORBIT_FOCUS_CHOICES)
|
|
|
|
|
|
def camera_shot_choices() -> list[str]:
|
|
return list(CAMERA_SHOT_PROMPTS)
|
|
|
|
|
|
def camera_angle_choices() -> list[str]:
|
|
return list(CAMERA_ANGLE_PROMPTS)
|
|
|
|
|
|
def camera_lens_choices() -> list[str]:
|
|
return list(CAMERA_LENS_PROMPTS)
|
|
|
|
|
|
def camera_distance_choices() -> list[str]:
|
|
return list(CAMERA_DISTANCE_PROMPTS)
|
|
|
|
|
|
def camera_orientation_choices() -> list[str]:
|
|
return list(CAMERA_ORIENTATION_PROMPTS)
|
|
|
|
|
|
def camera_phone_choices() -> list[str]:
|
|
return list(CAMERA_PHONE_PROMPTS)
|
|
|
|
|
|
def camera_priority_choices() -> list[str]:
|
|
return list(CAMERA_PRIORITY_PROMPTS)
|
|
|
|
|
|
def build_camera_config_json(
|
|
camera_mode: str = "standard",
|
|
shot_size: str = "auto",
|
|
angle: str = "auto",
|
|
lens: str = "auto",
|
|
distance: str = "auto",
|
|
orientation: str = "auto",
|
|
phone_visibility: str = "auto",
|
|
priority: str = "strong",
|
|
camera_detail: str = "compact",
|
|
) -> str:
|
|
return json.dumps(
|
|
{
|
|
"camera_mode": camera_mode,
|
|
"shot_size": shot_size,
|
|
"angle": angle,
|
|
"lens": lens,
|
|
"distance": distance,
|
|
"orientation": orientation,
|
|
"phone_visibility": phone_visibility,
|
|
"priority": priority,
|
|
"camera_detail": camera_detail,
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def _camera_orbit_direction(horizontal_angle: Any) -> str:
|
|
h_angle = int(float(horizontal_angle or 0)) % 360
|
|
if h_angle < 22.5 or h_angle >= 337.5:
|
|
return "front view"
|
|
if h_angle < 67.5:
|
|
return "front-right quarter view"
|
|
if h_angle < 112.5:
|
|
return "right side view"
|
|
if h_angle < 157.5:
|
|
return "back-right quarter view"
|
|
if h_angle < 202.5:
|
|
return "back view"
|
|
if h_angle < 247.5:
|
|
return "back-left quarter view"
|
|
if h_angle < 292.5:
|
|
return "left side view"
|
|
return "front-left quarter view"
|
|
|
|
|
|
def _camera_orbit_elevation(vertical_angle: Any) -> str:
|
|
vertical = int(float(vertical_angle or 0))
|
|
if vertical < -15:
|
|
return "low-angle shot"
|
|
if vertical < 15:
|
|
return "eye-level shot"
|
|
if vertical < 45:
|
|
return "elevated shot"
|
|
return "high-angle shot"
|
|
|
|
|
|
def _camera_orbit_distance(zoom: Any, framing: str = "from_zoom") -> str:
|
|
framing = framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom"
|
|
framing_labels = {
|
|
"wide": "wide shot",
|
|
"medium": "medium shot",
|
|
"full_body": "full-body shot",
|
|
"three_quarter": "three-quarter body shot",
|
|
"close_up": "close-up",
|
|
"extreme_close_up": "extreme close-up",
|
|
}
|
|
if framing != "from_zoom":
|
|
return framing_labels[framing]
|
|
zoom_value = float(zoom or 0.0)
|
|
if zoom_value < 2:
|
|
return "wide shot"
|
|
if zoom_value < 6:
|
|
return "medium shot"
|
|
return "close-up"
|
|
|
|
|
|
def _camera_orbit_focus(subject_focus: str) -> str:
|
|
return {
|
|
"face": "face and expression centered",
|
|
"torso": "torso and hands centered",
|
|
"hips": "hips and lower body centered",
|
|
"full_body": "full body centered",
|
|
"action": "main action centered",
|
|
"contact_points": "body contact points centered",
|
|
"environment": "subject and room both readable",
|
|
}.get(str(subject_focus or "auto"), "")
|
|
|
|
|
|
def _camera_orbit_prompt(
|
|
horizontal_angle: Any,
|
|
vertical_angle: Any,
|
|
zoom: Any,
|
|
framing: str = "from_zoom",
|
|
subject_focus: str = "auto",
|
|
include_degrees: bool = True,
|
|
) -> tuple[str, dict[str, Any]]:
|
|
azimuth = max(0, min(359, int(float(horizontal_angle or 0))))
|
|
elevation = max(-90, min(90, int(float(vertical_angle or 0))))
|
|
zoom_value = max(0.0, min(10.0, float(zoom or 0.0)))
|
|
direction = _camera_orbit_direction(azimuth)
|
|
elevation_label = _camera_orbit_elevation(elevation)
|
|
distance_label = _camera_orbit_distance(zoom_value, framing)
|
|
focus_label = _camera_orbit_focus(subject_focus)
|
|
pieces = [direction, elevation_label, distance_label, focus_label]
|
|
prompt = ", ".join(piece for piece in pieces if piece)
|
|
if include_degrees:
|
|
prompt = f"{azimuth}-degree {prompt}"
|
|
return prompt, {
|
|
"orbit_azimuth": azimuth,
|
|
"orbit_elevation": elevation,
|
|
"orbit_zoom": zoom_value,
|
|
"orbit_direction": direction,
|
|
"orbit_elevation_label": elevation_label,
|
|
"orbit_distance_label": distance_label,
|
|
"orbit_framing": framing if framing in CAMERA_ORBIT_FRAMING_CHOICES else "from_zoom",
|
|
"orbit_focus": subject_focus if subject_focus in CAMERA_ORBIT_FOCUS_CHOICES else "auto",
|
|
}
|
|
|
|
|
|
def build_camera_orbit_config_json(
|
|
enabled: bool = True,
|
|
camera_mode: str = "standard",
|
|
horizontal_angle: int = 0,
|
|
vertical_angle: int = 0,
|
|
zoom: float = 5.0,
|
|
framing: str = "from_zoom",
|
|
subject_focus: str = "auto",
|
|
lens: str = "auto",
|
|
orientation: str = "auto",
|
|
phone_visibility: str = "auto",
|
|
priority: str = "locked",
|
|
camera_detail: str = "compact",
|
|
include_degrees: bool = True,
|
|
) -> str:
|
|
orbit_prompt, orbit_metadata = _camera_orbit_prompt(
|
|
horizontal_angle,
|
|
vertical_angle,
|
|
zoom,
|
|
framing=framing,
|
|
subject_focus=subject_focus,
|
|
include_degrees=include_degrees,
|
|
)
|
|
config = {
|
|
"camera_mode": "disabled" if _is_false(enabled) else _choice(camera_mode, CAMERA_MODE_PROMPTS, "standard"),
|
|
"shot_size": "auto",
|
|
"angle": "auto",
|
|
"lens": _choice(lens, CAMERA_LENS_PROMPTS, "auto"),
|
|
"distance": "auto",
|
|
"orientation": _choice(orientation, CAMERA_ORIENTATION_PROMPTS, "auto"),
|
|
"phone_visibility": _choice(phone_visibility, CAMERA_PHONE_PROMPTS, "auto"),
|
|
"priority": _choice(priority, CAMERA_PRIORITY_PROMPTS, "locked"),
|
|
"camera_detail": camera_detail if camera_detail in CAMERA_DETAIL_CHOICES else "compact",
|
|
"camera_source": "orbit",
|
|
"custom_camera_prompt": orbit_prompt if not _is_false(enabled) else "",
|
|
**orbit_metadata,
|
|
}
|
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
|
|
|
|
|
QWEN_CAMERA_DIRECTIONS = {
|
|
"front-right quarter view": 45,
|
|
"right side view": 90,
|
|
"back-right quarter view": 135,
|
|
"back view": 180,
|
|
"back-left quarter view": 225,
|
|
"left side view": 270,
|
|
"front-left quarter view": 315,
|
|
"front view": 0,
|
|
}
|
|
QWEN_CAMERA_ELEVATIONS = {
|
|
"low-angle shot": -30,
|
|
"eye-level shot": 0,
|
|
"elevated shot": 30,
|
|
"high-angle shot": 60,
|
|
}
|
|
QWEN_CAMERA_ZOOMS = {
|
|
"wide shot": 0.0,
|
|
"medium shot": 5.0,
|
|
"close-up": 8.0,
|
|
}
|
|
QWEN_CAMERA_SCENE_CENTER_Y = 0.5
|
|
|
|
|
|
def _qwen_prompt_camera_values(qwen_prompt: Any) -> tuple[int, int, float]:
|
|
text = _clean_prompt_punctuation(str(qwen_prompt or "").lower().replace(",", " "))
|
|
horizontal_angle = 0
|
|
vertical_angle = 0
|
|
zoom = 5.0
|
|
for label, value in QWEN_CAMERA_DIRECTIONS.items():
|
|
if label in text:
|
|
horizontal_angle = value
|
|
break
|
|
for label, value in QWEN_CAMERA_ELEVATIONS.items():
|
|
if label in text:
|
|
vertical_angle = value
|
|
break
|
|
for label, value in QWEN_CAMERA_ZOOMS.items():
|
|
if label in text:
|
|
zoom = value
|
|
break
|
|
return horizontal_angle, vertical_angle, zoom
|
|
|
|
|
|
def _camera_info_dict(camera_info: Any) -> dict[str, Any] | None:
|
|
if not camera_info:
|
|
return None
|
|
if isinstance(camera_info, dict):
|
|
return camera_info
|
|
if isinstance(camera_info, str):
|
|
try:
|
|
raw = json.loads(camera_info)
|
|
except json.JSONDecodeError:
|
|
return None
|
|
return raw if isinstance(raw, dict) else None
|
|
return None
|
|
|
|
|
|
def _qwen_camera_info_values(camera_info: Any) -> tuple[int, int, float] | None:
|
|
info = _camera_info_dict(camera_info)
|
|
if not info:
|
|
return None
|
|
position = info.get("position") if isinstance(info.get("position"), dict) else {}
|
|
target = info.get("target") if isinstance(info.get("target"), dict) else {}
|
|
try:
|
|
dx = float(position.get("x", 0.0)) - float(target.get("x", 0.0))
|
|
dy = float(position.get("y", QWEN_CAMERA_SCENE_CENTER_Y)) - float(
|
|
target.get("y", QWEN_CAMERA_SCENE_CENTER_Y)
|
|
)
|
|
dz = float(position.get("z", 0.0)) - float(target.get("z", 0.0))
|
|
except (TypeError, ValueError):
|
|
return None
|
|
distance = math.sqrt(dx * dx + dy * dy + dz * dz)
|
|
if distance <= 0:
|
|
return None
|
|
horizontal_angle = int(round(math.degrees(math.atan2(dx, dz)))) % 360
|
|
vertical_angle = int(round(math.degrees(math.asin(max(-1.0, min(1.0, dy / distance))))))
|
|
zoom = max(0.0, min(10.0, ((2.6 - distance) / 2.0) * 10.0))
|
|
return horizontal_angle, vertical_angle, round(zoom, 2)
|
|
|
|
|
|
def build_qwen_camera_config_json(
|
|
qwen_prompt: str = "",
|
|
camera_info: Any = None,
|
|
prefer_camera_info: bool = True,
|
|
camera_mode: str = "standard",
|
|
subject_focus: str = "auto",
|
|
lens: str = "auto",
|
|
orientation: str = "auto",
|
|
phone_visibility: str = "auto",
|
|
priority: str = "locked",
|
|
camera_detail: str = "compact",
|
|
include_degrees: bool = False,
|
|
) -> str:
|
|
info_values = _qwen_camera_info_values(camera_info)
|
|
if prefer_camera_info and info_values is not None:
|
|
horizontal_angle, vertical_angle, zoom = info_values
|
|
source = "qwen_multiangle_camera_info"
|
|
else:
|
|
horizontal_angle, vertical_angle, zoom = _qwen_prompt_camera_values(qwen_prompt)
|
|
source = "qwen_multiangle_prompt"
|
|
config = json.loads(
|
|
build_camera_orbit_config_json(
|
|
enabled=True,
|
|
camera_mode=camera_mode,
|
|
horizontal_angle=horizontal_angle,
|
|
vertical_angle=vertical_angle,
|
|
zoom=zoom,
|
|
framing="from_zoom",
|
|
subject_focus=subject_focus,
|
|
lens=lens,
|
|
orientation=orientation,
|
|
phone_visibility=phone_visibility,
|
|
priority=priority,
|
|
camera_detail=camera_detail,
|
|
include_degrees=include_degrees,
|
|
)
|
|
)
|
|
config["camera_source"] = source
|
|
config["qwen_prompt"] = str(qwen_prompt or "").strip()
|
|
if info_values is not None:
|
|
config["qwen_camera_info_values"] = {
|
|
"horizontal_angle": info_values[0],
|
|
"vertical_angle": info_values[1],
|
|
"zoom": info_values[2],
|
|
}
|
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
|
|
|
|
|
def _choice(value: Any, choices: dict[str, str], default: str) -> str:
|
|
value = str(value or default)
|
|
return value if value in choices else default
|
|
|
|
|
|
def _parse_camera_config(camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
defaults = {
|
|
"camera_mode": "standard",
|
|
"shot_size": "auto",
|
|
"angle": "auto",
|
|
"lens": "auto",
|
|
"distance": "auto",
|
|
"orientation": "auto",
|
|
"phone_visibility": "auto",
|
|
"priority": "strong",
|
|
"camera_detail": "compact",
|
|
}
|
|
if not camera_config:
|
|
return defaults
|
|
if isinstance(camera_config, dict):
|
|
raw = camera_config
|
|
else:
|
|
try:
|
|
raw = json.loads(str(camera_config))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid camera_config JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("camera_config must be a JSON object")
|
|
parsed = {**defaults, **raw}
|
|
custom_camera_prompt = _clean_prompt_punctuation(parsed.get("custom_camera_prompt", "")).rstrip(".")
|
|
camera_source = str(parsed.get("camera_source") or "")
|
|
normalized = {
|
|
"camera_mode": _choice(parsed.get("camera_mode"), CAMERA_MODE_PROMPTS, defaults["camera_mode"]),
|
|
"shot_size": _choice(parsed.get("shot_size"), CAMERA_SHOT_PROMPTS, defaults["shot_size"]),
|
|
"angle": _choice(parsed.get("angle"), CAMERA_ANGLE_PROMPTS, defaults["angle"]),
|
|
"lens": _choice(parsed.get("lens"), CAMERA_LENS_PROMPTS, defaults["lens"]),
|
|
"distance": _choice(parsed.get("distance"), CAMERA_DISTANCE_PROMPTS, defaults["distance"]),
|
|
"orientation": _choice(parsed.get("orientation"), CAMERA_ORIENTATION_PROMPTS, defaults["orientation"]),
|
|
"phone_visibility": _choice(parsed.get("phone_visibility"), CAMERA_PHONE_PROMPTS, defaults["phone_visibility"]),
|
|
"priority": _choice(parsed.get("priority"), CAMERA_PRIORITY_PROMPTS, defaults["priority"]),
|
|
"camera_detail": str(parsed.get("camera_detail") or defaults["camera_detail"])
|
|
if str(parsed.get("camera_detail") or defaults["camera_detail"]) in CAMERA_DETAIL_CHOICES
|
|
else defaults["camera_detail"],
|
|
}
|
|
if custom_camera_prompt:
|
|
normalized["custom_camera_prompt"] = custom_camera_prompt
|
|
if camera_source:
|
|
normalized["camera_source"] = camera_source
|
|
for key in (
|
|
"orbit_azimuth",
|
|
"orbit_elevation",
|
|
"orbit_zoom",
|
|
"orbit_direction",
|
|
"orbit_elevation_label",
|
|
"orbit_distance_label",
|
|
"orbit_framing",
|
|
"orbit_focus",
|
|
):
|
|
if key in parsed:
|
|
normalized[key] = parsed[key]
|
|
return normalized
|
|
|
|
|
|
def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, Any]:
|
|
parsed = _parse_camera_config(camera_config)
|
|
if camera_mode and camera_mode != "from_camera_config":
|
|
parsed["camera_mode"] = _choice(camera_mode, CAMERA_MODE_PROMPTS, parsed["camera_mode"])
|
|
return parsed
|
|
|
|
|
|
def _camera_directive(camera_config: str | dict[str, Any] | None) -> tuple[str, dict[str, Any]]:
|
|
parsed = _parse_camera_config(camera_config)
|
|
if parsed["camera_detail"] == "off" or parsed["camera_mode"] == "disabled":
|
|
return "", parsed
|
|
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
|
|
if parsed["camera_detail"] == "compact":
|
|
values = [
|
|
parsed["camera_mode"],
|
|
parsed["shot_size"],
|
|
parsed["angle"],
|
|
parsed["lens"],
|
|
parsed["distance"],
|
|
parsed["orientation"],
|
|
parsed["phone_visibility"],
|
|
]
|
|
labels = [CAMERA_COMPACT_LABELS.get(value, value.replace("_", " ")) for value in values]
|
|
labels = [label for value, label in zip(values, labels) if label and value != "auto"]
|
|
if custom_camera_prompt:
|
|
labels.append(custom_camera_prompt)
|
|
if not labels:
|
|
return "", parsed
|
|
directive = "Camera: " + ", ".join(labels) + "."
|
|
if parsed["priority"] == "locked":
|
|
directive += " Keep this camera framing."
|
|
return directive, parsed
|
|
parts = [
|
|
CAMERA_MODE_PROMPTS[parsed["camera_mode"]],
|
|
CAMERA_SHOT_PROMPTS[parsed["shot_size"]],
|
|
CAMERA_ANGLE_PROMPTS[parsed["angle"]],
|
|
CAMERA_LENS_PROMPTS[parsed["lens"]],
|
|
CAMERA_DISTANCE_PROMPTS[parsed["distance"]],
|
|
CAMERA_ORIENTATION_PROMPTS[parsed["orientation"]],
|
|
CAMERA_PHONE_PROMPTS[parsed["phone_visibility"]],
|
|
]
|
|
if custom_camera_prompt:
|
|
parts.append(f"Camera orbit: {custom_camera_prompt}.")
|
|
parts = [part for part in parts if part]
|
|
if not parts:
|
|
return "", parsed
|
|
parts.append(CAMERA_PRIORITY_PROMPTS[parsed["priority"]])
|
|
return " ".join(parts), parsed
|
|
|
|
|
|
def _insert_positive_directive(prompt: str, directive: str) -> str:
|
|
marker = " Avoid:"
|
|
if marker in prompt:
|
|
before, after = prompt.split(marker, 1)
|
|
return f"{before.rstrip()} {directive}{marker}{after}"
|
|
return f"{prompt.rstrip()} {directive}"
|
|
|
|
|
|
def _camera_caption_text(parsed: dict[str, Any]) -> str:
|
|
custom_camera_prompt = str(parsed.get("custom_camera_prompt") or "").strip()
|
|
if custom_camera_prompt:
|
|
return custom_camera_prompt
|
|
camera_mode = str(parsed.get("camera_mode") or "").replace("_", " ").strip()
|
|
if not camera_mode or camera_mode == "standard":
|
|
return ""
|
|
return f"{camera_mode} camera framing"
|
|
|
|
|
|
def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
directive, parsed = _camera_directive(camera_config)
|
|
row["camera_config"] = parsed
|
|
row["camera_directive"] = directive
|
|
if not directive:
|
|
return row
|
|
row["prompt"] = _insert_positive_directive(row["prompt"], directive)
|
|
camera_caption = _camera_caption_text(parsed)
|
|
if camera_caption:
|
|
row["caption"] = f"{row.get('caption', '').rstrip()}, {camera_caption}"
|
|
return row
|
|
|
|
|
|
def _row_seed(seed: int, row_number: int, salt: int = 0) -> int:
|
|
return int(seed) + int(row_number) * 1009 + salt * 9176
|
|
|
|
|
|
def _pick_clothing_mode(rng: random.Random, clothing: str, minimal_ratio: float | None) -> str:
|
|
if minimal_ratio is None:
|
|
return clothing
|
|
return "minimal" if rng.random() < minimal_ratio else "full"
|
|
|
|
|
|
def _pick_pose_mode(rng: random.Random, poses: str, standard_ratio: float | None) -> str:
|
|
if standard_ratio is None:
|
|
return poses
|
|
return "standard" if rng.random() < standard_ratio else "evocative"
|
|
|
|
|
|
def _build_auto_weighted_row(
|
|
row_number: int,
|
|
start_index: int,
|
|
clothing: str,
|
|
ethnicity: str,
|
|
poses: str,
|
|
backside_bias: float,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
minimal_clothing_ratio: float | None,
|
|
standard_pose_ratio: float | None,
|
|
seed: int,
|
|
) -> dict[str, Any]:
|
|
batch_number = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
|
|
rows = g.build_rows(
|
|
batch_number * g.BATCH_SIZE,
|
|
start_index,
|
|
clothing,
|
|
ethnicity,
|
|
poses,
|
|
backside_bias,
|
|
figure,
|
|
no_plus_women,
|
|
no_black,
|
|
minimal_clothing_ratio,
|
|
standard_pose_ratio,
|
|
seed,
|
|
g.EXPRESSION_SEED + seed,
|
|
)
|
|
row = rows[row_number - 1]
|
|
row["main_category"] = "auto_weighted"
|
|
row["subcategory"] = row.get("primary_subject", "auto")
|
|
row["source"] = "built_in_generator"
|
|
return row
|
|
|
|
|
|
def _build_direct_builtin_row(
|
|
category: str,
|
|
row_number: int,
|
|
start_index: int,
|
|
clothing: str,
|
|
ethnicity: str,
|
|
poses: str,
|
|
backside_bias: float,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
minimal_clothing_ratio: float | None,
|
|
standard_pose_ratio: float | None,
|
|
seed: int,
|
|
) -> dict[str, Any]:
|
|
rng = random.Random(_row_seed(seed, row_number))
|
|
expr_deck = g.ExpressionDeck(g.EXPRESSIONS, random.Random(_row_seed(g.EXPRESSION_SEED + seed, row_number)))
|
|
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
|
|
index = start_index + row_number - 1
|
|
row_clothing = _pick_clothing_mode(rng, clothing, minimal_clothing_ratio)
|
|
row_poses = _pick_pose_mode(rng, poses, standard_pose_ratio)
|
|
|
|
if category == "woman":
|
|
row = g.make_single(
|
|
index,
|
|
batch,
|
|
rng,
|
|
"woman",
|
|
expr_deck,
|
|
row_clothing,
|
|
ethnicity,
|
|
row_poses,
|
|
backside_bias,
|
|
figure,
|
|
no_plus_women,
|
|
no_black,
|
|
)
|
|
elif category == "man":
|
|
row = g.make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_poses, backside_bias, figure)
|
|
elif category == "couple":
|
|
row = g.make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
|
|
elif category == "group_or_layout":
|
|
row = g.make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus_women)
|
|
else:
|
|
raise ValueError(f"Unknown built-in category: {category}")
|
|
|
|
row["main_category"] = category
|
|
row["subcategory"] = row.get("pose_mode", category)
|
|
row["source"] = "built_in_generator"
|
|
return row
|
|
|
|
|
|
def _find_category(categories: list[dict[str, Any]], name_or_slug: str) -> dict[str, Any] | None:
|
|
wanted = name_or_slug.strip().lower()
|
|
for category in categories:
|
|
if category["name"].lower() == wanted or category["slug"].lower() == wanted:
|
|
return category
|
|
return None
|
|
|
|
|
|
def _base_cast_counts(women_count: int, men_count: int) -> tuple[int, int]:
|
|
women_count = max(0, int(women_count))
|
|
men_count = max(0, int(men_count))
|
|
if women_count + men_count == 0:
|
|
women_count = 1
|
|
return women_count, men_count
|
|
|
|
|
|
def _counts_for_exact_subcategory(
|
|
subcategory: dict[str, Any],
|
|
women_count: int,
|
|
men_count: int,
|
|
) -> tuple[int, int]:
|
|
women_count, men_count = _base_cast_counts(women_count, men_count)
|
|
|
|
min_women = _constraint_int(subcategory, "min_women")
|
|
if min_women is not None and women_count < min_women:
|
|
women_count = min_women
|
|
min_men = _constraint_int(subcategory, "min_men")
|
|
if min_men is not None and men_count < min_men:
|
|
men_count = min_men
|
|
|
|
min_people = _constraint_int(subcategory, "min_people")
|
|
if min_people is not None:
|
|
missing = min_people - (women_count + men_count)
|
|
if missing > 0:
|
|
if women_count > 0 or men_count == 0:
|
|
women_count += missing
|
|
else:
|
|
men_count += missing
|
|
return women_count, men_count
|
|
|
|
|
|
def _find_subcategory(
|
|
categories: list[dict[str, Any]],
|
|
category_choice: str,
|
|
subcategory_choice: str,
|
|
category_rng: random.Random,
|
|
subcategory_rng: random.Random,
|
|
women_count: int = 1,
|
|
men_count: int = 1,
|
|
) -> tuple[dict[str, Any], dict[str, Any], int, int]:
|
|
women_count, men_count = _base_cast_counts(women_count, men_count)
|
|
if subcategory_choice and subcategory_choice != RANDOM_SUBCATEGORY and " / " in subcategory_choice:
|
|
category_name, subcategory_name = subcategory_choice.split(" / ", 1)
|
|
category = _find_category(categories, category_name)
|
|
if not category:
|
|
raise ValueError(f"Unknown category in subcategory picker: {category_name}")
|
|
wanted = subcategory_name.strip().lower()
|
|
for subcategory in category["subcategories"]:
|
|
if subcategory["name"].lower() == wanted or subcategory["slug"].lower() == wanted:
|
|
adjusted_women_count, adjusted_men_count = _counts_for_exact_subcategory(
|
|
subcategory,
|
|
women_count,
|
|
men_count,
|
|
)
|
|
if not _compatible_entry(subcategory, adjusted_women_count, adjusted_men_count):
|
|
raise ValueError(
|
|
f"Subcategory '{subcategory['name']}' is not compatible with "
|
|
f"women_count={women_count}, men_count={men_count}"
|
|
)
|
|
return category, subcategory, adjusted_women_count, adjusted_men_count
|
|
raise ValueError(f"Unknown subcategory '{subcategory_name}' for category '{category_name}'")
|
|
|
|
if category_choice == "custom_random":
|
|
if not categories:
|
|
raise ValueError("No custom categories found in categories/*.json")
|
|
category = _weighted_choice(category_rng, categories)
|
|
else:
|
|
category = _find_category(categories, category_choice)
|
|
if not category:
|
|
raise ValueError(f"Unknown custom category: {category_choice}")
|
|
subcategories = _compatible_entries(category["subcategories"], women_count, men_count)
|
|
subcategory = _weighted_choice(subcategory_rng, subcategories)
|
|
return category, subcategory, women_count, men_count
|
|
|
|
|
|
def _merged_field(category: dict[str, Any], subcategory: dict[str, Any], item: Any, key: str, default: Any = None) -> Any:
|
|
if isinstance(item, dict) and key in item:
|
|
return item[key]
|
|
if key in subcategory:
|
|
return subcategory[key]
|
|
if key in category:
|
|
return category[key]
|
|
return default
|
|
|
|
|
|
def _body_phrase(body: Any, figure_note: Any = "") -> str:
|
|
body = str(body or "").strip()
|
|
figure_note = str(figure_note or "").strip()
|
|
if not body:
|
|
return figure_note
|
|
if not figure_note:
|
|
return f"{body} figure"
|
|
if "figure" in figure_note.lower():
|
|
return f"{body} build and {figure_note}"
|
|
return f"{body} figure with {figure_note}"
|
|
|
|
|
|
def _safe_profile_name(profile_name: str) -> str:
|
|
profile_name = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(profile_name or "").strip()).strip("_")
|
|
return profile_name[:64] or "profile"
|
|
|
|
|
|
def _profile_path(profile_name: str) -> Path:
|
|
return PROFILE_DIR / f"{_safe_profile_name(profile_name)}.json"
|
|
|
|
|
|
def character_profile_choices() -> list[str]:
|
|
if not PROFILE_DIR.exists():
|
|
return ["manual"]
|
|
names = sorted(path.stem for path in PROFILE_DIR.glob("*.json") if path.is_file())
|
|
return ["manual"] + names
|
|
|
|
|
|
def _load_json_object(value: str | dict[str, Any] | None, label: str) -> dict[str, Any]:
|
|
if not value:
|
|
return {}
|
|
if isinstance(value, dict):
|
|
return value
|
|
try:
|
|
raw = json.loads(str(value))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid {label} JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError(f"{label} must be a JSON object")
|
|
return raw
|
|
|
|
|
|
def _slot_value(value: Any) -> str:
|
|
text = str(value or "").strip()
|
|
if text.lower() in CHARACTER_RANDOM_TOKENS:
|
|
return ""
|
|
return text
|
|
|
|
|
|
def _normalize_descriptor_detail(value: Any) -> str:
|
|
text = str(value or "auto").strip()
|
|
return text if text in CHARACTER_DESCRIPTOR_DETAIL_CHOICES else "auto"
|
|
|
|
|
|
def _normalize_presence_mode(value: Any, subject_type: str) -> str:
|
|
text = str(value or "visible").strip().lower()
|
|
if text not in CHARACTER_PRESENCE_CHOICES:
|
|
text = "visible"
|
|
if subject_type != "man":
|
|
return "visible"
|
|
return text
|
|
|
|
|
|
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
|
|
if not slot:
|
|
return False
|
|
return slot.get("subject_type") == "man" and slot.get("presence_mode") == "pov"
|
|
|
|
|
|
def _normalize_slot_expression_intensity(value: Any) -> float:
|
|
try:
|
|
intensity = float(value)
|
|
except (TypeError, ValueError):
|
|
return -1.0
|
|
if intensity < 0:
|
|
return -1.0
|
|
return _clamped_float(intensity, 0.5)
|
|
|
|
|
|
def _slot_expression_enabled(slot: dict[str, Any] | None) -> bool:
|
|
if not slot:
|
|
return True
|
|
return not _is_false(slot.get("expression_enabled", True))
|
|
|
|
|
|
def _slot_expression_intensity(slot: dict[str, Any] | None) -> float | None:
|
|
if not slot or not _slot_expression_enabled(slot):
|
|
return None
|
|
intensity = _normalize_slot_expression_intensity(slot.get("expression_intensity"))
|
|
return intensity if intensity >= 0 else None
|
|
|
|
|
|
def _slot_expression_intensity_for_phase(slot: dict[str, Any] | None, phase: str = "") -> float | None:
|
|
if not slot or not _slot_expression_enabled(slot):
|
|
return None
|
|
phase_key = f"{phase}_expression_intensity" if phase in ("softcore", "hardcore") else ""
|
|
if phase_key:
|
|
intensity = _normalize_slot_expression_intensity(slot.get(phase_key))
|
|
if intensity >= 0:
|
|
return intensity
|
|
return _slot_expression_intensity(slot)
|
|
|
|
|
|
def _mean(values: list[float]) -> float:
|
|
return sum(values) / len(values)
|
|
|
|
|
|
def _cast_expression_intensity_override(
|
|
fallback: float,
|
|
label_map: dict[str, dict[str, Any]],
|
|
women_count: int,
|
|
men_count: int,
|
|
expression_phase: str = "",
|
|
) -> tuple[float | None, str]:
|
|
groups: list[tuple[str, list[str]]] = [
|
|
("women", [f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))]),
|
|
("men", [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]),
|
|
]
|
|
all_values: list[float] = []
|
|
matching_slots: list[dict[str, Any]] = []
|
|
for group_name, labels in groups:
|
|
values: list[float] = []
|
|
value_labels: list[str] = []
|
|
for label in labels:
|
|
slot = label_map.get(label)
|
|
if _slot_is_pov(slot):
|
|
continue
|
|
if slot:
|
|
matching_slots.append(slot)
|
|
value = _slot_expression_intensity_for_phase(slot, expression_phase)
|
|
if value is not None:
|
|
values.append(value)
|
|
value_labels.append(label)
|
|
all_values.append(value)
|
|
if values:
|
|
if len(values) == 1:
|
|
return values[0], f"character_slot:{value_labels[0]}"
|
|
return _mean(values), f"character_slots:{group_name}"
|
|
if all_values:
|
|
return _mean(all_values), "character_slots:cast"
|
|
if matching_slots and all(not _slot_expression_enabled(slot) for slot in matching_slots):
|
|
return None, "character_slots:disabled"
|
|
return fallback, "input"
|
|
|
|
|
|
def _character_expression_entries(
|
|
rng: random.Random,
|
|
expression_pool: list[Any],
|
|
fallback_intensity: float,
|
|
label_map: dict[str, dict[str, Any]],
|
|
women_count: int,
|
|
men_count: int,
|
|
expression_phase: str = "",
|
|
) -> list[str]:
|
|
labels = [
|
|
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
|
|
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
|
|
]
|
|
expressions: list[str] = []
|
|
used: set[str] = set()
|
|
for label in labels:
|
|
slot = label_map.get(label)
|
|
if not slot:
|
|
continue
|
|
if _slot_is_pov(slot):
|
|
continue
|
|
if not _slot_expression_enabled(slot):
|
|
continue
|
|
intensity = _slot_expression_intensity_for_phase(slot, expression_phase)
|
|
if intensity is None:
|
|
intensity = fallback_intensity
|
|
entries = _compatible_entries(
|
|
_expression_entries_for_intensity(expression_pool, intensity),
|
|
women_count,
|
|
men_count,
|
|
)
|
|
if not entries:
|
|
continue
|
|
choice = ""
|
|
for _attempt in range(5):
|
|
candidate = _choose_text(rng, entries)
|
|
if candidate not in used:
|
|
choice = candidate
|
|
break
|
|
if not choice:
|
|
choice = _choose_text(rng, entries)
|
|
used.add(choice)
|
|
expressions.append(f"{label} has {choice}")
|
|
return expressions
|
|
|
|
|
|
def _descriptor_detail_for_subject(subject: Any, descriptor_detail: Any) -> str:
|
|
detail = _normalize_descriptor_detail(descriptor_detail)
|
|
if detail != "auto":
|
|
return detail
|
|
return "compact" if str(subject or "").strip().lower() == "man" else "full"
|
|
|
|
|
|
def _descriptor_from_parts(
|
|
subject: Any,
|
|
age: Any,
|
|
body_phrase: Any,
|
|
skin: Any,
|
|
hair: Any,
|
|
eyes: Any,
|
|
descriptor_detail: Any = "auto",
|
|
) -> str:
|
|
subject = str(subject or "person").strip() or "person"
|
|
age_text = " ".join(str(age or "").strip().split())
|
|
age_text = age_text.removesuffix(" adults").removesuffix(" adult").strip()
|
|
if age_text in ("adult", "adults"):
|
|
age_text = ""
|
|
subject_phrase = f"{age_text} adult {subject}".strip() if age_text else f"adult {subject}"
|
|
detail = _descriptor_detail_for_subject(subject, descriptor_detail)
|
|
detail_map = {
|
|
"minimal": (body_phrase,),
|
|
"compact": (body_phrase, skin),
|
|
"medium": (body_phrase, skin, hair),
|
|
"full": (body_phrase, skin, hair, eyes),
|
|
}
|
|
pieces = [subject_phrase, *detail_map.get(detail, detail_map["full"])]
|
|
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
|
|
|
|
|
def _slot_manual_or_choice(choice: str, manual_value: str) -> str:
|
|
choice = str(choice or "").strip()
|
|
manual_value = str(manual_value or "").strip()
|
|
if choice == "manual":
|
|
return manual_value or "random"
|
|
if choice.lower() in CHARACTER_RANDOM_TOKENS:
|
|
return "random"
|
|
return choice
|
|
|
|
|
|
def _normalize_slot_ethnicity(value: Any) -> str:
|
|
text = str(value or "").strip()
|
|
if text.lower() in CHARACTER_RANDOM_TOKENS:
|
|
return "random"
|
|
if text == "any" or text in ETHNICITY_FILTER_CHOICES or "+" in text:
|
|
return text
|
|
return "random"
|
|
|
|
|
|
def _normalize_character_slot(slot: dict[str, Any]) -> dict[str, Any]:
|
|
subject_type = str(slot.get("subject_type") or slot.get("subject") or "").strip().lower()
|
|
if subject_type not in ("woman", "man"):
|
|
subject_type = "woman"
|
|
label = str(slot.get("label") or slot.get("label_mode") or "auto_chain").strip()
|
|
label = label.replace("Woman ", "").replace("Man ", "").strip().upper()
|
|
if label == "AUTO_CHAIN":
|
|
label = "auto_chain"
|
|
if label not in CHARACTER_LABEL_CHOICES:
|
|
label = "auto_chain"
|
|
|
|
age = _slot_manual_or_choice(str(slot.get("age") or "random"), str(slot.get("manual_age") or ""))
|
|
body = _slot_manual_or_choice(str(slot.get("body") or "random"), str(slot.get("manual_body") or ""))
|
|
figure = str(slot.get("figure") or "random").strip()
|
|
if figure not in character_figure_choices():
|
|
figure = "random"
|
|
|
|
normalized = {
|
|
"profile_type": "character_slot",
|
|
"subject_type": subject_type,
|
|
"label": label,
|
|
"age": age,
|
|
"ethnicity": _normalize_slot_ethnicity(slot.get("ethnicity")),
|
|
"figure": figure,
|
|
"body": body,
|
|
"body_phrase": _slot_value(slot.get("body_phrase")),
|
|
"skin": _slot_value(slot.get("skin")),
|
|
"hair": _slot_value(slot.get("hair")),
|
|
"eyes": _slot_value(slot.get("eyes")),
|
|
"descriptor_detail": _normalize_descriptor_detail(slot.get("descriptor_detail")),
|
|
"presence_mode": _normalize_presence_mode(slot.get("presence_mode"), subject_type),
|
|
"softcore_outfit": _slot_value(slot.get("softcore_outfit")),
|
|
"hardcore_clothing": _slot_value(slot.get("hardcore_clothing") or slot.get("hardcore_outfit")),
|
|
"expression_enabled": not _is_false(slot.get("expression_enabled", True)),
|
|
"expression_intensity": _normalize_slot_expression_intensity(slot.get("expression_intensity")),
|
|
"softcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("softcore_expression_intensity")),
|
|
"hardcore_expression_intensity": _normalize_slot_expression_intensity(slot.get("hardcore_expression_intensity")),
|
|
}
|
|
normalized["summary"] = _character_slot_summary(normalized)
|
|
return normalized
|
|
|
|
|
|
def _parse_character_cast(character_cast: str | dict[str, Any] | list[Any] | None) -> list[dict[str, Any]]:
|
|
if not character_cast:
|
|
return []
|
|
if isinstance(character_cast, list):
|
|
raw = character_cast
|
|
elif isinstance(character_cast, dict):
|
|
raw = character_cast
|
|
else:
|
|
try:
|
|
raw = json.loads(str(character_cast))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid character_cast JSON: {exc}") from exc
|
|
|
|
if isinstance(raw, list):
|
|
slots = raw
|
|
elif isinstance(raw, dict) and isinstance(raw.get("slots"), list):
|
|
slots = raw["slots"]
|
|
elif isinstance(raw, dict) and raw.get("profile_type") == "character_slot":
|
|
slots = [raw]
|
|
elif isinstance(raw, dict) and raw.get("subject_type") in ("woman", "man"):
|
|
slots = [raw]
|
|
else:
|
|
return []
|
|
return [_normalize_character_slot(slot) for slot in slots if isinstance(slot, dict)]
|
|
|
|
|
|
def _character_slot_summary(slot: dict[str, Any]) -> str:
|
|
subject = str(slot.get("subject_type") or "woman")
|
|
label = str(slot.get("label") or "auto_chain")
|
|
label_text = "nearest free label" if label == "auto_chain" else f"{subject.capitalize()} {label}"
|
|
parts = [
|
|
subject,
|
|
label_text,
|
|
f"age={slot.get('age', 'random')}",
|
|
f"ethnicity={slot.get('ethnicity', 'random')}",
|
|
f"figure={slot.get('figure', 'random')}",
|
|
f"body={slot.get('body', 'random')}",
|
|
f"detail={slot.get('descriptor_detail', 'auto')}",
|
|
]
|
|
if _slot_is_pov(slot):
|
|
parts.append("presence=pov")
|
|
if not _slot_expression_enabled(slot):
|
|
parts.append("expression=disabled")
|
|
else:
|
|
expression_intensity = _slot_expression_intensity(slot)
|
|
if expression_intensity is not None:
|
|
parts.append(f"expression={expression_intensity:.2f}")
|
|
softcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "softcore")
|
|
hardcore_expression_intensity = _slot_expression_intensity_for_phase(slot, "hardcore")
|
|
if softcore_expression_intensity is not None and softcore_expression_intensity != expression_intensity:
|
|
parts.append(f"soft_expr={softcore_expression_intensity:.2f}")
|
|
if hardcore_expression_intensity is not None and hardcore_expression_intensity != expression_intensity:
|
|
parts.append(f"hard_expr={hardcore_expression_intensity:.2f}")
|
|
if slot.get("softcore_outfit"):
|
|
parts.append(f"soft_outfit={slot['softcore_outfit']}")
|
|
if slot.get("hardcore_clothing"):
|
|
parts.append(f"hard_clothing={slot['hardcore_clothing']}")
|
|
for key in ("body_phrase", "skin", "hair", "eyes"):
|
|
value = slot.get(key)
|
|
if value:
|
|
parts.append(f"{key}={value}")
|
|
return "; ".join(parts)
|
|
|
|
|
|
def build_character_slot_json(
|
|
subject_type: str = "woman",
|
|
label: str = "auto_chain",
|
|
age: str = "random",
|
|
manual_age: str = "",
|
|
ethnicity: str = "random",
|
|
figure: str = "random",
|
|
body: str = "random",
|
|
manual_body: str = "",
|
|
body_phrase: str = "",
|
|
skin: str = "",
|
|
hair: str = "",
|
|
eyes: str = "",
|
|
descriptor_detail: str = "auto",
|
|
expression_enabled: bool = True,
|
|
expression_intensity: float = -1.0,
|
|
enabled: bool = True,
|
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
|
presence_mode: str = "visible",
|
|
softcore_expression_intensity: float = -1.0,
|
|
hardcore_expression_intensity: float = -1.0,
|
|
softcore_outfit: str = "",
|
|
hardcore_clothing: str = "",
|
|
) -> dict[str, str]:
|
|
existing_slots = _parse_character_cast(character_cast)
|
|
slot = _normalize_character_slot(
|
|
{
|
|
"subject_type": subject_type,
|
|
"label": label,
|
|
"age": age,
|
|
"manual_age": manual_age,
|
|
"ethnicity": ethnicity,
|
|
"figure": figure,
|
|
"body": body,
|
|
"manual_body": manual_body,
|
|
"body_phrase": body_phrase,
|
|
"skin": skin,
|
|
"hair": hair,
|
|
"eyes": eyes,
|
|
"descriptor_detail": descriptor_detail,
|
|
"presence_mode": presence_mode,
|
|
"softcore_outfit": softcore_outfit,
|
|
"hardcore_clothing": hardcore_clothing,
|
|
"expression_enabled": expression_enabled,
|
|
"expression_intensity": expression_intensity,
|
|
"softcore_expression_intensity": softcore_expression_intensity,
|
|
"hardcore_expression_intensity": hardcore_expression_intensity,
|
|
}
|
|
)
|
|
slots = existing_slots + ([slot] if enabled else [])
|
|
cast = {
|
|
"profile_type": "character_cast",
|
|
"version": 1,
|
|
"slots": slots,
|
|
}
|
|
return {
|
|
"character_cast": json.dumps(cast, ensure_ascii=True, sort_keys=True),
|
|
"character_slot": json.dumps(slot, ensure_ascii=True, sort_keys=True) if enabled else "",
|
|
"summary": slot["summary"] if enabled else "disabled",
|
|
"status": f"{len(slots)} slot(s)",
|
|
}
|
|
|
|
|
|
def _slot_explicit_label(slot: dict[str, Any]) -> str:
|
|
label = str(slot.get("label") or "").strip().upper()
|
|
if label in CHARACTER_LABEL_CHOICES and label != "AUTO_CHAIN":
|
|
return label
|
|
return ""
|
|
|
|
|
|
def _character_slot_label_map(slots: list[dict[str, Any]]) -> dict[str, dict[str, Any]]:
|
|
label_map: dict[str, dict[str, Any]] = {}
|
|
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
for subject_type, prefix in (("woman", "Woman"), ("man", "Man")):
|
|
subject_slots = [slot for slot in slots if slot.get("subject_type") == subject_type]
|
|
auto_slots = [slot for slot in subject_slots if not _slot_explicit_label(slot)]
|
|
for index, slot in enumerate(reversed(auto_slots)):
|
|
if index >= len(letters):
|
|
break
|
|
label_map[f"{prefix} {letters[index]}"] = slot
|
|
for slot in subject_slots:
|
|
explicit = _slot_explicit_label(slot)
|
|
if explicit:
|
|
label_map[f"{prefix} {explicit}"] = slot
|
|
return label_map
|
|
|
|
|
|
def _pov_character_labels(
|
|
label_map: dict[str, dict[str, Any]],
|
|
men_count: int | None = None,
|
|
) -> list[str]:
|
|
if men_count is None:
|
|
labels = sorted(label for label in label_map if label.startswith("Man "))
|
|
else:
|
|
labels = [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))]
|
|
return [label for label in labels if _slot_is_pov(label_map.get(label))]
|
|
|
|
|
|
def _pov_text_with_viewer(text: Any, pov_labels: list[str]) -> str:
|
|
rendered = str(text or "").strip()
|
|
if not rendered or not pov_labels:
|
|
return rendered
|
|
for label in sorted(pov_labels, key=len, reverse=True):
|
|
escaped = re.escape(label)
|
|
rendered = re.sub(rf"\b{escaped}'s\b", "the POV viewer's", rendered)
|
|
rendered = re.sub(rf"\b{escaped}\b", "the POV viewer", rendered)
|
|
rendered = re.sub(r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
|
|
return _clean_prompt_punctuation(rendered)
|
|
|
|
|
|
def _pov_role_graph_prompt(role_graph: Any, pov_labels: list[str]) -> str:
|
|
role_graph_text = str(role_graph or "").strip()
|
|
if not role_graph_text or not pov_labels:
|
|
return role_graph_text
|
|
viewer_text = _pov_text_with_viewer(role_graph_text, pov_labels)
|
|
label_text = ", ".join(pov_labels)
|
|
return f"First-person POV from {label_text}; {viewer_text}"
|
|
|
|
|
|
def _pov_prompt_directive(pov_labels: list[str]) -> str:
|
|
if not pov_labels:
|
|
return ""
|
|
label_text = ", ".join(pov_labels)
|
|
return (
|
|
f"POV participant: {label_text} is the first-person camera viewpoint; "
|
|
"he remains the off-camera viewpoint, represented by foreground hands, body position, or camera perspective cues when needed."
|
|
)
|
|
|
|
|
|
def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str:
|
|
text = str(composition or "").strip()
|
|
if not text or not pov_labels:
|
|
return text
|
|
text = re.sub(r"\ball participants visible\b", "visible partners readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\ball adult bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\ball bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\ball three bodies readable\b", "visible partner bodies readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\bwide group-sex composition\b", "first-person group-sex POV composition", text, flags=re.IGNORECASE)
|
|
if "pov" not in text.lower() and "first-person" not in text.lower():
|
|
text = f"{text}, adapted for first-person POV with the POV participant kept off-camera"
|
|
return _clean_prompt_punctuation(text)
|
|
|
|
|
|
def _body_exposure_scene_text(scene: Any) -> str:
|
|
text = str(scene or "").strip()
|
|
if not text:
|
|
return ""
|
|
replacements = (
|
|
(r",?\s*\bscattered (?:clothes|clothing)\b", ""),
|
|
(r",?\s*\bfloor clothes\b", ""),
|
|
(r"\bclothes scattered\b", "soft floor shadows"),
|
|
(r",?\s*\bscattered lingerie\b", ""),
|
|
(r",?\s*\blingerie visible nearby\b", ""),
|
|
(r"\boutfit racks\b", "mirror shelves"),
|
|
(r"\bcostume racks\b", "mirror shelves"),
|
|
(r"\bhanging outfits\b", "hanging fabric"),
|
|
(r"\bclothing hooks\b", "wall hooks"),
|
|
(r"\boutfit-check\b", "creator-shot"),
|
|
(r"\boutfit framing\b", "body framing"),
|
|
(r"\bfull outfits\b", "full bodies"),
|
|
(r"\bcoordinated outfits\b", "coordinated posing"),
|
|
)
|
|
for pattern, replacement in replacements:
|
|
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\bwith,\s*", "with ", text, flags=re.IGNORECASE)
|
|
text = re.sub(r",\s*,", ",", text)
|
|
return _clean_prompt_punctuation(text)
|
|
|
|
|
|
def _slot_softcore_outfit(slot: dict[str, Any] | None) -> str:
|
|
return _slot_value(slot.get("softcore_outfit")) if slot else ""
|
|
|
|
|
|
def _slot_hardcore_clothing(slot: dict[str, Any] | None) -> str:
|
|
return _slot_value(slot.get("hardcore_clothing")) if slot else ""
|
|
|
|
|
|
def _softcore_outfit_sentence(label: str, outfit: str) -> str:
|
|
outfit = str(outfit or "").strip()
|
|
if not outfit:
|
|
return ""
|
|
lower = outfit.lower()
|
|
if lower.startswith(("wears ", "wearing ", "in ")):
|
|
return f"{label} {outfit}"
|
|
return f"{label} wears {outfit}"
|
|
|
|
|
|
def _hardcore_clothing_sentence(label: str, clothing: str) -> str:
|
|
clothing = str(clothing or "").strip().rstrip(".")
|
|
if not clothing:
|
|
return ""
|
|
lower = clothing.lower()
|
|
if lower.startswith(("fully nude", "nude")):
|
|
return f"{label}'s body is fully exposed, bare skin unobstructed"
|
|
if lower.startswith("partly nude"):
|
|
return f"{label}'s body is partly exposed"
|
|
if lower.startswith(("is ", "wears ", "wearing ", "keeps ", "has ", "with ")):
|
|
return f"{label} {clothing}"
|
|
return f"{label}'s clothing: {clothing}"
|
|
|
|
|
|
def _character_hardcore_clothing_entries(
|
|
label_map: dict[str, dict[str, Any]],
|
|
women_count: int,
|
|
men_count: int,
|
|
pov_labels: list[str] | None = None,
|
|
) -> list[str]:
|
|
pov_set = set(pov_labels or [])
|
|
labels = [
|
|
*[f"Woman {chr(ord('A') + index)}" for index in range(max(0, women_count))],
|
|
*[f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))],
|
|
]
|
|
entries: list[str] = []
|
|
for label in labels:
|
|
if label in pov_set:
|
|
continue
|
|
clothing = _slot_hardcore_clothing(label_map.get(label))
|
|
sentence = _hardcore_clothing_sentence(label, clothing)
|
|
if sentence:
|
|
entries.append(sentence)
|
|
return entries
|
|
|
|
|
|
def _context_from_character_slot(
|
|
rng: random.Random,
|
|
slot: dict[str, Any],
|
|
subject_type: str,
|
|
ethnicity: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
) -> dict[str, str]:
|
|
slot_ethnicity = _slot_value(slot.get("ethnicity"))
|
|
slot_figure = _slot_value(slot.get("figure"))
|
|
slot_body = _slot_value(slot.get("body"))
|
|
effective_ethnicity = slot_ethnicity or ethnicity
|
|
effective_figure = slot_figure if slot_figure in ("curvy", "balanced", "bombshell") else figure
|
|
effective_no_plus = bool(no_plus_women) and not slot_body
|
|
effective_no_black = bool(no_black) and not slot_ethnicity
|
|
context = _appearance_for_subject(
|
|
rng,
|
|
subject_type,
|
|
effective_ethnicity,
|
|
effective_figure,
|
|
effective_no_plus,
|
|
effective_no_black,
|
|
)
|
|
|
|
age = _slot_value(slot.get("age"))
|
|
body_phrase = _slot_value(slot.get("body_phrase"))
|
|
if age:
|
|
context["age"] = age
|
|
if slot_body:
|
|
context["body"] = slot_body
|
|
if subject_type == "woman":
|
|
context["body_phrase"] = _body_phrase(slot_body, context.get("figure", ""))
|
|
else:
|
|
context["body_phrase"] = f"{slot_body} figure"
|
|
if body_phrase:
|
|
context["body_phrase"] = body_phrase
|
|
for key in ("skin", "hair", "eyes"):
|
|
value = _slot_value(slot.get(key))
|
|
if value:
|
|
context[key] = value
|
|
context["descriptor_detail"] = _normalize_descriptor_detail(slot.get("descriptor_detail"))
|
|
context["presence_mode"] = _normalize_presence_mode(slot.get("presence_mode"), subject_type)
|
|
context["expression_enabled"] = _slot_expression_enabled(slot)
|
|
expression_intensity = _slot_expression_intensity(slot)
|
|
if expression_intensity is not None:
|
|
context["expression_intensity"] = expression_intensity
|
|
context["subject_type"] = subject_type
|
|
context["subject"] = subject_type
|
|
context["subject_phrase"] = subject_type
|
|
return context
|
|
|
|
|
|
def _character_context_for_label(
|
|
label: str,
|
|
label_map: dict[str, dict[str, Any]],
|
|
rng: random.Random,
|
|
ethnicity: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
) -> tuple[dict[str, str], dict[str, Any] | None]:
|
|
subject_type = "man" if label.startswith("Man ") else "woman"
|
|
slot = label_map.get(label)
|
|
if slot:
|
|
return _context_from_character_slot(rng, slot, subject_type, ethnicity, figure, no_plus_women, no_black), slot
|
|
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black), None
|
|
|
|
|
|
def _apply_character_context_to_row(row: dict[str, Any], context: dict[str, Any]) -> dict[str, Any]:
|
|
for key in (
|
|
"subject_type",
|
|
"subject",
|
|
"subject_phrase",
|
|
"age",
|
|
"body",
|
|
"body_phrase",
|
|
"skin",
|
|
"hair",
|
|
"eyes",
|
|
"figure",
|
|
"descriptor_detail",
|
|
"presence_mode",
|
|
"expression_enabled",
|
|
"expression_intensity",
|
|
):
|
|
value = context.get(key)
|
|
if value is not None and value != "":
|
|
row[key] = value
|
|
if context.get("age"):
|
|
row["age_band"] = context["age"]
|
|
return row
|
|
|
|
|
|
def _cast_descriptor_entries(
|
|
seed_config: dict[str, int],
|
|
seed: int,
|
|
row_number: int,
|
|
ethnicity: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
women_count: int,
|
|
men_count: int,
|
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
|
primary_descriptor: str = "",
|
|
) -> tuple[list[str], list[dict[str, Any]]]:
|
|
slots = _parse_character_cast(character_cast)
|
|
label_map = _character_slot_label_map(slots)
|
|
rng = _axis_rng(seed_config, "person", seed, row_number + 997)
|
|
descriptors: list[str] = []
|
|
for index in range(max(0, women_count)):
|
|
label = f"Woman {chr(ord('A') + index)}"
|
|
if index == 0 and primary_descriptor:
|
|
descriptors.append(f"Woman A / primary creator: {primary_descriptor}")
|
|
continue
|
|
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
|
|
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
|
|
for index in range(max(0, men_count)):
|
|
label = f"Man {chr(ord('A') + index)}"
|
|
if _slot_is_pov(label_map.get(label)):
|
|
continue
|
|
context, _slot = _character_context_for_label(label, label_map, rng, ethnicity, figure, no_plus_women, no_black)
|
|
descriptors.append(f"{label}: {_insta_of_descriptor_from_context(context)}")
|
|
return descriptors, slots
|
|
|
|
|
|
def _row_from_profile_metadata(metadata_json: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
row = _load_json_object(metadata_json, "metadata_json")
|
|
if isinstance(row.get("softcore_row"), dict):
|
|
return row["softcore_row"]
|
|
return row
|
|
|
|
|
|
def _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 = "",
|
|
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 == "metadata_json":
|
|
row = _row_from_profile_metadata(metadata_json)
|
|
raw_profile = {
|
|
"profile_name": profile_name,
|
|
"subject_type": row.get("subject_type") or row.get("primary_subject") or subject_type,
|
|
"age": row.get("age") or row.get("age_band") or age,
|
|
"body": row.get("body") or row.get("body_type") or body,
|
|
"body_phrase": row.get("body_phrase") or body_phrase,
|
|
"skin": row.get("skin") or skin,
|
|
"hair": row.get("hair") or hair,
|
|
"eyes": row.get("eyes") or eyes,
|
|
"figure": row.get("figure") or figure,
|
|
"descriptor_detail": row.get("descriptor_detail") or "auto",
|
|
}
|
|
else:
|
|
raw_profile = {
|
|
"profile_name": profile_name,
|
|
"subject_type": subject_type,
|
|
"age": age,
|
|
"body": body,
|
|
"body_phrase": body_phrase,
|
|
"skin": skin,
|
|
"hair": hair,
|
|
"eyes": eyes,
|
|
"figure": figure,
|
|
"descriptor_detail": "auto",
|
|
}
|
|
profile = _normalize_character_profile(raw_profile, profile_name)
|
|
saved_path = ""
|
|
status = "not_saved"
|
|
if save_now:
|
|
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
|
path = _profile_path(profile["profile_name"])
|
|
path.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
saved_path = str(path)
|
|
status = "saved"
|
|
return {
|
|
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
|
"profile_name": profile["profile_name"],
|
|
"descriptor": profile["descriptor"],
|
|
"saved_path": saved_path,
|
|
"status": status,
|
|
}
|
|
|
|
|
|
def _empty_profile_result(status: str = "empty") -> dict[str, str]:
|
|
return {
|
|
"profile_json": "",
|
|
"profile_name": "",
|
|
"descriptor": "",
|
|
"saved_path": "",
|
|
"status": status,
|
|
}
|
|
|
|
|
|
def load_character_profile_json(
|
|
profile_name: str = "",
|
|
fallback_profile_json: str | dict[str, Any] | None = "",
|
|
enabled: bool = True,
|
|
delete_now: bool = False,
|
|
rename_now: bool = False,
|
|
rename_to: str = "",
|
|
) -> dict[str, str]:
|
|
if not enabled:
|
|
return _empty_profile_result("disabled")
|
|
if delete_now and rename_now:
|
|
return _empty_profile_result("choose_delete_or_rename")
|
|
|
|
raw_profile = _load_json_object(fallback_profile_json, "fallback_profile_json")
|
|
saved_path = ""
|
|
if profile_name and profile_name != "manual":
|
|
path = _profile_path(profile_name)
|
|
if delete_now:
|
|
if path.exists():
|
|
path.unlink()
|
|
return _empty_profile_result(f"deleted:{path.stem}")
|
|
return _empty_profile_result(f"delete_missing:{_safe_profile_name(profile_name)}")
|
|
if rename_now:
|
|
new_name = _safe_profile_name(rename_to)
|
|
if not rename_to.strip():
|
|
return _empty_profile_result("rename_missing_name")
|
|
if not path.exists():
|
|
return _empty_profile_result(f"rename_missing:{_safe_profile_name(profile_name)}")
|
|
target = _profile_path(new_name)
|
|
if target.exists() and target != path:
|
|
return _empty_profile_result(f"rename_target_exists:{target.stem}")
|
|
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
|
profile = _normalize_character_profile(raw_profile, new_name)
|
|
target.write_text(json.dumps(profile, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
if target != path:
|
|
path.unlink()
|
|
return {
|
|
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
|
"profile_name": profile["profile_name"],
|
|
"descriptor": profile["descriptor"],
|
|
"saved_path": str(target),
|
|
"status": f"renamed:{path.stem}->{target.stem}",
|
|
}
|
|
if path.exists():
|
|
raw_profile = _load_json_object(path.read_text(encoding="utf-8"), "character_profile")
|
|
saved_path = str(path)
|
|
if not raw_profile:
|
|
return _empty_profile_result("empty")
|
|
profile = _normalize_character_profile(raw_profile, profile_name or raw_profile.get("profile_name", ""))
|
|
return {
|
|
"profile_json": json.dumps(profile, ensure_ascii=True, sort_keys=True),
|
|
"profile_name": profile["profile_name"],
|
|
"descriptor": profile["descriptor"],
|
|
"saved_path": saved_path,
|
|
"status": "loaded" if saved_path else "fallback",
|
|
}
|
|
|
|
|
|
def _parse_character_profile(character_profile: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
raw = _load_json_object(character_profile, "character_profile")
|
|
if not raw:
|
|
return {}
|
|
if raw.get("profile_type") == "character" or any(key in raw for key in ("age", "age_band", "skin", "hair", "eyes")):
|
|
return _normalize_character_profile(raw, str(raw.get("profile_name") or ""))
|
|
return {}
|
|
|
|
|
|
def _apply_character_profile_to_context(
|
|
context: dict[str, Any],
|
|
character_profile: str | dict[str, Any] | None,
|
|
) -> tuple[dict[str, Any], dict[str, Any], str]:
|
|
profile = _parse_character_profile(character_profile)
|
|
if not profile:
|
|
return context, {}, "none"
|
|
if context.get("subject_type") not in ("woman", "man"):
|
|
return context, profile, "skipped_non_single_subject"
|
|
if profile["subject_type"] != context.get("subject_type"):
|
|
return context, profile, "skipped_subject_mismatch"
|
|
updated = dict(context)
|
|
for key in (
|
|
"subject_type",
|
|
"subject",
|
|
"subject_phrase",
|
|
"age",
|
|
"body",
|
|
"body_phrase",
|
|
"skin",
|
|
"hair",
|
|
"eyes",
|
|
"figure",
|
|
"descriptor_detail",
|
|
):
|
|
value = profile.get(key)
|
|
if value:
|
|
updated[key] = value
|
|
updated["subject"] = profile["subject_type"]
|
|
updated["subject_phrase"] = profile["subject_type"]
|
|
return updated, profile, "applied"
|
|
|
|
|
|
def _composition_prompt(composition: str) -> str:
|
|
composition = str(composition or "").strip()
|
|
if not composition:
|
|
return composition
|
|
lower = composition.lower()
|
|
if lower.startswith("vertical ") or " vertical " in lower or lower.endswith(" vertical"):
|
|
return composition
|
|
return f"vertical {composition}"
|
|
|
|
|
|
def _appearance_for_subject(
|
|
rng: random.Random,
|
|
subject_type: str,
|
|
ethnicity: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
) -> dict[str, str]:
|
|
if subject_type == "single_any":
|
|
subject_type = "woman" if rng.random() < 0.82 else "man"
|
|
|
|
if subject_type == "man":
|
|
men_ethnicity = ethnicity if ethnicity else "any"
|
|
subject, age, body, skin, hair, eyes = g.choose(rng, g.by_ethnicity(g.MEN, men_ethnicity))
|
|
return {
|
|
"subject_type": "man",
|
|
"subject": subject,
|
|
"subject_phrase": subject,
|
|
"age": age,
|
|
"body": body,
|
|
"skin": skin,
|
|
"hair": hair,
|
|
"eyes": eyes,
|
|
"body_phrase": f"{body} figure",
|
|
}
|
|
|
|
subject, age, body, skin, hair, eyes = g.choose_woman(rng, ethnicity, no_plus_women, no_black)
|
|
figure_note = g.choose(rng, g.figure_pool(figure))
|
|
return {
|
|
"subject_type": "woman",
|
|
"subject": subject,
|
|
"subject_phrase": subject,
|
|
"age": age,
|
|
"body": body,
|
|
"skin": skin,
|
|
"hair": hair,
|
|
"eyes": eyes,
|
|
"body_phrase": _body_phrase(body, figure_note),
|
|
"figure": figure_note,
|
|
}
|
|
|
|
|
|
def _count_phrase(count: int, singular: str, plural: str) -> str:
|
|
words = {
|
|
0: "no",
|
|
1: "one",
|
|
2: "two",
|
|
3: "three",
|
|
4: "four",
|
|
5: "five",
|
|
6: "six",
|
|
7: "seven",
|
|
8: "eight",
|
|
9: "nine",
|
|
10: "ten",
|
|
11: "eleven",
|
|
12: "twelve",
|
|
}
|
|
label = singular if count == 1 else plural
|
|
return f"{words.get(count, str(count))} {label}"
|
|
|
|
|
|
def _configured_cast_context(women_count: int, men_count: int) -> dict[str, str]:
|
|
women_count = max(0, int(women_count))
|
|
men_count = max(0, int(men_count))
|
|
if women_count + men_count == 0:
|
|
women_count = 1
|
|
parts = []
|
|
if women_count:
|
|
parts.append(_count_phrase(women_count, "adult woman", "adult women"))
|
|
if men_count:
|
|
parts.append(_count_phrase(men_count, "adult man", "adult men"))
|
|
if len(parts) == 1:
|
|
subject_phrase = parts[0]
|
|
else:
|
|
subject_phrase = f"{parts[0]} and {parts[1]}"
|
|
person_count = women_count + men_count
|
|
if person_count == 1:
|
|
scene_kind = "solo adult sexual pose"
|
|
elif person_count == 2:
|
|
scene_kind = "adult couple sex scene"
|
|
elif person_count == 3:
|
|
scene_kind = "adult threesome sex scene"
|
|
else:
|
|
scene_kind = "adult group sex scene"
|
|
women_label = "woman" if women_count == 1 else "women"
|
|
men_label = "man" if men_count == 1 else "men"
|
|
cast_summary = f"{women_count} {women_label}, {men_count} {men_label}, {person_count} total adults"
|
|
return {
|
|
"subject_type": "configured_cast",
|
|
"subject": f"{women_count}w_{men_count}m_sex_scene",
|
|
"subject_phrase": subject_phrase,
|
|
"age": "21+ adults",
|
|
"body": "varied",
|
|
"skin": "",
|
|
"hair": "",
|
|
"eyes": "",
|
|
"body_phrase": "varied adult bodies",
|
|
"women_count": str(women_count),
|
|
"men_count": str(men_count),
|
|
"person_count": str(person_count),
|
|
"cast_summary": cast_summary,
|
|
"scene_kind": scene_kind,
|
|
}
|
|
|
|
|
|
def _couple_type_from_counts(
|
|
rng: random.Random,
|
|
women_count: int,
|
|
men_count: int,
|
|
) -> tuple[str, str, str, int, int]:
|
|
women_count = max(0, int(women_count))
|
|
men_count = max(0, int(men_count))
|
|
if women_count >= 2 and men_count == 0:
|
|
return "two women", "two women", "close affectionate couple pose", 2, 0
|
|
if men_count >= 2 and women_count == 0:
|
|
return "two men", "two men", "relaxed romantic couple pose", 0, 2
|
|
if women_count >= 1 and men_count >= 1:
|
|
return "woman and man", "a woman and a man", "playful date-night pose", 1, 1
|
|
|
|
primary_subject, subject_phrase, pose = g.choose(rng, g.COUPLE_TYPES)
|
|
if primary_subject == "two women":
|
|
return primary_subject, subject_phrase, pose, 2, 0
|
|
if primary_subject == "two men":
|
|
return primary_subject, subject_phrase, pose, 0, 2
|
|
return primary_subject, subject_phrase, pose, 1, 1
|
|
|
|
|
|
def _lettered(prefix: str, count: int) -> list[str]:
|
|
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
return [f"{prefix.capitalize()} {letters[index]}" for index in range(max(0, count))]
|
|
|
|
|
|
def _pick_distinct(rng: random.Random, items: list[str], count: int) -> list[str]:
|
|
if not items:
|
|
return []
|
|
if len(items) >= count:
|
|
return rng.sample(items, count)
|
|
picked = list(items)
|
|
while len(picked) < count:
|
|
picked.append(items[rng.randrange(len(items))])
|
|
return picked
|
|
|
|
|
|
def _participant_context(women_count: int, men_count: int) -> dict[str, list[str]]:
|
|
women = _lettered("woman", women_count)
|
|
men = _lettered("man", men_count)
|
|
return {"women": women, "men": men, "people": women + men}
|
|
|
|
|
|
def _role_graph(
|
|
rng: random.Random,
|
|
subcategory: dict[str, Any],
|
|
context: dict[str, str],
|
|
item_axis_values: dict[str, str] | None = None,
|
|
) -> str:
|
|
if context.get("subject_type") != "configured_cast":
|
|
return ""
|
|
women_count = int(context.get("women_count") or 0)
|
|
men_count = int(context.get("men_count") or 0)
|
|
people_count = women_count + men_count
|
|
if people_count <= 0:
|
|
return ""
|
|
|
|
participants = _participant_context(women_count, men_count)
|
|
women = participants["women"]
|
|
men = participants["men"]
|
|
people = participants["people"]
|
|
slug = str(subcategory.get("slug") or subcategory.get("name") or "").lower()
|
|
item_text = " ".join((item_axis_values or {}).values()).lower()
|
|
|
|
def any_person(exclude: set[str] | None = None) -> str:
|
|
exclude = exclude or set()
|
|
pool = [person for person in people if person not in exclude] or people
|
|
return rng.choice(pool)
|
|
|
|
def any_woman(exclude: set[str] | None = None) -> str:
|
|
exclude = exclude or set()
|
|
pool = [person for person in women if person not in exclude] or [person for person in people if person not in exclude] or people
|
|
return rng.choice(pool)
|
|
|
|
def any_man(exclude: set[str] | None = None) -> str:
|
|
exclude = exclude or set()
|
|
pool = [person for person in men if person not in exclude] or [person for person in people if person not in exclude] or people
|
|
return rng.choice(pool)
|
|
|
|
def support_sentence(exclude: set[str]) -> str:
|
|
extras = [person for person in people if person not in exclude]
|
|
if not extras:
|
|
return ""
|
|
extra = rng.choice(extras)
|
|
actions = [
|
|
"kisses and grips the nearest body",
|
|
"holds hips open for the camera",
|
|
"touches breasts, thighs, and stomach",
|
|
"keeps one hand on a partner's ass",
|
|
"watches close and joins the body contact",
|
|
"presses in from the side with hands on skin",
|
|
]
|
|
return f" {extra} {rng.choice(actions)}."
|
|
|
|
def mentions_ass(text: str) -> bool:
|
|
return bool(
|
|
re.search(
|
|
r"\bass\b|ass[- ](?:up|raised|exposed|lifted)|spread cheeks|lower back and ass|cum (?:on|dripping from) ass|pussy, ass|ass and",
|
|
text,
|
|
)
|
|
)
|
|
|
|
def climax_position_graph(woman: str, man: str, third: str = "") -> str:
|
|
if "lying between two partners" in item_text and third:
|
|
return f"{woman} lies between {man} and {third}, with {man} under her hips and {third} positioned above her torso as visible semen lands on her body."
|
|
if "held between front-and-back partners" in item_text and third:
|
|
return f"{woman} is held between {man} behind her and {third} in front of her as visible semen lands across her body."
|
|
if "kneeling between standing partners" in item_text and third:
|
|
return f"{woman} kneels between {man} and {third} while both stand close around her face and torso for visible ejaculation."
|
|
if "side-lying with thighs parted" in item_text:
|
|
return f"{woman} lies on her side with thighs parted while {man} kneels beside her hips and ejaculates semen across her thighs and pussy."
|
|
if "sitting on the edge of the bed" in item_text:
|
|
return f"{woman} sits on the edge of the bed with knees spread while {man} stands close between her legs and ejaculates semen across her body."
|
|
if "lying at the bed edge with thighs open" in item_text:
|
|
return f"{woman} lies at the bed edge with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs."
|
|
if "reclining with thighs open" in item_text or "lying on the back with legs spread" in item_text:
|
|
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs."
|
|
if "on all fours with hips raised" in item_text:
|
|
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and ejaculates semen across her ass, thighs, and lower back."
|
|
if "face-down ass-up" in item_text:
|
|
return f"{woman} lies face-down with ass raised while {man} is positioned behind her and ejaculates semen across her lower back and ass."
|
|
if "bent over with ass raised" in item_text or "bent over" in item_text:
|
|
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs."
|
|
if "kneeling with mouth open" in item_text:
|
|
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
|
|
if "kneeling in front of a standing partner" in item_text:
|
|
return f"{woman} kneels in front of {man} at hip height while {man} stands over her for visible ejaculation."
|
|
if "standing with cum on the body" in item_text:
|
|
return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body."
|
|
if "squatting on top of a partner" in item_text:
|
|
return f"{woman} squats over {man}'s hips while {man} lies on his back under her and ejaculates semen onto her body."
|
|
if "reverse cowgirl over a partner's hips" in item_text:
|
|
return f"{woman} straddles {man}'s hips facing away while {man} lies on his back under her and ejaculates semen onto her body."
|
|
if any(term in item_text for term in ("straddling a partner", "straddling a partner's hips", "shared climax after penetration", "orgasm during penetration")):
|
|
return f"{woman} straddles {man}'s hips while {man} lies on his back under her, their bodies still aligned from penetration as he ejaculates semen onto her body."
|
|
if "seated in a partner's lap facing them" in item_text:
|
|
return f"{woman} sits in {man}'s lap facing him, legs wrapped around his hips as he ejaculates semen across her body."
|
|
if any(term in item_text for term in ("lower back", "cum dripping from ass", "cum on lower back")) or mentions_ass(item_text):
|
|
return f"{woman} is bent forward with hips raised while {man} is positioned behind her, visible semen across her lower back, ass, and thighs."
|
|
if any(term in item_text for term in ("cum on face", "cum on tongue", "cum on lips", "cum on face and lips", "cum on tongue and chin")):
|
|
if third:
|
|
return f"{woman} kneels in the center while {man} and {third} stand close around her face and torso for visible ejaculation."
|
|
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
|
|
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen onto her body."
|
|
|
|
def penetration_position_graph(woman: str, man: str) -> str:
|
|
text = " ".join(
|
|
str(part or "").lower()
|
|
for part in (
|
|
item_text,
|
|
*((item_axis_values or {}).values()),
|
|
)
|
|
)
|
|
if "missionary" in text:
|
|
return f"{woman} lies on her back with legs open while {man} is above her and {man}'s penis thrusts into her."
|
|
if "reverse cowgirl" in text:
|
|
return f"{woman} straddles {man}'s hips facing away while {man} lies under her and {man}'s penis thrusts into her."
|
|
if "cowgirl" in text or "straddling" in text:
|
|
return f"{woman} straddles {man}'s hips facing him while {man} lies under her and {man}'s penis thrusts into her."
|
|
if "doggy" in text or "rear-entry" in text or "bent-over" in text or "bent over" in text:
|
|
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and {man}'s penis thrusts into her."
|
|
if "standing" in text:
|
|
return f"{woman} stands braced with hips angled back while {man} stands behind her and {man}'s penis thrusts into her."
|
|
if "spooning" in text or "side-lying" in text:
|
|
return f"{woman} lies on her side with thighs parted while {man} presses behind her and {man}'s penis thrusts into her."
|
|
if "edge-of-bed" in text or "edge of bed" in text or "bed edge" in text:
|
|
return f"{woman} lies at the bed edge with hips near the edge while {man} kneels between her legs and {man}'s penis thrusts into her."
|
|
if "kneeling straddle" in text:
|
|
return f"{woman} kneels straddling {man}'s hips while {man} supports her waist and {man}'s penis thrusts into her."
|
|
if "lotus" in text:
|
|
return f"{woman} sits in {man}'s lap facing him with legs around his hips while {man}'s penis thrusts into her."
|
|
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and {man}'s penis thrusts into her."
|
|
|
|
if people_count == 1:
|
|
solo = people[0]
|
|
if women_count == 1:
|
|
if "cumshot" in slug or "climax" in slug:
|
|
return f"{solo} is shown in a solo explicit orgasm pose with thighs open, one hand on her body, and visible arousal on skin and sheets."
|
|
return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness."
|
|
if "cumshot" in slug or "climax" in slug:
|
|
return f"{solo} is shown in a solo visible ejaculation pose with one hand on his penis, body angled toward the camera, and semen visible."
|
|
return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing."
|
|
|
|
if women_count > 0 and men_count == 0:
|
|
a, b = _pick_distinct(rng, women, 2)
|
|
c = any_woman({a, b}) if len(women) >= 3 else ""
|
|
used = {a, b}
|
|
if "oral" in slug:
|
|
graph = f"{a} kneels between {b}'s spread thighs and uses tongue and fingers on her pussy."
|
|
elif "anal" in slug or "double" in slug:
|
|
graph = f"{a} uses a strap-on on {b} while keeping her hips held open."
|
|
elif "threesome" in slug or "group" in slug or "orgy" in slug:
|
|
helper = c or any_woman({a})
|
|
graph = f"{a} uses a strap-on on {b} while {helper} gives oral contact and touches both bodies."
|
|
used.add(helper)
|
|
elif "cumshot" in slug or "climax" in slug:
|
|
graph = f"{a} brings {b} to orgasm with mouth and fingers while wetness is visible on thighs and sheets."
|
|
else:
|
|
graph = f"{a} uses a strap-on on {b} while their bodies stay pressed together."
|
|
return graph + support_sentence(used)
|
|
|
|
if men_count > 0 and women_count == 0:
|
|
a, b = _pick_distinct(rng, men, 2)
|
|
c = any_man({a, b}) if len(men) >= 3 else ""
|
|
used = {a, b}
|
|
if "oral" in slug:
|
|
graph = f"{a} kneels and takes {b}'s penis in his mouth while holding his hips."
|
|
elif "anal" in slug or "double" in slug or "penetrative" in slug:
|
|
graph = f"{a} penetrates {b} anally while {b}'s hips are held open."
|
|
elif "threesome" in slug or "group" in slug or "orgy" in slug:
|
|
helper = c or any_man({a})
|
|
graph = f"{a} penetrates {b} anally while {helper} gives oral contact from the front."
|
|
used.add(helper)
|
|
elif "cumshot" in slug or "climax" in slug:
|
|
graph = f"{a} ejaculates semen over {b}'s body while {b} keeps eye contact and one hand on his penis."
|
|
else:
|
|
graph = f"{a} and {b} keep explicit penis and anal contact visible."
|
|
return graph + support_sentence(used)
|
|
|
|
# Mixed cast.
|
|
woman = any_woman()
|
|
man = any_man()
|
|
third = any_person({woman, man}) if people_count >= 3 else ""
|
|
if "oral" in slug:
|
|
if "sixty-nine" in item_text or ("blowjob" in item_text and ("cunnilingus" in item_text or "pussy" in item_text)):
|
|
graph = f"{woman} has {man}'s penis in her mouth while {man} uses his mouth on {woman}'s pussy, with both mouths pressed to genitals."
|
|
elif any(
|
|
term in item_text
|
|
for term in (
|
|
"cunnilingus",
|
|
"pussy licking",
|
|
"tongue on pussy",
|
|
"mouth on pussy",
|
|
"pussy and tongue",
|
|
"tongue contact",
|
|
)
|
|
) or ("pussy" in item_text and "penis" not in item_text):
|
|
graph = f"{man} gives oral to {woman}, mouth on her pussy while {woman}'s thighs are held open for the camera."
|
|
else:
|
|
graph = f"{woman} takes {man}'s penis in her mouth while {man} holds her hair and hips."
|
|
elif "anal" in slug or "double" in slug:
|
|
if "double" in item_text or "toy" in item_text:
|
|
if people_count >= 3:
|
|
graph = f"{man} thrusts his penis into {woman} while {third} adds a second penetration point from the front."
|
|
else:
|
|
graph = f"{man} thrusts his penis into {woman} while a toy adds a second penetration point."
|
|
elif people_count >= 3:
|
|
graph = f"{man} thrusts his penis into {woman} while {third} gives oral contact from the front."
|
|
else:
|
|
graph = f"{man} thrusts his penis into {woman}'s ass while keeping her hips held open."
|
|
elif "threesome" in slug:
|
|
graph = f"{man} thrusts his penis into {woman} while {third or any_person({woman, man})} uses mouth and hands on the exposed body."
|
|
elif "group" in slug or "orgy" in slug:
|
|
graph = f"{man} thrusts his penis into {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs."
|
|
elif "cumshot" in slug or "climax" in slug:
|
|
graph = climax_position_graph(woman, man, third)
|
|
else:
|
|
graph = penetration_position_graph(woman, man)
|
|
return graph + support_sentence({woman, man, third} if third else {woman, man})
|
|
|
|
|
|
def _subject_context(
|
|
rng: random.Random,
|
|
subject_type: str,
|
|
ethnicity: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
women_count: int = 1,
|
|
men_count: int = 1,
|
|
) -> dict[str, str]:
|
|
if subject_type in ("woman", "man", "single_any"):
|
|
return _appearance_for_subject(rng, subject_type, ethnicity, figure, no_plus_women, no_black)
|
|
|
|
if subject_type == "configured_cast":
|
|
return _configured_cast_context(women_count, men_count)
|
|
|
|
if subject_type == "couple":
|
|
primary_subject, subject_phrase, pose, effective_women_count, effective_men_count = _couple_type_from_counts(
|
|
rng,
|
|
women_count,
|
|
men_count,
|
|
)
|
|
return {
|
|
"subject_type": "couple",
|
|
"subject": primary_subject,
|
|
"subject_phrase": subject_phrase,
|
|
"age": g.choose(rng, g.COUPLE_AGES),
|
|
"body": g.choose(rng, ["slim and average", "curvy and broad", "stocky and curvy", "average and athletic"]),
|
|
"skin": "",
|
|
"hair": "",
|
|
"eyes": "",
|
|
"body_phrase": "",
|
|
"fallback_pose": pose,
|
|
"women_count": str(effective_women_count),
|
|
"men_count": str(effective_men_count),
|
|
"person_count": "2",
|
|
}
|
|
|
|
if subject_type == "group":
|
|
eth = "Asian " if ethnicity == "asian" else ""
|
|
return {
|
|
"subject_type": "group",
|
|
"subject": f"mixed {eth}adult group",
|
|
"subject_phrase": f"A mixed {eth}adult group of women and men",
|
|
"age": g.choose(rng, g.GROUP_AGES),
|
|
"body": "diverse",
|
|
"skin": "",
|
|
"hair": "",
|
|
"eyes": "",
|
|
"body_phrase": "diverse adult body types",
|
|
}
|
|
|
|
return {
|
|
"subject_type": subject_type,
|
|
"subject": "layout scene",
|
|
"subject_phrase": "Adult layout scene",
|
|
"age": "adult",
|
|
"body": "varied",
|
|
"skin": "",
|
|
"hair": "",
|
|
"eyes": "",
|
|
"body_phrase": "varied adult figures",
|
|
}
|
|
|
|
|
|
def _scene_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str) -> list[Any]:
|
|
fallback = g.GROUP_SCENES if subject_type in ("group", "configured_cast") else g.SCENES
|
|
scene_entries: list[Any] = []
|
|
scene_pools = load_scene_pool_library()
|
|
item_source = item if isinstance(item, dict) else None
|
|
if item_source is not None and _is_false(item_source.get("inherit_scenes")):
|
|
sources = (item_source,)
|
|
elif _is_false(subcategory.get("inherit_scenes")):
|
|
sources = (subcategory, item_source)
|
|
else:
|
|
sources = (category, subcategory, item_source)
|
|
for source in sources:
|
|
if not isinstance(source, dict):
|
|
continue
|
|
if "scenes" in source:
|
|
_unique_extend(scene_entries, _list_from(source["scenes"]))
|
|
refs = _list_from(source.get("scene_pool")) + _list_from(source.get("scene_pools"))
|
|
for ref in refs:
|
|
ref_name = str(ref).strip()
|
|
if ref_name not in scene_pools:
|
|
raise ValueError(f"Unknown scene pool '{ref_name}'")
|
|
_unique_extend(scene_entries, scene_pools[ref_name])
|
|
return scene_entries or fallback
|
|
|
|
|
|
def _sources_with_inheritance(
|
|
category: dict[str, Any],
|
|
subcategory: dict[str, Any],
|
|
item: Any,
|
|
inherit_key: str,
|
|
) -> tuple[Any, ...]:
|
|
item_source = item if isinstance(item, dict) else None
|
|
if item_source is not None and _is_false(item_source.get(inherit_key)):
|
|
return (item_source,)
|
|
if _is_false(subcategory.get(inherit_key)):
|
|
return (subcategory, item_source)
|
|
return (category, subcategory, item_source)
|
|
|
|
|
|
def _configured_pool(
|
|
category: dict[str, Any],
|
|
subcategory: dict[str, Any],
|
|
item: Any,
|
|
direct_key: str,
|
|
pool_key: str,
|
|
pool_library: dict[str, list[Any]],
|
|
inherit_key: str,
|
|
) -> list[Any]:
|
|
entries: list[Any] = []
|
|
singular_pool_key = pool_key[:-1] if pool_key.endswith("s") else pool_key
|
|
for source in _sources_with_inheritance(category, subcategory, item, inherit_key):
|
|
if not isinstance(source, dict):
|
|
continue
|
|
if direct_key in source:
|
|
_unique_extend(entries, _list_from(source[direct_key]))
|
|
refs = _list_from(source.get(singular_pool_key)) + _list_from(source.get(pool_key))
|
|
for ref in refs:
|
|
ref_name = str(ref).strip()
|
|
if ref_name not in pool_library:
|
|
raise ValueError(f"Unknown {singular_pool_key} '{ref_name}'")
|
|
_unique_extend(entries, pool_library[ref_name])
|
|
return entries
|
|
|
|
|
|
def _expression_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any) -> list[Any]:
|
|
return _configured_pool(
|
|
category,
|
|
subcategory,
|
|
item,
|
|
"expressions",
|
|
"expression_pools",
|
|
load_expression_pool_library(),
|
|
"inherit_expressions",
|
|
) or g.EXPRESSIONS
|
|
|
|
|
|
def _expression_intensity_hint(entry: Any) -> float:
|
|
if isinstance(entry, dict):
|
|
for key in ("expression_intensity", "intensity"):
|
|
if key in entry:
|
|
return _clamped_float(entry[key], 0.5)
|
|
|
|
text = _entry_text(entry).lower()
|
|
high_terms = (
|
|
"ahegao",
|
|
"orgasm",
|
|
"climax",
|
|
"drool",
|
|
"drooling",
|
|
"tongue out",
|
|
"eyes rolled",
|
|
"fucked-out",
|
|
"cum-smeared",
|
|
"saliva",
|
|
"gagging",
|
|
"slack jaw",
|
|
"jaw slack",
|
|
"slack-jawed",
|
|
"sex-drunk",
|
|
"overwhelmed",
|
|
"strained",
|
|
"messy",
|
|
"panting",
|
|
"trembling",
|
|
"shaking",
|
|
"wide open mouth",
|
|
"raw ",
|
|
"wild ",
|
|
"dazed",
|
|
"spent",
|
|
)
|
|
if any(term in text for term in high_terms):
|
|
return 0.9
|
|
|
|
medium_terms = (
|
|
"seductive",
|
|
"teasing",
|
|
"lustful",
|
|
"aroused",
|
|
"bedroom",
|
|
"dominant",
|
|
"predatory",
|
|
"control",
|
|
"stern",
|
|
"strict",
|
|
"smirk",
|
|
"parted lips",
|
|
"open-mouthed",
|
|
"heated",
|
|
"hungry",
|
|
"inviting",
|
|
"sensual",
|
|
"fetish",
|
|
"commanding",
|
|
"flushed",
|
|
"moan",
|
|
)
|
|
if any(term in text for term in medium_terms):
|
|
return 0.62
|
|
|
|
low_terms = (
|
|
"neutral",
|
|
"quiet",
|
|
"calm",
|
|
"reserved",
|
|
"relaxed",
|
|
"candid",
|
|
"closed-mouth",
|
|
"thoughtful",
|
|
"controlled",
|
|
"focused",
|
|
"steady",
|
|
"bitten-lip",
|
|
"braced",
|
|
"held breath",
|
|
"concentrated",
|
|
"aloof",
|
|
"bored",
|
|
"tired",
|
|
"unfocused",
|
|
"contented",
|
|
"fashion",
|
|
"soft",
|
|
"sleepy",
|
|
"fresh-faced",
|
|
)
|
|
if any(term in text for term in low_terms):
|
|
return 0.25
|
|
return 0.5
|
|
|
|
|
|
def _expression_entries_for_intensity(entries: list[Any], expression_intensity: float) -> list[Any]:
|
|
target = _clamped_float(expression_intensity, 0.5)
|
|
weighted: list[Any] = []
|
|
for entry in entries:
|
|
entry_intensity = _expression_intensity_hint(entry)
|
|
distance = abs(target - entry_intensity)
|
|
if distance <= 0.18:
|
|
intensity_weight = 4.0
|
|
elif distance <= 0.35:
|
|
intensity_weight = 1.4
|
|
elif distance <= 0.55:
|
|
intensity_weight = 0.35
|
|
else:
|
|
intensity_weight = 0.05
|
|
|
|
if isinstance(entry, dict):
|
|
adjusted = dict(entry)
|
|
try:
|
|
base_weight = float(adjusted.get("weight", 1.0))
|
|
except (TypeError, ValueError):
|
|
base_weight = 1.0
|
|
adjusted["weight"] = max(0.0, base_weight) * intensity_weight
|
|
weighted.append(adjusted)
|
|
else:
|
|
weighted.append({"text": _entry_text(entry), "weight": intensity_weight})
|
|
return weighted or entries
|
|
|
|
|
|
def _pose_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str, poses: str) -> list[Any]:
|
|
configured = _merged_field(category, subcategory, item, "poses")
|
|
if configured:
|
|
return _list_from(configured)
|
|
if subject_type == "couple":
|
|
return [entry[2] for entry in g.COUPLE_TYPES]
|
|
if subject_type in ("layout", "scene"):
|
|
return ["clean designed layout"]
|
|
return g.EVOCATIVE_ALL if poses == "evocative" else g.POSES
|
|
|
|
|
|
def _composition_pool(category: dict[str, Any], subcategory: dict[str, Any], item: Any, subject_type: str) -> list[Any]:
|
|
configured = _configured_pool(
|
|
category,
|
|
subcategory,
|
|
item,
|
|
"compositions",
|
|
"composition_pools",
|
|
load_composition_pool_library(),
|
|
"inherit_compositions",
|
|
)
|
|
if configured:
|
|
return configured
|
|
if subject_type in ("group", "configured_cast"):
|
|
return g.GROUP_COMPOSITIONS
|
|
if subject_type in ("layout", "scene"):
|
|
return ["designed illustration layout"]
|
|
return g.COMPOSITIONS
|
|
|
|
|
|
def _build_custom_row(
|
|
category_choice: str,
|
|
subcategory_choice: str,
|
|
row_number: int,
|
|
start_index: int,
|
|
ethnicity: str,
|
|
poses: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
women_count: int,
|
|
men_count: int,
|
|
seed: int,
|
|
seed_config: dict[str, int],
|
|
expression_enabled: bool,
|
|
expression_intensity: float,
|
|
character_profile: str | dict[str, Any] | None = None,
|
|
character_cast: str | dict[str, Any] | list[Any] | None = None,
|
|
expression_phase: str = "",
|
|
) -> dict[str, Any]:
|
|
categories = load_category_library()
|
|
category_rng = _axis_rng(seed_config, "category", seed, row_number)
|
|
subcategory_rng = _axis_rng(seed_config, "subcategory", seed, row_number)
|
|
person_rng = _axis_rng(seed_config, "person", seed, row_number)
|
|
scene_rng = _axis_rng(seed_config, "scene", seed, row_number)
|
|
pose_rng = _axis_rng(seed_config, "pose", seed, row_number)
|
|
role_rng = _axis_rng(seed_config, "role", seed, row_number)
|
|
expression_rng = _axis_rng(seed_config, "expression", seed, row_number)
|
|
composition_rng = _axis_rng(seed_config, "composition", seed, row_number)
|
|
|
|
requested_women_count = women_count
|
|
requested_men_count = men_count
|
|
category, subcategory, women_count, men_count = _find_subcategory(
|
|
categories,
|
|
category_choice,
|
|
subcategory_choice,
|
|
category_rng,
|
|
subcategory_rng,
|
|
women_count,
|
|
men_count,
|
|
)
|
|
count_adjustment = {}
|
|
if women_count != requested_women_count or men_count != requested_men_count:
|
|
count_adjustment = {
|
|
"requested_women_count": requested_women_count,
|
|
"requested_men_count": requested_men_count,
|
|
"effective_women_count": women_count,
|
|
"effective_men_count": men_count,
|
|
}
|
|
content_axis = "pose" if _is_pose_content_category(category, subcategory) else "content"
|
|
content_rng = _axis_rng(seed_config, content_axis, seed, row_number)
|
|
items = _list_from(subcategory.get("items", [subcategory["name"]]))
|
|
item = _weighted_choice(content_rng, items)
|
|
item_text, item_name, item_axis_values = _compose_item(content_rng, category, subcategory, item, women_count, men_count)
|
|
subject_type = str(_merged_field(category, subcategory, item, "subject_type", "single_any"))
|
|
context = _subject_context(person_rng, subject_type, ethnicity, figure, no_plus_women, no_black, women_count, men_count)
|
|
character_slots = _parse_character_cast(character_cast)
|
|
character_slot_map = _character_slot_label_map(character_slots)
|
|
applied_slot: dict[str, Any] = {}
|
|
slot_status = "none"
|
|
if context.get("subject_type") in ("woman", "man"):
|
|
slot_label = "Woman A" if context["subject_type"] == "woman" else "Man A"
|
|
if slot_label in character_slot_map:
|
|
context, applied_slot = _character_context_for_label(
|
|
slot_label,
|
|
character_slot_map,
|
|
person_rng,
|
|
ethnicity,
|
|
figure,
|
|
no_plus_women,
|
|
no_black,
|
|
)
|
|
slot_status = f"applied:{slot_label}"
|
|
applied_profile, profile_status = {}, "skipped_character_slot"
|
|
else:
|
|
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
|
|
else:
|
|
context, applied_profile, profile_status = _apply_character_profile_to_context(context, character_profile)
|
|
subject_type = context["subject_type"]
|
|
pov_character_labels = (
|
|
_pov_character_labels(character_slot_map, men_count)
|
|
if subject_type == "configured_cast"
|
|
else []
|
|
)
|
|
source_role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
|
|
role_graph = _pov_role_graph_prompt(source_role_graph, pov_character_labels)
|
|
cast_descriptors: list[str] = []
|
|
cast_descriptor_text = ""
|
|
expression_intensity_source = "input"
|
|
expression_disabled = not bool(expression_enabled)
|
|
if expression_disabled:
|
|
expression_intensity_source = "disabled"
|
|
elif subject_type in ("woman", "man") and applied_slot:
|
|
slot_label = "Woman A" if subject_type == "woman" else "Man A"
|
|
if not _slot_expression_enabled(applied_slot):
|
|
expression_disabled = True
|
|
expression_intensity_source = f"character_slot:{slot_label}:disabled"
|
|
else:
|
|
slot_expression_intensity = _slot_expression_intensity_for_phase(applied_slot, expression_phase)
|
|
if slot_expression_intensity is not None:
|
|
expression_intensity = slot_expression_intensity
|
|
expression_intensity_source = f"character_slot:{slot_label}"
|
|
elif subject_type == "configured_cast" and character_slots:
|
|
expression_intensity, expression_intensity_source = _cast_expression_intensity_override(
|
|
expression_intensity,
|
|
character_slot_map,
|
|
women_count,
|
|
men_count,
|
|
expression_phase,
|
|
)
|
|
if expression_intensity is None:
|
|
expression_disabled = True
|
|
if subject_type == "configured_cast" and character_slots:
|
|
cast_descriptors, _descriptor_slots = _cast_descriptor_entries(
|
|
seed_config,
|
|
seed,
|
|
row_number,
|
|
ethnicity,
|
|
figure,
|
|
no_plus_women,
|
|
no_black,
|
|
women_count,
|
|
men_count,
|
|
character_slots,
|
|
)
|
|
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
|
|
|
|
scene_slug, scene = _choose_pair(scene_rng, _compatible_entries(_scene_pool(category, subcategory, item, subject_type), women_count, men_count))
|
|
pose = str(_merged_field(category, subcategory, item, "pose", "") or context.get("fallback_pose") or _choose_text(
|
|
pose_rng, _compatible_entries(_pose_pool(category, subcategory, item, subject_type, poses), women_count, men_count)
|
|
))
|
|
expression_pool = _expression_pool(category, subcategory, item)
|
|
if expression_disabled:
|
|
expression = ""
|
|
else:
|
|
expression_entries = _compatible_entries(
|
|
_expression_entries_for_intensity(expression_pool, expression_intensity),
|
|
women_count,
|
|
men_count,
|
|
)
|
|
expression = _choose_text(expression_rng, expression_entries)
|
|
if subject_type in ("couple", "group") and ";" not in expression:
|
|
secondary_expression = _choose_distinct_text(expression_rng, expression_entries, expression)
|
|
if secondary_expression:
|
|
expression = f"{expression}; {secondary_expression}"
|
|
shared_expression = expression
|
|
character_expressions: list[str] = []
|
|
character_expression_text = ""
|
|
if not expression_disabled and subject_type == "configured_cast" and character_slots:
|
|
character_expressions = _character_expression_entries(
|
|
expression_rng,
|
|
expression_pool,
|
|
expression_intensity,
|
|
character_slot_map,
|
|
women_count,
|
|
men_count,
|
|
expression_phase,
|
|
)
|
|
character_expression_text = "; ".join(character_expressions)
|
|
if character_expression_text:
|
|
expression = character_expression_text
|
|
source_composition = _choose_text(
|
|
composition_rng,
|
|
_compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count),
|
|
)
|
|
composition = _pov_composition_prompt(source_composition, pov_character_labels)
|
|
|
|
negative_prompt = str(_merged_field(category, subcategory, item, "negative_prompt", g.NEGATIVE_PROMPT))
|
|
positive_suffix = str(_merged_field(category, subcategory, item, "positive_suffix", GENERIC_POSITIVE_SUFFIX))
|
|
style = str(
|
|
_merged_field(
|
|
category,
|
|
subcategory,
|
|
item,
|
|
"style",
|
|
"sexy but tasteful adult pin-up coloured-pencil comic illustration",
|
|
)
|
|
)
|
|
item_label = str(_merged_field(category, subcategory, item, "item_label", category["name"]))
|
|
|
|
context.update(
|
|
{
|
|
"trigger": g.TRIGGER,
|
|
"main_category": category["name"],
|
|
"subcategory": subcategory["name"],
|
|
"category": category["name"],
|
|
"item": item_text,
|
|
"item_name": item_name,
|
|
"item_label": item_label,
|
|
"style": style,
|
|
"scene": scene,
|
|
"scene_slug": scene_slug,
|
|
"pose": pose,
|
|
"expression": expression,
|
|
"shared_expression": shared_expression,
|
|
"character_expressions": character_expressions,
|
|
"character_expression_text": character_expression_text,
|
|
"expression_enabled": not expression_disabled,
|
|
"expression_disabled": expression_disabled,
|
|
"expression_intensity": expression_intensity,
|
|
"expression_intensity_source": expression_intensity_source,
|
|
"composition": composition,
|
|
"source_composition": source_composition,
|
|
"composition_prompt": _composition_prompt(composition),
|
|
"role_graph": role_graph,
|
|
"source_role_graph": source_role_graph,
|
|
"pov_character_labels": pov_character_labels,
|
|
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
|
|
"cast_descriptors": cast_descriptor_text,
|
|
"positive_suffix": positive_suffix,
|
|
"negative_prompt": negative_prompt,
|
|
}
|
|
)
|
|
|
|
if isinstance(item, dict) and "prompt_template" in item:
|
|
template = str(item["prompt_template"])
|
|
else:
|
|
template = str(subcategory.get("prompt_template") or category.get("prompt_template") or "")
|
|
if not template:
|
|
if subject_type in ("woman", "man"):
|
|
template = SINGLE_TEMPLATE
|
|
elif subject_type == "couple":
|
|
template = COUPLE_TEMPLATE
|
|
elif subject_type == "group":
|
|
template = GROUP_TEMPLATE
|
|
else:
|
|
template = LAYOUT_TEMPLATE
|
|
|
|
caption_template = str(
|
|
(item.get("caption_template") if isinstance(item, dict) else None)
|
|
or subcategory.get("caption_template")
|
|
or category.get("caption_template")
|
|
or "{trigger}, {subject_phrase}, {age}, {item}, {scene}, {composition}, coloured pencil comic illustration"
|
|
)
|
|
|
|
prompt = _format(template, context)
|
|
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in template:
|
|
prompt = _insert_positive_directive(prompt, f"Characters: {cast_descriptor_text}.")
|
|
if subject_type == "configured_cast" and pov_character_labels:
|
|
prompt = _insert_positive_directive(prompt, _pov_prompt_directive(pov_character_labels))
|
|
caption = _format(caption_template, context)
|
|
if subject_type == "configured_cast" and cast_descriptor_text and "{cast_descriptors}" not in caption_template:
|
|
caption = f"{caption.rstrip()}, {cast_descriptor_text}"
|
|
batch = max(1, ((row_number - 1) // g.BATCH_SIZE) + 1)
|
|
index = start_index + row_number - 1
|
|
row = g.row_base(index, batch, context["subject"], context["age"], context["body"], scene_slug, composition)
|
|
row.update(
|
|
{
|
|
"prompt": prompt,
|
|
"caption": caption,
|
|
"negative_prompt": negative_prompt,
|
|
"expression": expression,
|
|
"main_category": category["name"],
|
|
"subcategory": subcategory["name"],
|
|
"category_slug": category["slug"],
|
|
"subcategory_slug": subcategory["slug"],
|
|
"subject_type": subject_type,
|
|
"subject_phrase": context.get("subject_phrase", ""),
|
|
"body_phrase": context.get("body_phrase", ""),
|
|
"skin": context.get("skin", ""),
|
|
"hair": context.get("hair", ""),
|
|
"eyes": context.get("eyes", ""),
|
|
"style": style,
|
|
"item": item_text,
|
|
"item_label": item_label,
|
|
"positive_suffix": positive_suffix,
|
|
"custom_item": item_name,
|
|
"item_axis_values": item_axis_values,
|
|
"scene_text": scene,
|
|
"pose": pose,
|
|
"seed_config": seed_config,
|
|
"content_seed_axis": content_axis,
|
|
"role_graph": role_graph,
|
|
"source_role_graph": source_role_graph,
|
|
"source_composition": source_composition,
|
|
"pov_character_labels": pov_character_labels,
|
|
"pov_prompt_directive": _pov_prompt_directive(pov_character_labels),
|
|
"shared_expression": shared_expression,
|
|
"character_expressions": character_expressions,
|
|
"character_expression_text": character_expression_text,
|
|
"expression_enabled": not expression_disabled,
|
|
"expression_disabled": expression_disabled,
|
|
"cast_summary": context.get("cast_summary", ""),
|
|
"cast_descriptors": cast_descriptors,
|
|
"cast_descriptor_text": cast_descriptor_text,
|
|
"scene_kind": context.get("scene_kind", ""),
|
|
"women_count": context.get("women_count", ""),
|
|
"men_count": context.get("men_count", ""),
|
|
"person_count": context.get("person_count", ""),
|
|
"cast_count_adjustment": count_adjustment if subject_type == "configured_cast" else {},
|
|
"character_profile": applied_profile,
|
|
"character_profile_status": profile_status,
|
|
"character_slot": applied_slot,
|
|
"character_slot_status": slot_status,
|
|
"character_cast_slots": character_slots,
|
|
"expression_intensity": expression_intensity,
|
|
"expression_intensity_source": expression_intensity_source,
|
|
"source": "json_category",
|
|
}
|
|
)
|
|
if context.get("figure"):
|
|
row["figure"] = context["figure"]
|
|
if expression_disabled:
|
|
row = _disable_row_expression(row, expression_intensity_source)
|
|
return row
|
|
|
|
|
|
def build_prompt(
|
|
category: str,
|
|
subcategory: str,
|
|
row_number: int,
|
|
start_index: int,
|
|
seed: int,
|
|
clothing: str,
|
|
ethnicity: str,
|
|
poses: str,
|
|
backside_bias: float,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
minimal_clothing_ratio: float,
|
|
standard_pose_ratio: float,
|
|
trigger: str,
|
|
prepend_trigger_to_prompt: bool,
|
|
extra_positive: str,
|
|
extra_negative: str,
|
|
seed_config: str | dict[str, Any] | None = None,
|
|
women_count: int = 1,
|
|
men_count: int = 1,
|
|
camera_config: str | dict[str, Any] | None = None,
|
|
expression_intensity: float = 0.5,
|
|
character_profile: str | dict[str, Any] | None = None,
|
|
character_cast: str | dict[str, Any] | list[Any] | None = None,
|
|
expression_enabled: bool = True,
|
|
expression_phase: str = "",
|
|
) -> dict[str, Any]:
|
|
apply_pool_extensions()
|
|
row_number = max(1, int(row_number))
|
|
start_index = max(1, int(start_index))
|
|
seed = int(seed)
|
|
clothing = clothing if clothing in ("full", "minimal") else "full"
|
|
ethnicity = ethnicity if ethnicity == "any" or ethnicity in ETHNICITY_FILTER_CHOICES or "+" in str(ethnicity) else "any"
|
|
poses = poses if poses in ("standard", "evocative") else "standard"
|
|
figure = figure if figure in ("curvy", "balanced", "bombshell") else "curvy"
|
|
expression_enabled = not _is_false(expression_enabled)
|
|
minimal_ratio = _ratio_or_none(minimal_clothing_ratio)
|
|
pose_ratio = _ratio_or_none(standard_pose_ratio)
|
|
expression_intensity = _clamped_float(expression_intensity, 0.5)
|
|
parsed_seed_config = _parse_seed_config(seed_config)
|
|
|
|
exact_custom_subcategory = bool(subcategory and subcategory != RANDOM_SUBCATEGORY and " / " in subcategory)
|
|
|
|
if category == "auto_weighted" and not exact_custom_subcategory:
|
|
row = _build_auto_weighted_row(
|
|
row_number,
|
|
start_index,
|
|
clothing,
|
|
ethnicity,
|
|
poses,
|
|
float(backside_bias),
|
|
figure,
|
|
bool(no_plus_women),
|
|
bool(no_black),
|
|
minimal_ratio,
|
|
pose_ratio,
|
|
seed,
|
|
)
|
|
elif category in ("woman", "man", "couple", "group_or_layout") and not exact_custom_subcategory:
|
|
row = _build_direct_builtin_row(
|
|
category,
|
|
row_number,
|
|
start_index,
|
|
clothing,
|
|
ethnicity,
|
|
poses,
|
|
float(backside_bias),
|
|
figure,
|
|
bool(no_plus_women),
|
|
bool(no_black),
|
|
minimal_ratio,
|
|
pose_ratio,
|
|
seed,
|
|
)
|
|
else:
|
|
row = _build_custom_row(
|
|
category,
|
|
subcategory,
|
|
row_number,
|
|
start_index,
|
|
ethnicity,
|
|
poses,
|
|
figure,
|
|
bool(no_plus_women),
|
|
bool(no_black),
|
|
int(women_count),
|
|
int(men_count),
|
|
seed,
|
|
parsed_seed_config,
|
|
expression_enabled,
|
|
expression_intensity,
|
|
character_profile,
|
|
character_cast,
|
|
expression_phase,
|
|
)
|
|
|
|
if not expression_enabled:
|
|
row = _disable_row_expression(row, "disabled")
|
|
if extra_positive.strip():
|
|
row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}"
|
|
row = _apply_camera_config(row, camera_config)
|
|
active_trigger = trigger.strip() or g.TRIGGER
|
|
row["prompt"] = _prepend_trigger(row["prompt"], active_trigger, bool(prepend_trigger_to_prompt))
|
|
row["negative_prompt"] = _combined_negative(row.get("negative_prompt", g.NEGATIVE_PROMPT), extra_negative)
|
|
row["trigger"] = active_trigger
|
|
row.setdefault("expression_intensity", expression_intensity)
|
|
row.setdefault("expression_intensity_source", "input")
|
|
return row
|
|
|
|
|
|
def build_prompt_from_configs(
|
|
row_number: int,
|
|
start_index: int,
|
|
seed: int,
|
|
category_config: str | dict[str, Any] | None = "",
|
|
cast_config: str | dict[str, Any] | None = "",
|
|
generation_profile: str | dict[str, Any] | None = "",
|
|
filter_config: str | dict[str, Any] | None = "",
|
|
seed_config: str | dict[str, Any] | None = "",
|
|
camera_config: str | dict[str, Any] | None = "",
|
|
character_profile: str | dict[str, Any] | None = "",
|
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
|
extra_positive: str = "",
|
|
extra_negative: str = "",
|
|
) -> dict[str, Any]:
|
|
category, subcategory = _parse_category_config(category_config)
|
|
cast = _parse_cast_config(cast_config)
|
|
profile = _parse_generation_profile(generation_profile)
|
|
filters = _parse_filter_config(filter_config)
|
|
return build_prompt(
|
|
category=category,
|
|
subcategory=subcategory,
|
|
row_number=row_number,
|
|
start_index=start_index,
|
|
seed=seed,
|
|
clothing=profile["clothing"],
|
|
ethnicity=filters["ethnicity"],
|
|
poses=profile["poses"],
|
|
expression_enabled=profile["expression_enabled"],
|
|
expression_intensity=profile["expression_intensity"],
|
|
backside_bias=profile["backside_bias"],
|
|
figure=filters["figure"],
|
|
no_plus_women=filters["no_plus_women"],
|
|
no_black=filters["no_black"],
|
|
women_count=int(cast["women_count"]),
|
|
men_count=int(cast["men_count"]),
|
|
minimal_clothing_ratio=profile["minimal_clothing_ratio"],
|
|
standard_pose_ratio=profile["standard_pose_ratio"],
|
|
trigger=profile["trigger"],
|
|
prepend_trigger_to_prompt=profile["prepend_trigger_to_prompt"],
|
|
extra_positive=extra_positive or "",
|
|
extra_negative=extra_negative or "",
|
|
seed_config=seed_config or "",
|
|
camera_config=camera_config or "",
|
|
character_profile=character_profile or "",
|
|
character_cast=character_cast or "",
|
|
)
|
|
|
|
|
|
INSTA_OF_SOFT_LEVELS = {
|
|
"social_tease": "Instagram-style thirst-trap post, suggestive but non-explicit, polished social feed energy",
|
|
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate",
|
|
"implied_nude": "implied nude creator set, strategically covered body and intimate teaser framing",
|
|
"explicit_tease": "stronger adult teaser set with bolder nude-adjacent styling and solo-tease framing",
|
|
"explicit_nude": "explicit nude creator set with fully nude solo-tease framing",
|
|
}
|
|
|
|
INSTA_OF_HARDCORE_LEVELS = {
|
|
"explicit": "explicit adult creator content with clear sexual contact and adult-only framing",
|
|
"hardcore": "hardcore adult creator content with anatomically clear sexual contact and intense body language",
|
|
}
|
|
|
|
INSTA_OF_PLATFORM_STYLES = {
|
|
"hybrid": "hybrid Instagram-to-OF creator shoot, polished social-media framing with intimate subscriber-content energy",
|
|
"instagram": "Instagram-inspired creator shoot, polished mirror-selfie and feed-post aesthetics",
|
|
"onlyfans": "OnlyFans-inspired creator shoot, intimate subscriber-view camera and candid premium-content framing",
|
|
}
|
|
|
|
INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = {
|
|
"none": "",
|
|
"same_outfit": "Woman A keeps her teaser outfit on, with sexual contact still clearly visible",
|
|
"partially_removed": "Woman A's teaser outfit is pushed aside and partly removed, exposing the sexual contact clearly",
|
|
"implied_nude": "Woman A's body is partly exposed, with fabric slipping off or covering only part of the body",
|
|
"explicit_nude": "Woman A's body is fully exposed, bare skin unobstructed",
|
|
}
|
|
|
|
INSTA_OF_NEGATIVE = (
|
|
"minors, childlike appearance, teen, underage, schoolgirl, non-consensual, coercion, rape, "
|
|
"violence, injury, blood, gore, incest, bestiality, watermark, logo, readable username, social media UI"
|
|
)
|
|
|
|
INSTA_OF_SOFT_NEGATIVE = (
|
|
INSTA_OF_NEGATIVE
|
|
+ ", explicit intercourse, penetration, oral sex, cumshot, genital contact, group sex, "
|
|
"shirtless partner, bare-chested partner, partner nudity"
|
|
)
|
|
|
|
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL = {
|
|
"social_tease": "Casual clothes / Smart casual",
|
|
"lingerie_tease": "Provocative erotic clothes / Provocative lingerie",
|
|
"implied_nude": "Provocative erotic clothes / Provocative lingerie",
|
|
"explicit_tease": "Provocative erotic clothes / Sheer exposed",
|
|
"explicit_nude": "Provocative erotic clothes / Nude accessories",
|
|
}
|
|
|
|
INSTA_OF_SOFTCORE_OUTFITS = {
|
|
"social_tease": [
|
|
"cropped fitted tee, low-rise jeans, delicate jewelry, and polished feed-post styling",
|
|
"oversized off-shoulder sweater with fitted shorts and soft lounge socks",
|
|
"ribbed tank top, mini skirt, hoop earrings, and casual creator styling",
|
|
"silky camisole tucked into relaxed trousers with a subtle waist chain",
|
|
"sporty crop top, bike shorts, clean sneakers, and glossy social-feed styling",
|
|
"button-down shirt tied at the waist over a fitted bralette and denim shorts",
|
|
"body-hugging knit dress with bare shoulders and simple heels",
|
|
"relaxed hoodie half-zipped over a crop top with high-cut shorts",
|
|
],
|
|
"lingerie_tease": [
|
|
"black lace lingerie set with opaque cups, high-waisted briefs, garter straps, and sheer robe",
|
|
"satin bralette and matching high-waisted panties under an oversized shirt",
|
|
"lace bodysuit with opaque cups, soft stockings, and delicate garter details",
|
|
"silk slip dress with thin straps, thigh slit, and subtle lace trim",
|
|
"matching balconette bra and brief set under a loosely draped satin robe",
|
|
"velvet lingerie set with covered cups, garter belt, sheer stockings, and small gold accents",
|
|
"mesh robe over a covered lace teddy, styled as a premium non-explicit teaser",
|
|
"structured corset top with opaque panels, matching briefs, and sheer stockings",
|
|
],
|
|
"implied_nude": [
|
|
"oversized white shirt slipping off one shoulder, body mostly covered, bare legs, and soft sheets",
|
|
"towel wrap held across the chest and hips, implied nude but fully covered",
|
|
"satin sheet wrapped around the body with shoulders and legs visible but intimate areas covered",
|
|
"open robe held closed by hand, implied nude beneath without explicit exposure",
|
|
"bath towel and damp hair after a shower, covered chest and hips, intimate creator styling",
|
|
"soft blanket wrapped around the body, bare shoulders visible, sensual but covered",
|
|
],
|
|
"explicit_tease": [
|
|
"sheer robe over matching lingerie with intimate areas obscured by lace pattern and pose",
|
|
"wet-look bodysuit with opaque panels, high-cut legs, and glossy club-light styling",
|
|
"transparent mesh dress over covered lingerie, posed as an adult teaser without sex act",
|
|
"lace teddy with strategic opaque embroidery, garter straps, and sheer stockings",
|
|
"bare-shoulder robe opened around covered lingerie, explicit adult tease without partnered contact",
|
|
"strappy lingerie set with covered cups and high-waisted bottoms, styled as a stronger solo teaser",
|
|
],
|
|
"explicit_nude": [
|
|
"body fully exposed with jewelry accents and direct adult selfie confidence",
|
|
"mirror-selfie body exposure with jewelry accents and bold creator-shot framing",
|
|
"body fully exposed on soft sheets with direct eye contact",
|
|
"vanity-mirror body exposure with necklace detail and premium creator-shot styling",
|
|
"shower-afterglow body exposure with wet hair, skin highlights, and phone-shot framing",
|
|
"bedroom body exposure with one hand holding the phone and direct camera awareness",
|
|
],
|
|
}
|
|
|
|
INSTA_OF_SOFTCORE_POSES = {
|
|
"social_tease": [
|
|
"taking a mirror selfie with one hip angled and relaxed social-feed confidence",
|
|
"leaning against a doorway with one hand holding the phone and a casual teasing smile",
|
|
"sitting on the edge of the bed for a polished outfit-check selfie",
|
|
"standing by the window with shoulders relaxed and body angled toward the phone",
|
|
"posing in a clean feed-post stance with one hand at the waist",
|
|
"stretching one arm above the head in a casual morning selfie pose",
|
|
],
|
|
"lingerie_tease": [
|
|
"taking a mirror lingerie selfie with one hip angled and the outfit clearly visible",
|
|
"kneeling on the bed in a covered lingerie teaser pose with hands kept on fabric",
|
|
"leaning against the vanity with the robe draped around covered lingerie",
|
|
"standing in a three-quarter lingerie outfit-check pose with legs softly crossed",
|
|
"sitting on the bed with stockings and garter details visible, non-explicit and posed",
|
|
"turning slightly over one shoulder to show the lingerie silhouette",
|
|
],
|
|
"implied_nude": [
|
|
"holding the towel or sheet securely in place while posing for an implied nude selfie",
|
|
"sitting under soft sheets with shoulders visible and the body strategically covered",
|
|
"standing by the bathroom mirror with a towel wrapped around the body",
|
|
"reclining under a satin sheet with intimate areas fully obscured",
|
|
"holding an open robe closed in a covered implied nude teaser pose",
|
|
"looking into the phone camera while wrapped in a blanket with bare shoulders visible",
|
|
],
|
|
"explicit_tease": [
|
|
"posing in a stronger adult teaser stance with covered lingerie and no partnered contact",
|
|
"kneeling on the bed with a sheer robe arranged around covered lingerie",
|
|
"standing close to the mirror with the outfit framed boldly but non-explicitly",
|
|
"leaning forward slightly with hands on the robe and intimate areas obscured",
|
|
"sitting at the vanity in a bolder covered lingerie pose with direct eye contact",
|
|
"arching subtly in a solo adult tease while the styling keeps explicit anatomy obscured",
|
|
],
|
|
"explicit_nude": [
|
|
"taking a bold mirror selfie with direct eye contact and the body clearly framed",
|
|
"posing on the bed with body fully exposed and jewelry accents as styling",
|
|
"standing at the vanity with body fully exposed in a premium creator-shot pose",
|
|
"reclining on soft sheets with body fully exposed and the phone held close",
|
|
"turning slightly in a mirror pose with the body framed head-to-thigh",
|
|
"kneeling in a controlled adult teaser pose with body fully exposed and direct phone-camera awareness",
|
|
],
|
|
}
|
|
|
|
INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS = [
|
|
"satin slip dress under an oversized shirt",
|
|
"soft cardigan over a camisole with relaxed trousers",
|
|
"fitted crop top with high-waisted jeans",
|
|
"silky robe over a covered bralette and lounge shorts",
|
|
"bodycon mini dress with simple heels",
|
|
"ribbed tank top with joggers and delicate jewelry",
|
|
"oversized tee with fitted shorts and lounge socks",
|
|
"button-down shirt with a fitted skirt",
|
|
]
|
|
|
|
INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS = [
|
|
"fitted black tee with dark jeans",
|
|
"buttoned linen shirt with chinos",
|
|
"hoodie and joggers",
|
|
"open overshirt over a fitted tank with relaxed trousers",
|
|
"gym tee with track pants and a towel over one shoulder",
|
|
"casual knit shirt with tailored trousers",
|
|
"dark crewneck sweater with jeans",
|
|
"short-sleeve button-up shirt with relaxed shorts",
|
|
]
|
|
|
|
|
|
def build_insta_of_options_json(
|
|
softcore_cast: str = "solo",
|
|
hardcore_cast: str = "use_counts",
|
|
hardcore_women_count: int = 1,
|
|
hardcore_men_count: int = 1,
|
|
softcore_level: str = "lingerie_tease",
|
|
hardcore_level: str = "hardcore",
|
|
platform_style: str = "hybrid",
|
|
continuity: str = "same_creator_same_room",
|
|
hardcore_clothing_continuity: str = "partially_removed",
|
|
softcore_camera_mode: str = "handheld_selfie",
|
|
hardcore_camera_mode: str = "from_camera_config",
|
|
camera_detail: str = "compact",
|
|
softcore_expression_intensity: float = 0.45,
|
|
hardcore_expression_intensity: float = 0.85,
|
|
softcore_expression_enabled: bool = True,
|
|
hardcore_expression_enabled: bool = True,
|
|
hardcore_detail_density: str = "balanced",
|
|
) -> str:
|
|
hardcore_detail_density = (
|
|
hardcore_detail_density if hardcore_detail_density in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced"
|
|
)
|
|
return json.dumps(
|
|
{
|
|
"softcore_cast": softcore_cast,
|
|
"hardcore_cast": hardcore_cast,
|
|
"hardcore_women_count": int(hardcore_women_count),
|
|
"hardcore_men_count": int(hardcore_men_count),
|
|
"softcore_level": softcore_level,
|
|
"hardcore_level": hardcore_level,
|
|
"platform_style": platform_style,
|
|
"continuity": continuity,
|
|
"hardcore_clothing_continuity": hardcore_clothing_continuity,
|
|
"softcore_camera_mode": softcore_camera_mode,
|
|
"hardcore_camera_mode": hardcore_camera_mode,
|
|
"camera_detail": camera_detail,
|
|
"softcore_expression_enabled": not _is_false(softcore_expression_enabled),
|
|
"hardcore_expression_enabled": not _is_false(hardcore_expression_enabled),
|
|
"softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45),
|
|
"hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85),
|
|
"hardcore_detail_density": hardcore_detail_density,
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[str, Any]:
|
|
defaults = {
|
|
"softcore_cast": "solo",
|
|
"hardcore_cast": "use_counts",
|
|
"hardcore_women_count": 1,
|
|
"hardcore_men_count": 1,
|
|
"softcore_level": "lingerie_tease",
|
|
"hardcore_level": "hardcore",
|
|
"platform_style": "hybrid",
|
|
"continuity": "same_creator_same_room",
|
|
"hardcore_clothing_continuity": "partially_removed",
|
|
"softcore_camera_mode": "handheld_selfie",
|
|
"hardcore_camera_mode": "from_camera_config",
|
|
"camera_detail": "compact",
|
|
"softcore_expression_enabled": True,
|
|
"hardcore_expression_enabled": True,
|
|
"softcore_expression_intensity": 0.45,
|
|
"hardcore_expression_intensity": 0.85,
|
|
"hardcore_detail_density": "balanced",
|
|
}
|
|
if not options_json:
|
|
return defaults
|
|
if isinstance(options_json, dict):
|
|
raw = options_json
|
|
else:
|
|
try:
|
|
raw = json.loads(str(options_json))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid Insta/OF options JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("Insta/OF options must be a JSON object")
|
|
parsed = {**defaults, **raw}
|
|
parsed["softcore_cast"] = parsed["softcore_cast"] if parsed["softcore_cast"] in ("solo", "same_as_hardcore") else defaults["softcore_cast"]
|
|
parsed["hardcore_cast"] = parsed["hardcore_cast"] if parsed["hardcore_cast"] in ("use_counts", "couple", "threesome", "group") else defaults["hardcore_cast"]
|
|
parsed["softcore_level"] = parsed["softcore_level"] if parsed["softcore_level"] in INSTA_OF_SOFT_LEVELS else defaults["softcore_level"]
|
|
parsed["hardcore_level"] = parsed["hardcore_level"] if parsed["hardcore_level"] in INSTA_OF_HARDCORE_LEVELS else defaults["hardcore_level"]
|
|
parsed["platform_style"] = parsed["platform_style"] if parsed["platform_style"] in INSTA_OF_PLATFORM_STYLES else defaults["platform_style"]
|
|
parsed["continuity"] = parsed["continuity"] if parsed["continuity"] in ("same_creator_same_room", "same_creator_new_scene") else defaults["continuity"]
|
|
parsed["hardcore_clothing_continuity"] = (
|
|
parsed["hardcore_clothing_continuity"]
|
|
if parsed["hardcore_clothing_continuity"] in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
|
|
else defaults["hardcore_clothing_continuity"]
|
|
)
|
|
parsed["softcore_camera_mode"] = (
|
|
parsed["softcore_camera_mode"]
|
|
if parsed["softcore_camera_mode"] in CAMERA_MODE_PROMPTS or parsed["softcore_camera_mode"] == "from_camera_config"
|
|
else defaults["softcore_camera_mode"]
|
|
)
|
|
if (
|
|
parsed["hardcore_camera_mode"] not in CAMERA_MODE_PROMPTS
|
|
and parsed["hardcore_camera_mode"] not in ("from_camera_config", "same_as_softcore")
|
|
):
|
|
parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"]
|
|
parsed["camera_detail"] = parsed["camera_detail"] if parsed["camera_detail"] in CAMERA_DETAIL_CHOICES else defaults["camera_detail"]
|
|
parsed["softcore_expression_enabled"] = not _is_false(parsed.get("softcore_expression_enabled", True))
|
|
parsed["hardcore_expression_enabled"] = not _is_false(parsed.get("hardcore_expression_enabled", True))
|
|
parsed["softcore_expression_intensity"] = _clamped_float(
|
|
parsed.get("softcore_expression_intensity"),
|
|
defaults["softcore_expression_intensity"],
|
|
)
|
|
parsed["hardcore_expression_intensity"] = _clamped_float(
|
|
parsed.get("hardcore_expression_intensity"),
|
|
defaults["hardcore_expression_intensity"],
|
|
)
|
|
parsed["hardcore_detail_density"] = (
|
|
parsed["hardcore_detail_density"]
|
|
if parsed.get("hardcore_detail_density") in HARDCORE_DETAIL_DENSITY_CHOICES
|
|
else defaults["hardcore_detail_density"]
|
|
)
|
|
for key in ("hardcore_women_count", "hardcore_men_count"):
|
|
try:
|
|
parsed[key] = max(0, min(12, int(parsed[key])))
|
|
except (TypeError, ValueError):
|
|
parsed[key] = defaults[key]
|
|
return parsed
|
|
|
|
|
|
def _insta_of_hardcore_counts(options: dict[str, Any]) -> tuple[int, int]:
|
|
policy = str(options.get("hardcore_cast", "use_counts"))
|
|
if policy == "couple":
|
|
women_count, men_count = 1, 1
|
|
elif policy == "threesome":
|
|
women_count, men_count = 2, 1
|
|
elif policy == "group":
|
|
women_count, men_count = 3, 2
|
|
else:
|
|
women_count = int(options.get("hardcore_women_count") or 0)
|
|
men_count = int(options.get("hardcore_men_count") or 0)
|
|
women_count = max(1, min(12, women_count))
|
|
men_count = max(0, min(12, men_count))
|
|
if women_count + men_count < 2:
|
|
men_count = 1
|
|
return women_count, men_count
|
|
|
|
|
|
def _insta_of_descriptor(row: dict[str, Any]) -> str:
|
|
return _descriptor_from_parts(
|
|
"woman",
|
|
row.get("age_band") or row.get("age"),
|
|
row.get("body_phrase"),
|
|
row.get("skin"),
|
|
row.get("hair"),
|
|
row.get("eyes"),
|
|
row.get("descriptor_detail"),
|
|
)
|
|
|
|
|
|
def _insta_of_descriptor_from_context(context: dict[str, Any]) -> str:
|
|
subject = str(context.get("subject") or context.get("subject_type") or "person").strip()
|
|
return _descriptor_from_parts(
|
|
subject,
|
|
context.get("age"),
|
|
context.get("body_phrase"),
|
|
context.get("skin"),
|
|
context.get("hair"),
|
|
context.get("eyes"),
|
|
context.get("descriptor_detail"),
|
|
)
|
|
|
|
|
|
def _insta_of_cast_descriptors(
|
|
primary_descriptor: str,
|
|
seed_config: dict[str, int],
|
|
seed: int,
|
|
row_number: int,
|
|
ethnicity: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
women_count: int,
|
|
men_count: int,
|
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
|
) -> list[str]:
|
|
descriptors, _slots = _cast_descriptor_entries(
|
|
seed_config,
|
|
seed,
|
|
row_number,
|
|
ethnicity,
|
|
figure,
|
|
no_plus_women,
|
|
no_black,
|
|
women_count,
|
|
men_count,
|
|
character_cast,
|
|
primary_descriptor=primary_descriptor,
|
|
)
|
|
return descriptors
|
|
|
|
|
|
def _insta_of_cast_phrase(women_count: int, men_count: int) -> str:
|
|
context = _configured_cast_context(women_count, men_count)
|
|
return context["cast_summary"]
|
|
|
|
|
|
def _insta_of_prompt_cast_descriptors(text: str) -> str:
|
|
return str(text or "").replace("Woman A / primary creator:", "Woman A:")
|
|
|
|
|
|
SOFTCORE_CAST_POSES = [
|
|
"standing together for a mirror selfie with bodies close but no sexual contact",
|
|
"posing shoulder-to-shoulder in a creator-shot group teaser",
|
|
"leaning together on the bed in a non-explicit subscriber preview",
|
|
"sitting close together with hands kept above clothing",
|
|
"arranged around Woman A in a flirtatious non-explicit teaser pose",
|
|
"posing together as a coordinated adult creator set",
|
|
"standing near the phone tripod with relaxed teasing body language",
|
|
"framed together in a softcore cast reveal with no sex act",
|
|
]
|
|
|
|
|
|
def _insta_of_softcore_category(level: str) -> tuple[str, str]:
|
|
subcategory = INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL.get(
|
|
level,
|
|
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL["lingerie_tease"],
|
|
)
|
|
category, _subcategory = subcategory.split(" / ", 1)
|
|
return category, subcategory
|
|
|
|
|
|
def _insta_of_softcore_outfit(rng: random.Random, level: str) -> str:
|
|
pool = INSTA_OF_SOFTCORE_OUTFITS.get(level, INSTA_OF_SOFTCORE_OUTFITS["lingerie_tease"])
|
|
return g.choose(rng, pool)
|
|
|
|
|
|
def _insta_of_softcore_item_prompt_label(level: str) -> str:
|
|
return "Body exposure" if level == "explicit_nude" else "Outfit"
|
|
|
|
|
|
def _insta_of_softcore_pose(rng: random.Random, level: str) -> str:
|
|
pool = INSTA_OF_SOFTCORE_POSES.get(level, INSTA_OF_SOFTCORE_POSES["lingerie_tease"])
|
|
return g.choose(rng, pool)
|
|
|
|
|
|
def _insta_of_hardcore_clothing_state(mode: str, softcore_outfit: str) -> str:
|
|
mode = mode if mode in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY else "none"
|
|
outfit = str(softcore_outfit or "").strip()
|
|
if mode == "none" or not outfit:
|
|
return ""
|
|
base = INSTA_OF_HARDCORE_CLOTHING_CONTINUITY[mode]
|
|
if mode == "explicit_nude":
|
|
return f"Body exposure: {base}."
|
|
if mode == "implied_nude":
|
|
return f"Body exposure: {base}."
|
|
return f"Clothing state: {base}; teaser outfit detail: {outfit}."
|
|
|
|
|
|
def _insta_of_partner_styling(
|
|
seed_config: dict[str, int],
|
|
seed: int,
|
|
row_number: int,
|
|
women_count: int,
|
|
men_count: int,
|
|
pov_labels: list[str] | None = None,
|
|
label_map: dict[str, dict[str, Any]] | None = None,
|
|
) -> dict[str, Any]:
|
|
content_rng = _axis_rng(seed_config, "content", seed, row_number + 421)
|
|
pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421)
|
|
pov_set = set(pov_labels or [])
|
|
outfits: list[str] = []
|
|
for index in range(max(0, women_count - 1)):
|
|
label = chr(ord("B") + index)
|
|
full_label = f"Woman {label}"
|
|
outfit = _slot_softcore_outfit((label_map or {}).get(full_label)) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)
|
|
sentence = _softcore_outfit_sentence(full_label, outfit)
|
|
if sentence:
|
|
outfits.append(sentence)
|
|
for index in range(max(0, men_count)):
|
|
label = chr(ord("A") + index)
|
|
full_label = f"Man {label}"
|
|
if full_label in pov_set:
|
|
continue
|
|
outfit = _slot_softcore_outfit((label_map or {}).get(full_label)) or g.choose(content_rng, INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)
|
|
sentence = _softcore_outfit_sentence(full_label, outfit)
|
|
if sentence:
|
|
outfits.append(sentence)
|
|
return {
|
|
"outfits": outfits,
|
|
"pose": g.choose(pose_rng, SOFTCORE_CAST_POSES),
|
|
}
|
|
|
|
|
|
def _insta_of_active_trigger(prompt: str, trigger: str, enabled: bool) -> str:
|
|
return _prepend_trigger(prompt, trigger, enabled)
|
|
|
|
|
|
def build_insta_of_pair(
|
|
row_number: int,
|
|
start_index: int,
|
|
seed: int,
|
|
ethnicity: str,
|
|
figure: str,
|
|
no_plus_women: bool,
|
|
no_black: bool,
|
|
trigger: str,
|
|
prepend_trigger_to_prompt: bool,
|
|
seed_config: str | dict[str, Any] | None = None,
|
|
options_json: str | dict[str, Any] | None = None,
|
|
filter_config: str | dict[str, Any] | None = None,
|
|
camera_config: str | dict[str, Any] | None = None,
|
|
character_profile: str | dict[str, Any] | None = "",
|
|
character_cast: str | dict[str, Any] | list[Any] | None = "",
|
|
extra_positive: str = "",
|
|
extra_negative: str = "",
|
|
) -> dict[str, Any]:
|
|
options = _parse_insta_of_options(options_json)
|
|
if filter_config:
|
|
filters = _parse_filter_config(filter_config)
|
|
ethnicity = filters["ethnicity"]
|
|
figure = filters["figure"]
|
|
no_plus_women = filters["no_plus_women"]
|
|
no_black = filters["no_black"]
|
|
hard_women_count, hard_men_count = _insta_of_hardcore_counts(options)
|
|
active_trigger = trigger.strip() or g.TRIGGER
|
|
parsed_seed_config = _parse_seed_config(seed_config)
|
|
character_slots = _parse_character_cast(character_cast)
|
|
character_slot_map = _character_slot_label_map(character_slots)
|
|
pov_character_labels = _pov_character_labels(character_slot_map, hard_men_count)
|
|
softcore_level_key = str(options["softcore_level"])
|
|
soft_category, soft_subcategory = _insta_of_softcore_category(softcore_level_key)
|
|
soft_content_rng = _axis_rng(parsed_seed_config, "content", seed, row_number + 311)
|
|
soft_person_rng = _axis_rng(parsed_seed_config, "person", seed, row_number)
|
|
soft_expression_women_count = hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1
|
|
soft_expression_men_count = hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0
|
|
soft_expression_enabled = bool(options["softcore_expression_enabled"])
|
|
soft_expression_intensity = options["softcore_expression_intensity"]
|
|
soft_expression_intensity_source = "input"
|
|
if soft_expression_enabled:
|
|
soft_expression_intensity, soft_expression_intensity_source = _cast_expression_intensity_override(
|
|
options["softcore_expression_intensity"],
|
|
character_slot_map,
|
|
soft_expression_women_count,
|
|
soft_expression_men_count,
|
|
"softcore",
|
|
)
|
|
if soft_expression_intensity is None:
|
|
soft_expression_enabled = False
|
|
else:
|
|
soft_expression_intensity_source = "disabled"
|
|
primary_slot_context = None
|
|
primary_slot = character_slot_map.get("Woman A")
|
|
if primary_slot:
|
|
primary_slot_context = _context_from_character_slot(
|
|
soft_person_rng,
|
|
primary_slot,
|
|
"woman",
|
|
ethnicity,
|
|
figure,
|
|
no_plus_women,
|
|
no_black,
|
|
)
|
|
|
|
soft_row = build_prompt(
|
|
category=soft_category,
|
|
subcategory=soft_subcategory,
|
|
row_number=row_number,
|
|
start_index=start_index,
|
|
seed=seed,
|
|
clothing="minimal",
|
|
ethnicity=ethnicity,
|
|
poses="evocative",
|
|
backside_bias=0.0,
|
|
figure=figure,
|
|
no_plus_women=no_plus_women,
|
|
no_black=no_black,
|
|
minimal_clothing_ratio=-1,
|
|
standard_pose_ratio=-1,
|
|
trigger=active_trigger,
|
|
prepend_trigger_to_prompt=False,
|
|
extra_positive="",
|
|
extra_negative="",
|
|
seed_config=parsed_seed_config,
|
|
women_count=1,
|
|
men_count=0,
|
|
expression_enabled=soft_expression_enabled,
|
|
expression_intensity=soft_expression_intensity,
|
|
character_profile="" if primary_slot else character_profile or "",
|
|
character_cast="",
|
|
)
|
|
soft_row["expression_intensity_source"] = soft_expression_intensity_source
|
|
if primary_slot_context:
|
|
soft_row = _apply_character_context_to_row(soft_row, primary_slot_context)
|
|
soft_row["character_slot"] = primary_slot
|
|
soft_row["character_slot_status"] = "applied:Woman A"
|
|
if not soft_expression_enabled:
|
|
soft_row = _disable_row_expression(soft_row, soft_expression_intensity_source)
|
|
primary_softcore_outfit = _slot_softcore_outfit(primary_slot)
|
|
soft_row["item"] = primary_softcore_outfit or _insta_of_softcore_outfit(soft_content_rng, softcore_level_key)
|
|
soft_row["pose"] = _insta_of_softcore_pose(soft_content_rng, softcore_level_key)
|
|
soft_row["item_label"] = "Insta/OF softcore body exposure" if softcore_level_key == "explicit_nude" else "Insta/OF softcore outfit"
|
|
soft_row["softcore_item_prompt_label"] = _insta_of_softcore_item_prompt_label(softcore_level_key)
|
|
soft_row["custom_item"] = "insta_of_softcore_outfit"
|
|
soft_row["softcore_outfit_policy"] = "character_slot:Woman A" if primary_softcore_outfit else "insta_of_safe_softcore"
|
|
if softcore_level_key == "explicit_nude":
|
|
soft_row["source_scene_text"] = soft_row.get("source_scene_text") or soft_row.get("scene_text", "")
|
|
soft_row["scene_text"] = _body_exposure_scene_text(soft_row.get("scene_text", ""))
|
|
soft_row["pov_character_labels"] = (
|
|
pov_character_labels
|
|
if options["softcore_cast"] == "same_as_hardcore"
|
|
else []
|
|
)
|
|
soft_row["pov_prompt_directive"] = _pov_prompt_directive(soft_row["pov_character_labels"])
|
|
if soft_row["pov_character_labels"]:
|
|
soft_row["source_composition"] = soft_row.get("source_composition") or soft_row.get("composition", "")
|
|
soft_row["composition"] = _pov_composition_prompt(
|
|
soft_row["source_composition"],
|
|
soft_row["pov_character_labels"],
|
|
)
|
|
hard_row = build_prompt(
|
|
category="Hardcore sexual poses",
|
|
subcategory=RANDOM_SUBCATEGORY,
|
|
row_number=row_number,
|
|
start_index=start_index,
|
|
seed=seed,
|
|
clothing="minimal",
|
|
ethnicity=ethnicity,
|
|
poses="evocative",
|
|
backside_bias=0.0,
|
|
figure=figure,
|
|
no_plus_women=no_plus_women,
|
|
no_black=no_black,
|
|
minimal_clothing_ratio=-1,
|
|
standard_pose_ratio=-1,
|
|
trigger=active_trigger,
|
|
prepend_trigger_to_prompt=False,
|
|
extra_positive="",
|
|
extra_negative="",
|
|
seed_config=parsed_seed_config,
|
|
women_count=hard_women_count,
|
|
men_count=hard_men_count,
|
|
expression_enabled=options["hardcore_expression_enabled"],
|
|
expression_intensity=options["hardcore_expression_intensity"],
|
|
character_cast=character_cast or "",
|
|
expression_phase="hardcore",
|
|
)
|
|
hard_row["hardcore_detail_density"] = options["hardcore_detail_density"]
|
|
hard_row["pov_character_labels"] = pov_character_labels
|
|
hard_row["pov_prompt_directive"] = _pov_prompt_directive(pov_character_labels)
|
|
|
|
descriptor = _insta_of_descriptor(soft_row)
|
|
cast_descriptors = _insta_of_cast_descriptors(
|
|
descriptor,
|
|
parsed_seed_config,
|
|
seed,
|
|
row_number,
|
|
ethnicity,
|
|
figure,
|
|
no_plus_women,
|
|
no_black,
|
|
hard_women_count,
|
|
hard_men_count,
|
|
character_slots,
|
|
)
|
|
cast_descriptor_text = _insta_of_prompt_cast_descriptors("; ".join(cast_descriptors))
|
|
soft_cast_descriptor_text = (
|
|
cast_descriptor_text
|
|
if options["softcore_cast"] == "same_as_hardcore"
|
|
else f"Woman A: {descriptor}"
|
|
)
|
|
soft_partner_styling = _insta_of_partner_styling(
|
|
parsed_seed_config,
|
|
seed,
|
|
row_number,
|
|
hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1,
|
|
hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0,
|
|
pov_character_labels if options["softcore_cast"] == "same_as_hardcore" else [],
|
|
character_slot_map,
|
|
)
|
|
if options["softcore_cast"] != "same_as_hardcore":
|
|
soft_partner_styling = {"outfits": [], "pose": ""}
|
|
soft_partner_outfit_text = "; ".join(soft_partner_styling["outfits"])
|
|
platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]]
|
|
soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]]
|
|
hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]]
|
|
hard_camera_mode = options["hardcore_camera_mode"]
|
|
if hard_camera_mode == "same_as_softcore":
|
|
hard_camera_mode = options["softcore_camera_mode"]
|
|
soft_camera_config = _camera_config_with_mode(camera_config, options["softcore_camera_mode"])
|
|
hard_camera_config = _camera_config_with_mode(camera_config, hard_camera_mode)
|
|
soft_camera_config["camera_detail"] = options["camera_detail"]
|
|
hard_camera_config["camera_detail"] = options["camera_detail"]
|
|
soft_camera_directive, soft_camera_config = _camera_directive(soft_camera_config)
|
|
hard_camera_directive, hard_camera_config = _camera_directive(hard_camera_config)
|
|
soft_camera_sentence = f"Camera control: {soft_camera_directive} " if soft_camera_directive else ""
|
|
hard_camera_sentence = f"Camera control: {hard_camera_directive} " if hard_camera_directive else ""
|
|
hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"]
|
|
hard_composition = hard_row["composition"]
|
|
soft_cast = (
|
|
"solo creator setup with Woman A alone"
|
|
if options["softcore_cast"] == "solo"
|
|
else f"non-explicit teaser setup with {_insta_of_cast_phrase(hard_women_count, hard_men_count)}"
|
|
)
|
|
soft_cast_presence = (
|
|
(
|
|
"Frame Woman A from the POV participant's first-person camera in a non-explicit teaser setup; "
|
|
"keep the POV participant off-camera as the viewpoint and implied by camera perspective or foreground cues. "
|
|
)
|
|
if options["softcore_cast"] == "same_as_hardcore" and pov_character_labels
|
|
else (
|
|
"Place Woman A and the listed partners together in a non-explicit teaser pose with no sex act or genital contact. "
|
|
if options["softcore_cast"] == "same_as_hardcore"
|
|
else "Keep the softcore version focused on Woman A alone. "
|
|
)
|
|
)
|
|
soft_cast_styling_sentence = (
|
|
f"Partner softcore styling: {soft_partner_outfit_text}. Cast pose: {soft_partner_styling['pose']}. "
|
|
if options["softcore_cast"] == "same_as_hardcore" and soft_partner_outfit_text
|
|
else ""
|
|
)
|
|
hard_cast = _insta_of_cast_phrase(hard_women_count, hard_men_count)
|
|
character_hardcore_clothing_entries = _character_hardcore_clothing_entries(
|
|
character_slot_map,
|
|
hard_women_count,
|
|
hard_men_count,
|
|
pov_character_labels,
|
|
)
|
|
has_primary_hardcore_clothing = any(entry.startswith("Woman A") for entry in character_hardcore_clothing_entries)
|
|
fallback_hard_clothing_state = "" if has_primary_hardcore_clothing else _insta_of_hardcore_clothing_state(
|
|
options["hardcore_clothing_continuity"],
|
|
soft_row["item"],
|
|
)
|
|
hard_clothing_parts = [part for part in (fallback_hard_clothing_state, *character_hardcore_clothing_entries) if part]
|
|
hard_clothing_state = " ".join(hard_clothing_parts)
|
|
if "body is fully exposed" in hard_clothing_state.lower() or "bare skin unobstructed" in hard_clothing_state.lower():
|
|
hard_scene = _body_exposure_scene_text(hard_scene)
|
|
hard_row["source_scene_text"] = hard_row.get("source_scene_text") or hard_row.get("scene_text", "")
|
|
hard_row["scene_text"] = _body_exposure_scene_text(hard_row.get("scene_text", ""))
|
|
hard_detail_density = options["hardcore_detail_density"]
|
|
hard_detail_directive = {
|
|
"compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ",
|
|
"balanced": "",
|
|
"dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ",
|
|
}[hard_detail_density]
|
|
pov_directive = _pov_prompt_directive(pov_character_labels)
|
|
soft_descriptor_sentence = (
|
|
f"Cast descriptors: {soft_cast_descriptor_text}. "
|
|
if options["softcore_cast"] == "same_as_hardcore"
|
|
else f"Woman A: {descriptor}. "
|
|
)
|
|
|
|
soft_prompt = (
|
|
f"Insta/OF softcore mode: {platform_style}. "
|
|
f"{soft_descriptor_sentence}"
|
|
f"Softcore setup: {soft_level}. Cast: {soft_cast}. "
|
|
f"{soft_cast_presence}"
|
|
f"{soft_cast_styling_sentence}"
|
|
f"{soft_row['softcore_item_prompt_label']}: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. "
|
|
f"{_labeled_expression_sentence('Facial expression', soft_row.get('expression'))}"
|
|
f"Composition: {soft_row['composition']}. "
|
|
f"{soft_camera_sentence}"
|
|
"Keep the softcore version seductive, creator-shot, and non-explicit. "
|
|
f"{soft_row['positive_suffix']}."
|
|
)
|
|
hard_prompt = (
|
|
f"Insta/OF hardcore mode: {platform_style}. "
|
|
f"Hardcore setup: {hard_level}. Cast: {hard_cast}. "
|
|
f"Cast descriptors: {cast_descriptor_text}. "
|
|
f"{pov_directive + ' ' if pov_directive else ''}"
|
|
f"{'Keep Woman A visually central from the POV camera. ' if pov_character_labels else 'Keep Woman A visually central. '}"
|
|
f"{hard_clothing_state} "
|
|
f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. "
|
|
f"Setting: {hard_scene}. "
|
|
f"{_labeled_expression_sentence('Facial expressions', hard_row.get('expression'))}"
|
|
f"Composition: {hard_composition}. "
|
|
f"{hard_detail_directive}"
|
|
f"{hard_camera_sentence}"
|
|
f"{hard_row['positive_suffix']}."
|
|
)
|
|
if extra_positive.strip():
|
|
soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}"
|
|
hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}"
|
|
|
|
soft_prompt = _insta_of_active_trigger(soft_prompt, active_trigger, bool(prepend_trigger_to_prompt))
|
|
hard_prompt = _insta_of_active_trigger(hard_prompt, active_trigger, bool(prepend_trigger_to_prompt))
|
|
soft_negative = _combined_negative(INSTA_OF_SOFT_NEGATIVE, extra_negative)
|
|
hard_negative = _combined_negative(INSTA_OF_NEGATIVE, extra_negative)
|
|
soft_caption_parts = [
|
|
active_trigger,
|
|
"Insta/OF softcore mode",
|
|
descriptor,
|
|
soft_level,
|
|
soft_row["item"],
|
|
soft_row["pose"],
|
|
soft_partner_outfit_text,
|
|
soft_partner_styling["pose"],
|
|
soft_row["scene_text"],
|
|
soft_row["composition"],
|
|
_camera_caption_text(soft_camera_config) if soft_camera_directive else "",
|
|
]
|
|
soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip())
|
|
hard_caption_parts = [
|
|
active_trigger,
|
|
"Insta/OF hardcore mode",
|
|
"Woman A",
|
|
descriptor,
|
|
hard_cast,
|
|
hard_row["role_graph"],
|
|
hard_row["item"],
|
|
hard_scene,
|
|
hard_composition,
|
|
_camera_caption_text(hard_camera_config) if hard_camera_directive else "",
|
|
]
|
|
hard_caption = ", ".join(str(part).strip() for part in hard_caption_parts if str(part).strip())
|
|
metadata = {
|
|
"mode": "Insta/OF",
|
|
"options": options,
|
|
"shared_descriptor": descriptor,
|
|
"shared_cast_descriptors": cast_descriptors,
|
|
"pov_character_labels": pov_character_labels,
|
|
"pov_prompt_directive": pov_directive,
|
|
"softcore_partner_styling": soft_partner_styling,
|
|
"character_hardcore_clothing": character_hardcore_clothing_entries,
|
|
"hardcore_clothing_state": hard_clothing_state,
|
|
"hardcore_detail_density": hard_detail_density,
|
|
"softcore_prompt": soft_prompt,
|
|
"hardcore_prompt": hard_prompt,
|
|
"softcore_negative_prompt": soft_negative,
|
|
"hardcore_negative_prompt": hard_negative,
|
|
"softcore_caption": soft_caption,
|
|
"hardcore_caption": hard_caption,
|
|
"softcore_row": soft_row,
|
|
"hardcore_row": hard_row,
|
|
"hardcore_women_count": hard_women_count,
|
|
"hardcore_men_count": hard_men_count,
|
|
"character_cast_slots": character_slots,
|
|
"character_slot_labels": sorted(character_slot_map),
|
|
"softcore_camera_config": soft_camera_config,
|
|
"hardcore_camera_config": hard_camera_config,
|
|
"softcore_camera_directive": soft_camera_directive,
|
|
"hardcore_camera_directive": hard_camera_directive,
|
|
}
|
|
return metadata
|