2446 lines
91 KiB
Python
2446 lines
91 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import random
|
|
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"
|
|
|
|
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"),
|
|
}
|
|
|
|
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 = {
|
|
"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_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 cock in pussy and one cock 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 = (
|
|
" cock",
|
|
"cock ",
|
|
"cocks",
|
|
"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 "cocks" 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_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 _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,
|
|
) -> str:
|
|
return json.dumps(
|
|
{
|
|
"category_seed": int(category_seed),
|
|
"subcategory_seed": int(subcategory_seed),
|
|
"content_seed": int(content_seed),
|
|
"person_seed": int(person_seed),
|
|
"scene_seed": int(scene_seed),
|
|
"pose_seed": int(pose_seed),
|
|
"role_seed": int(role_seed),
|
|
"expression_seed": int(expression_seed),
|
|
"composition_seed": int(composition_seed),
|
|
},
|
|
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 _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 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",
|
|
) -> 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,
|
|
},
|
|
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, str]:
|
|
defaults = {
|
|
"camera_mode": "standard",
|
|
"shot_size": "auto",
|
|
"angle": "auto",
|
|
"lens": "auto",
|
|
"distance": "auto",
|
|
"orientation": "auto",
|
|
"phone_visibility": "auto",
|
|
"priority": "strong",
|
|
}
|
|
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}
|
|
return {
|
|
"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"]),
|
|
}
|
|
|
|
|
|
def _camera_config_with_mode(camera_config: str | dict[str, Any] | None, camera_mode: str) -> dict[str, str]:
|
|
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, str]]:
|
|
parsed = _parse_camera_config(camera_config)
|
|
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"]],
|
|
]
|
|
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 _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)
|
|
row["caption"] = f"{row.get('caption', '').rstrip()}, {parsed['camera_mode'].replace('_', ' ')} camera framing"
|
|
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 _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 == "asian" 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)}."
|
|
|
|
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 climax 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 explicit climax pose with one hand on his cock, body angled toward the camera, and visible ejaculation detail."
|
|
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 or toy 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 or toy 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 cock 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} climaxes over {b}'s body while {b} keeps eye contact and one hand on his cock."
|
|
else:
|
|
graph = f"{a} and {b} keep explicit cock 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} and {man} are in mutual oral contact with mouth-to-genital contact visible."
|
|
elif any(term in item_text for term in ("cunnilingus", "pussy licking", "tongue on pussy", "mouth on pussy")):
|
|
graph = f"{man} gives oral to {woman} while {woman}'s thighs are held open for the camera."
|
|
else:
|
|
graph = f"{woman} gives oral to {man} 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} penetrates {woman} while {third} adds a second point of contact from the front."
|
|
else:
|
|
graph = f"{man} penetrates {woman} while a toy adds a second point of contact."
|
|
elif people_count >= 3:
|
|
graph = f"{man} penetrates {woman} while {third} gives oral contact from the front."
|
|
else:
|
|
graph = f"{man} penetrates {woman} anally while keeping her hips held open."
|
|
elif "threesome" in slug:
|
|
graph = f"{man} penetrates {woman} while {third or any_person({woman, man})} uses mouth or hands on the exposed body."
|
|
elif "group" in slug or "orgy" in slug:
|
|
graph = f"{man} penetrates {woman} while surrounding partners give oral contact and keep hands on hips, breasts, and thighs."
|
|
elif "cumshot" in slug or "climax" in slug:
|
|
graph = f"{man} climaxes on {woman}'s body while {woman} stays posed with thighs open and direct eye contact."
|
|
else:
|
|
graph = f"{man} and {woman} keep penetration and body contact visible."
|
|
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_intensity: float,
|
|
) -> 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)
|
|
subject_type = context["subject_type"]
|
|
role_graph = _role_graph(role_rng, subcategory, context, item_axis_values)
|
|
|
|
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_entries = _compatible_entries(
|
|
_expression_entries_for_intensity(_expression_pool(category, subcategory, item), expression_intensity),
|
|
women_count,
|
|
men_count,
|
|
)
|
|
expression = _choose_text(expression_rng, expression_entries)
|
|
if subject_type in ("couple", "group") and ";" not in expression:
|
|
expression = f"{expression}; {_choose_text(expression_rng, expression_entries)}"
|
|
composition = _choose_text(
|
|
composition_rng,
|
|
_compatible_entries(_composition_pool(category, subcategory, item, subject_type), women_count, men_count),
|
|
)
|
|
|
|
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,
|
|
"expression_intensity": expression_intensity,
|
|
"composition": composition,
|
|
"composition_prompt": _composition_prompt(composition),
|
|
"role_graph": role_graph,
|
|
"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)
|
|
caption = _format(caption_template, context)
|
|
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,
|
|
"cast_summary": context.get("cast_summary", ""),
|
|
"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 {},
|
|
"source": "json_category",
|
|
}
|
|
)
|
|
if context.get("figure"):
|
|
row["figure"] = context["figure"]
|
|
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,
|
|
) -> 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 in ("any", "asian", "white_asian") else "any"
|
|
poses = poses if poses in ("standard", "evocative") else "standard"
|
|
figure = figure if figure in ("curvy", "balanced", "bombshell") else "curvy"
|
|
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_intensity,
|
|
)
|
|
|
|
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["expression_intensity"] = expression_intensity
|
|
return row
|
|
|
|
|
|
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 but without explicit sex",
|
|
"implied_nude": "implied nude creator set, strategically covered body, sensual but no visible sex act",
|
|
"explicit_tease": "explicit adult tease, nudity can be visible, but no penetration or partnered sex act",
|
|
}
|
|
|
|
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_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"
|
|
)
|
|
|
|
|
|
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",
|
|
softcore_camera_mode: str = "handheld_selfie",
|
|
hardcore_camera_mode: str = "same_as_softcore",
|
|
softcore_expression_intensity: float = 0.45,
|
|
hardcore_expression_intensity: float = 0.85,
|
|
) -> str:
|
|
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,
|
|
"softcore_camera_mode": softcore_camera_mode,
|
|
"hardcore_camera_mode": hardcore_camera_mode,
|
|
"softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45),
|
|
"hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85),
|
|
},
|
|
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",
|
|
"softcore_camera_mode": "handheld_selfie",
|
|
"hardcore_camera_mode": "same_as_softcore",
|
|
"softcore_expression_intensity": 0.45,
|
|
"hardcore_expression_intensity": 0.85,
|
|
}
|
|
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["softcore_camera_mode"] = parsed["softcore_camera_mode"] if parsed["softcore_camera_mode"] in CAMERA_MODE_PROMPTS else defaults["softcore_camera_mode"]
|
|
if parsed["hardcore_camera_mode"] not in CAMERA_MODE_PROMPTS and parsed["hardcore_camera_mode"] != "same_as_softcore":
|
|
parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"]
|
|
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"],
|
|
)
|
|
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:
|
|
age = str(row.get("age_band") or row.get("age") or "").strip()
|
|
age = " ".join(age.split())
|
|
age = age.removesuffix(" adults").removesuffix(" adult").strip()
|
|
pieces = [
|
|
f"{age} adult woman" if age else "adult woman",
|
|
row.get("body_phrase"),
|
|
row.get("skin"),
|
|
row.get("hair"),
|
|
row.get("eyes"),
|
|
]
|
|
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
|
|
|
|
|
def _insta_of_descriptor_from_context(context: dict[str, Any]) -> str:
|
|
age = str(context.get("age") or "").strip()
|
|
age = " ".join(age.split())
|
|
age = age.removesuffix(" adults").removesuffix(" adult").strip()
|
|
subject = str(context.get("subject") or context.get("subject_type") or "person").strip()
|
|
pieces = [
|
|
f"{age} adult {subject}".strip(),
|
|
context.get("body_phrase"),
|
|
context.get("skin"),
|
|
context.get("hair"),
|
|
context.get("eyes"),
|
|
]
|
|
return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip())
|
|
|
|
|
|
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,
|
|
) -> list[str]:
|
|
descriptors = [f"Woman A / primary creator: {primary_descriptor}"]
|
|
rng = _axis_rng(seed_config, "person", seed, row_number + 997)
|
|
for index in range(max(0, women_count - 1)):
|
|
label = chr(ord("B") + index)
|
|
context = _appearance_for_subject(rng, "woman", ethnicity, figure, no_plus_women, no_black)
|
|
descriptors.append(f"Woman {label}: {_insta_of_descriptor_from_context(context)}")
|
|
for index in range(max(0, men_count)):
|
|
label = chr(ord("A") + index)
|
|
context = _appearance_for_subject(rng, "man", ethnicity, figure, no_plus_women, no_black)
|
|
descriptors.append(f"Man {label}: {_insta_of_descriptor_from_context(context)}")
|
|
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"]
|
|
|
|
|
|
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 in the same room 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_partner_styling(
|
|
seed_config: dict[str, int],
|
|
seed: int,
|
|
row_number: int,
|
|
women_count: int,
|
|
men_count: int,
|
|
) -> 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)
|
|
outfits: list[str] = []
|
|
for index in range(max(0, women_count - 1)):
|
|
label = chr(ord("B") + index)
|
|
outfits.append(f"Woman {label} wears {g.choose(content_rng, g.WOMEN_CLOTHES_MINIMAL)}")
|
|
for index in range(max(0, men_count)):
|
|
label = chr(ord("A") + index)
|
|
outfits.append(f"Man {label} wears {g.choose(content_rng, g.MEN_CLOTHES_MINIMAL)}")
|
|
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,
|
|
camera_config: str | dict[str, Any] | None = None,
|
|
extra_positive: str = "",
|
|
extra_negative: str = "",
|
|
) -> dict[str, Any]:
|
|
options = _parse_insta_of_options(options_json)
|
|
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)
|
|
|
|
soft_row = build_prompt(
|
|
category="Provocative erotic clothes",
|
|
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=1,
|
|
men_count=0,
|
|
expression_intensity=options["softcore_expression_intensity"],
|
|
)
|
|
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_intensity=options["hardcore_expression_intensity"],
|
|
)
|
|
|
|
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,
|
|
)
|
|
cast_descriptor_text = "; ".join(cast_descriptors)
|
|
soft_cast_descriptor_text = (
|
|
cast_descriptor_text
|
|
if options["softcore_cast"] == "same_as_hardcore"
|
|
else f"Woman A / primary creator: {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,
|
|
)
|
|
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_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 = soft_row["composition"] if options["continuity"] == "same_creator_same_room" else hard_row["composition"]
|
|
soft_cast = (
|
|
"solo creator setup; the primary creator is alone in the softcore version"
|
|
if options["softcore_cast"] == "solo"
|
|
else f"non-explicit teaser setup with the same adult cast as the hardcore version: {_insta_of_cast_phrase(hard_women_count, hard_men_count)}"
|
|
)
|
|
soft_cast_presence = (
|
|
"Show the same listed adult cast together in the softcore version, present in the same location 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}. Shared softcore 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)
|
|
|
|
soft_prompt = (
|
|
f"Insta/OF softcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. "
|
|
f"Softcore setup: {soft_level}. Cast continuity: {soft_cast}. "
|
|
f"Shared cast descriptors: {soft_cast_descriptor_text}. "
|
|
f"{soft_cast_presence}"
|
|
f"{soft_cast_styling_sentence}"
|
|
f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. "
|
|
f"Facial expression: {soft_row['expression']}. Composition: {soft_row['composition']}. "
|
|
f"{soft_camera_sentence}"
|
|
"Keep the softcore version adult-only, consensual, seductive, creator-shot, and non-explicit. "
|
|
f"{soft_row['positive_suffix']} Avoid: {INSTA_OF_SOFT_NEGATIVE}."
|
|
)
|
|
hard_prompt = (
|
|
f"Insta/OF hardcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. "
|
|
f"Hardcore setup: {hard_level}. Cast: {hard_cast}. "
|
|
f"Shared cast descriptors: {cast_descriptor_text}. "
|
|
"Apply the shared descriptor to the most visually central woman, keeping her continuous with the softcore version. "
|
|
f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. "
|
|
f"Setting: {hard_scene}. Facial expressions: {hard_row['expression']}. Composition: {hard_composition}. "
|
|
f"{hard_camera_sentence}"
|
|
"All participants are consenting adults 21+. "
|
|
f"{hard_row['positive_suffix']} Avoid: {INSTA_OF_NEGATIVE}."
|
|
)
|
|
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"],
|
|
f"{soft_camera_config['camera_mode'].replace('_', ' ')} camera",
|
|
]
|
|
soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip())
|
|
hard_caption = (
|
|
f"{active_trigger}, Insta/OF hardcore mode, same primary creator descriptor, {descriptor}, "
|
|
f"{hard_cast}, {hard_row['role_graph']}, {hard_row['item']}, {hard_scene}, {hard_composition}, "
|
|
f"{hard_camera_config['camera_mode'].replace('_', ' ')} camera"
|
|
)
|
|
metadata = {
|
|
"mode": "Insta/OF",
|
|
"options": options,
|
|
"shared_descriptor": descriptor,
|
|
"shared_cast_descriptors": cast_descriptors,
|
|
"softcore_partner_styling": soft_partner_styling,
|
|
"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,
|
|
"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
|