commit d342b418103da2e2037decf21331a2b66a129fc3 Author: Ethanfel Date: Wed Jun 24 09:24:51 2026 +0200 Initial ComfyUI prompt builder nodes diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c903b31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ diff --git a/IMPROVEMENTS.md b/IMPROVEMENTS.md new file mode 100644 index 0000000..8970aa8 --- /dev/null +++ b/IMPROVEMENTS.md @@ -0,0 +1,81 @@ +# Improvement Path + +## Done + +- ComfyUI prompt node. +- JSON-defined main categories and subcategories. +- Compositional item generators with `item_templates` and `item_axes`. +- Softcore/custom clothing categories. +- Explicit erotic clothing category. +- Hardcore sexual-pose category. +- Configurable cast counts with `women_count` and `men_count`. +- Per-axis seed control through `SxCP Seed Control`. +- Cast-aware filtering for subcategories, templates, and axis values. +- Role graph generation for configured hardcore casts. + +## Highest-Value Next Steps + +1. Explicitness preset + + Add a node input like: + + - `softcore` + - `nude` + - `explicit` + - `hardcore` + + Then categories can share the same cast/person/scene system while swapping + the pose/content pools and negative prompts. + +2. Anatomy clarity axis + + Add a controlled axis for visual clarity: + + - full-body view + - hips-focused view + - genital-contact view + - face-and-body view + - mirror view + + This helps hardcore outputs read as sex scenes instead of vague tangled + bodies. + +3. Outfit and pose compatibility + + Hardcore pose categories should optionally pull from erotic clothing or nude + accessory categories. Add an input or template field for: + + - clothed sex + - lingerie sex + - nude sex + - fetishwear sex + - wet/shower sex + +4. More seed/reroll utility nodes + + Add tiny helper nodes: + + - `SxCP Reroll Pose Seed` + - `SxCP Reroll Scene Seed` + - `SxCP Reroll Person Seed` + + These can output a modified `seed_config` while preserving the other locked + seeds. + +5. Validation and preview tools + + Add a local validator that reports: + + - category and subcategory counts + - template placeholder errors + - axis size and variation count + - impossible cast/template combinations + - missing scene/pose/expression pools + +## Hardcore-Specific Improvement Order + +1. Split hardcore into act families with deeper compatibility rules. +2. Add explicitness preset and prompt-strength controls. +3. Add anatomy/camera clarity axis. +4. Add outfit-state control for nude/lingerie/fetish/clothed sex. +5. Add validation so impossible prompts are caught before ComfyUI generation. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc90b5c --- /dev/null +++ b/README.md @@ -0,0 +1,278 @@ +# ComfyUI Prompt Builder + +Custom ComfyUI node for building prompts from the existing `generate_prompt_batches.py` +generator without writing image batches. + +The node is registered as: + +- `prompt_builder / SxCP Prompt Builder` +- `prompt_builder / SxCP Seed Control` +- `prompt_builder / SxCP Caption Naturalizer` + +It outputs: + +- `prompt` +- `negative_prompt` +- `caption` +- `metadata_json` +- `category` +- `subcategory` + +`SxCP Seed Control` outputs `seed_config`, which can be connected to the prompt +builder's optional `seed_config` input. + +`SxCP Caption Naturalizer` rewrites tag-like captions or labeled prompts into +more natural language. Connect the prompt builder's `metadata_json` output to +`source_text` for the cleanest result. You can also connect `caption` or +`prompt`; in that case the node falls back to prompt-label parsing or comma-tag +cleanup. + +Naturalizer controls: + +- `input_hint`: `auto`, `metadata_json`, or `caption_or_prompt`. +- `detail_level`: `concise`, `balanced`, or `dense`. +- `style_policy`: `drop_style_tail` removes old fixed style tails; `keep_style_terms` + keeps style descriptions in the rewritten text. +- `trigger`: defaults to `sxcppnl7`. +- `include_trigger`: prepends the trigger as its own sentence. + +It outputs: + +- `natural_caption` +- `method` + +## Built-In Categories + +The node keeps the original generator controls: + +- `category`: `auto_weighted`, `woman`, `man`, `couple`, `group_or_layout`, or a custom JSON category. +- `clothing`: `full` or `minimal`. +- `minimal_clothing_ratio`: `-1` disables mixing; `0.0` to `1.0` mixes minimal/full clothing. +- `ethnicity`: `any`, `asian`, `white_asian`. +- `poses`: `standard` or `evocative`. +- `standard_pose_ratio`: `-1` disables mixing; `0.0` to `1.0` mixes standard/evocative poses. +- `backside_bias`: `0.0` to `1.0`, applies to evocative single-subject poses. +- `figure`: `curvy`, `balanced`, `bombshell`. +- `no_plus_women`: excludes plus-size women. +- `no_black`: excludes Black/African-coded women from women-focused pools. + +`auto_weighted` uses the original batch mix: mostly women, then men, couples, and +group/layout rows. Direct categories generate only that selected category. + +## Custom Categories + +Add or edit JSON files in `categories/*.json`. Each file can define new main +categories, subcategories, and optional extensions to the built-in pools. Restart +or reload ComfyUI after changing JSON so dropdown choices are rebuilt. + +Included JSON categories: + +- `Casual clothes` +- `Provocative erotic clothes` +- `Hardcore sexual poses` + +Example: + +```json +{ + "version": 1, + "categories": [ + { + "name": "Casual clothes", + "slug": "casual_clothes", + "subject_type": "woman", + "item_label": "Clothing", + "style": "tasteful adult fashion-editorial coloured-pencil comic illustration", + "subcategories": [ + { + "name": "Streetwear", + "slug": "streetwear", + "items": [ + "oversized hoodie with slim jeans and clean sneakers", + "cropped bomber jacket with cargo pants and chunky trainers" + ], + "scenes": [ + { + "slug": "city_crosswalk", + "prompt": "sunlit city crosswalk with storefront reflections" + } + ], + "poses": [ + "standing with one hand in a pocket", + "walking forward with a casual runway stride" + ] + } + ] + } + ] +} +``` + +Custom categories do not need a Python generator. If no `prompt_template` is +provided, the node uses a generic composer that selects subject appearance, +scene, pose, expression, composition, and a random item from the selected +subcategory. + +For large categories, prefer `item_templates` plus `item_axes` instead of writing +every final item by hand: + +```json +{ + "name": "Example clothes", + "subject_type": "woman", + "subcategories": [ + { + "name": "Lingerie", + "item_templates": [ + "{color} {fabric} {top} with {bottom} and {stockings}" + ], + "item_axes": { + "color": ["black", "red", "ivory"], + "fabric": ["lace", "satin", "transparent mesh"], + "top": ["balconette bra", "open-cup bra"], + "bottom": ["matching thong", "high-cut g-string"], + "stockings": ["thigh-high stockings", "fishnet stockings"] + } + } + ] +} +``` + +The node chooses one template, fills each placeholder from the matching axis, +then records the selected axis values in `metadata_json`. + +Supported `subject_type` values: + +- `single_any` +- `woman` +- `man` +- `couple` +- `group` +- `layout` +- `scene` +- `configured_cast` + +`configured_cast` uses the node's `women_count` and `men_count` inputs. It adds +template fields for `{women_count}`, `{men_count}`, `{person_count}`, +`{cast_summary}`, `{scene_kind}`, and `{role_graph}`. This is intended for +categories where the same prompt generator should support couples, threesomes, +and larger groups. + +`{role_graph}` is a generated choreography sentence that assigns roles to the +cast, such as who penetrates, who receives oral, and who joins from the side. +It is currently most useful for `Hardcore sexual poses`. + +Subcategories, templates, and axis values can declare cast constraints: + +```json +{ + "name": "Threesomes", + "min_people": 3, + "item_axes": { + "act": [ + { + "text": "strap-on penetration and cunnilingus", + "cast": "women_only" + }, + { + "text": "male/male oral and anal contact", + "cast": "men_only" + }, + { + "text": "front-and-back penetration", + "cast": "mixed" + } + ] + } +} +``` + +Supported constraints: + +- `min_women`, `max_women` +- `min_men`, `max_men` +- `min_people`, `max_people` +- `cast` or `requires`: `women_only`, `men_only`, `mixed`, `has_women`, + `has_men`, `solo`, `couple`, `threesome`, `group` + +If an exact subcategory is not compatible with `women_count` and `men_count`, +the node raises a clear error instead of generating an impossible prompt. + +Use the `subcategory` dropdown to select either `random` or an exact +`Main category / Subcategory` path. Exact paths override the `category` dropdown, +which is useful because ComfyUI does not provide dependent dropdowns from Python +alone. + +## Seed Control + +The main `seed` input is still the default master seed. Connect `SxCP Seed +Control` to `seed_config` when you want to lock or vary specific axes. + +Seed values: + +- `-1`: follow the main seed. +- `0` or higher: override only that axis. + +Axes: + +- `category_seed`: custom category selection when `custom_random` is used. +- `subcategory_seed`: random subcategory selection. +- `content_seed`: generated item content, such as outfit wording. +- `person_seed`: appearance/person selection. +- `scene_seed`: scene/environment selection. +- `pose_seed`: body pose selection. For `Hardcore sexual poses`, this also drives the generated sexual pose content. +- `role_seed`: participant choreography for `{role_graph}`. If left at `-1`, it follows `pose_seed`. +- `expression_seed`: facial expression selection. +- `composition_seed`: camera/composition selection. + +Example workflow: if the person and scene are right but the pose is wrong, keep +`person_seed` and `scene_seed` fixed, then change `pose_seed`. If the cast roles +are wrong but the act wording is good, change `role_seed`. If the clothing or +sexual act wording is wrong, change `content_seed`; for pose-driven categories, +change `pose_seed`. + +## Pool Extensions + +Use `pool_extensions` to add new entries to built-in pools without editing +Python: + +```json +{ + "pool_extensions": { + "women_clothes": ["relaxed high-waist jeans with a fitted ribbed tank top"], + "men_clothes": ["clean white T-shirt with relaxed jeans and canvas sneakers"], + "poses": ["standing with a relaxed hand-on-hip pose"], + "expressions": ["easygoing half-smile"], + "scenes": [ + { + "slug": "city_cafe", + "prompt": "quiet city cafe terrace with morning light and small round tables" + } + ] + } +} +``` + +Known extension pools: + +`women_clothes`, `women_clothes_minimal`, `men_clothes`, `men_clothes_minimal`, +`couple_outfits`, `couple_outfits_minimal`, `poses`, `evocative_poses`, +`backside_poses`, `expressions`, `compositions`, `props`, `figure_curvy`, +`figure_athletic`, `figure_bombshell`, `scenes`, `group_scenes`, +`layouts_full`, `layouts_minimal`, `group_compositions`, `group_ages`. + +## Templates + +A category, subcategory, or individual item can provide `prompt_template` and +`caption_template`. Templates can use these fields: + +`{trigger}`, `{main_category}`, `{subcategory}`, `{item}`, `{item_label}`, +`{subject}`, `{subject_phrase}`, `{age}`, `{body}`, `{body_phrase}`, `{skin}`, +`{hair}`, `{eyes}`, `{figure}`, `{scene}`, `{pose}`, `{expression}`, +`{composition}`, `{style}`, `{positive_suffix}`, `{negative_prompt}`, +`{women_count}`, `{men_count}`, `{person_count}`, `{cast_summary}`, +`{scene_kind}`, `{role_graph}`. + +If `prepend_trigger_to_prompt` is enabled, the node prepends the trigger to the +positive prompt. Disable it for output closer to the original script's `prompt` +field. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..c3f1d46 --- /dev/null +++ b/__init__.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import json + +try: + from .prompt_builder import build_prompt, build_seed_config_json, category_choices, subcategory_choices + from .caption_naturalizer import naturalize_caption +except ImportError: + from prompt_builder import build_prompt, build_seed_config_json, category_choices, subcategory_choices + from caption_naturalizer import naturalize_caption + + +class SxCPPromptBuilder: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "category": (category_choices(), {"default": "auto_weighted"}), + "subcategory": (subcategory_choices(), {"default": "random"}), + "row_number": ("INT", {"default": 1, "min": 1, "max": 1000000, "step": 1}), + "start_index": ("INT", {"default": 41, "min": 1, "max": 1000000, "step": 1}), + "seed": ("INT", {"default": 20260614, "min": 0, "max": 0xFFFFFFFF, "step": 1}), + "clothing": (["full", "minimal"], {"default": "full"}), + "ethnicity": (["any", "asian", "white_asian"], {"default": "any"}), + "poses": (["standard", "evocative"], {"default": "standard"}), + "backside_bias": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "figure": (["curvy", "balanced", "bombshell"], {"default": "curvy"}), + "no_plus_women": ("BOOLEAN", {"default": False}), + "no_black": ("BOOLEAN", {"default": False}), + "women_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), + "men_count": ("INT", {"default": 1, "min": 0, "max": 12, "step": 1}), + "minimal_clothing_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "standard_pose_ratio": ("FLOAT", {"default": -1.0, "min": -1.0, "max": 1.0, "step": 0.01}), + "trigger": ("STRING", {"default": "sxcpinup_coloredpencil"}), + "prepend_trigger_to_prompt": ("BOOLEAN", {"default": True}), + }, + "optional": { + "seed_config": ("STRING", {"default": "", "multiline": True}), + "extra_positive": ("STRING", {"default": "", "multiline": True}), + "extra_negative": ("STRING", {"default": "", "multiline": True}), + }, + } + + RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "STRING", "STRING") + RETURN_NAMES = ("prompt", "negative_prompt", "caption", "metadata_json", "category", "subcategory") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + category, + subcategory, + row_number, + start_index, + seed, + clothing, + ethnicity, + poses, + backside_bias, + figure, + no_plus_women, + no_black, + women_count, + men_count, + minimal_clothing_ratio, + standard_pose_ratio, + trigger, + prepend_trigger_to_prompt, + seed_config="", + extra_positive="", + extra_negative="", + ): + row = build_prompt( + category=category, + subcategory=subcategory, + row_number=row_number, + start_index=start_index, + seed=seed, + clothing=clothing, + ethnicity=ethnicity, + poses=poses, + backside_bias=backside_bias, + figure=figure, + no_plus_women=no_plus_women, + no_black=no_black, + women_count=women_count, + men_count=men_count, + minimal_clothing_ratio=minimal_clothing_ratio, + standard_pose_ratio=standard_pose_ratio, + trigger=trigger, + prepend_trigger_to_prompt=prepend_trigger_to_prompt, + extra_positive=extra_positive or "", + extra_negative=extra_negative or "", + seed_config=seed_config or "", + ) + return ( + row["prompt"], + row["negative_prompt"], + row["caption"], + json.dumps(row, ensure_ascii=True, sort_keys=True), + row.get("main_category", category), + row.get("subcategory", subcategory), + ) + + +class SxCPSeedControl: + @classmethod + def INPUT_TYPES(cls): + seed_spec = {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1} + return { + "required": { + "category_seed": ("INT", seed_spec), + "subcategory_seed": ("INT", seed_spec), + "content_seed": ("INT", seed_spec), + "person_seed": ("INT", seed_spec), + "scene_seed": ("INT", seed_spec), + "pose_seed": ("INT", seed_spec), + "role_seed": ("INT", seed_spec), + "expression_seed": ("INT", seed_spec), + "composition_seed": ("INT", seed_spec), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("seed_config",) + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + category_seed, + subcategory_seed, + content_seed, + person_seed, + scene_seed, + pose_seed, + role_seed, + expression_seed, + composition_seed, + ): + return ( + build_seed_config_json( + category_seed=category_seed, + subcategory_seed=subcategory_seed, + content_seed=content_seed, + person_seed=person_seed, + scene_seed=scene_seed, + pose_seed=pose_seed, + role_seed=role_seed, + expression_seed=expression_seed, + composition_seed=composition_seed, + ), + ) + + +class SxCPCaptionNaturalizer: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "source_text": ("STRING", {"default": "", "multiline": True}), + "input_hint": (["auto", "metadata_json", "caption_or_prompt"], {"default": "auto"}), + "detail_level": (["balanced", "concise", "dense"], {"default": "balanced"}), + "style_policy": (["drop_style_tail", "keep_style_terms"], {"default": "drop_style_tail"}), + "trigger": ("STRING", {"default": "sxcppnl7"}), + "include_trigger": ("BOOLEAN", {"default": True}), + }, + "optional": { + "metadata_json": ("STRING", {"default": "", "multiline": True}), + }, + } + + RETURN_TYPES = ("STRING", "STRING") + RETURN_NAMES = ("natural_caption", "method") + FUNCTION = "build" + CATEGORY = "prompt_builder" + + def build( + self, + source_text, + input_hint, + detail_level, + style_policy, + trigger, + include_trigger, + metadata_json="", + ): + return naturalize_caption( + source_text=source_text or "", + metadata_json=metadata_json or "", + input_hint=input_hint, + trigger=trigger, + include_trigger=include_trigger, + detail_level=detail_level, + style_policy=style_policy, + ) + + +NODE_CLASS_MAPPINGS = { + "SxCPPromptBuilder": SxCPPromptBuilder, + "SxCPSeedControl": SxCPSeedControl, + "SxCPCaptionNaturalizer": SxCPCaptionNaturalizer, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "SxCPPromptBuilder": "SxCP Prompt Builder", + "SxCPSeedControl": "SxCP Seed Control", + "SxCPCaptionNaturalizer": "SxCP Caption Naturalizer", +} diff --git a/caption_naturalizer.py b/caption_naturalizer.py new file mode 100644 index 0000000..c75cc20 --- /dev/null +++ b/caption_naturalizer.py @@ -0,0 +1,565 @@ +from __future__ import annotations + +import json +import re +from typing import Any + + +OLD_TRIGGER = "sxcpinup_coloredpencil" +DEFAULT_TRIGGER = "sxcppnl7" + +STYLE_TAILS = [ + ", coloured pencil comic illustration, crisp linework, hatching, soft pastel palette, warm sensual lighting, textured parchment paper", + ", coloured pencil comic illustration, crisp linework, hatching, soft pastel palette, warm sensual lighting, textured paper", +] + +PROMPT_FIELD_LABELS = ( + "Ages", + "Body types", + "Cast", + "Scene", + "Setting", + "Pose", + "Sexual pose", + "Facial expression", + "Facial expressions", + "Clothing", + "Erotic outfit", + "Prop/detail", + "Composition", + "Role graph", + "Use", + "Avoid", +) + +ITEM_LABELS = ( + "Sexual pose", + "Erotic outfit", + "Clothing", +) + + +def _clean_text(value: Any) -> str: + text = "" if value is None else str(value) + text = text.replace("\n", " ") + text = re.sub(r"\s+", " ", text).strip() + text = re.sub(r"\s+([,.;:])", r"\1", text) + return text + + +def _cap_first(text: str) -> str: + text = _clean_text(text).strip(" ,") + return text[:1].upper() + text[1:] if text else "" + + +def _article(noun_phrase: str) -> str: + word = noun_phrase.lstrip().lower() + if word.startswith("hour") or word[:1] in "aeiou": + return "an" + return "a" + + +def _sentence(text: str) -> str: + text = _clean_text(text).strip(" ,;") + if not text: + return "" + if text[-1] not in ".!?": + text += "." + return _cap_first(text) + + +def _join_sentences(parts: list[str]) -> str: + return " ".join(part for part in (_sentence(part) for part in parts) if part) + + +def _human_join(parts: list[str]) -> str: + parts = [part for part in (_clean_text(part) for part in parts) if part] + if len(parts) <= 1: + return "".join(parts) + if len(parts) == 2: + return f"{parts[0]} and {parts[1]}" + return f"{', '.join(parts[:-1])}, and {parts[-1]}" + + +def _strip_style_tail(text: str) -> str: + text = _clean_text(text) + for tail in STYLE_TAILS: + if text.endswith(tail): + return text[: -len(tail)].strip(" ,") + return text + + +def _remove_trigger(text: str, trigger: str) -> str: + text = _clean_text(text).strip(" ,") + for candidate in (trigger, OLD_TRIGGER, DEFAULT_TRIGGER): + candidate = candidate.strip() + if not candidate: + continue + if text.lower().startswith(candidate.lower() + ","): + return text[len(candidate) + 1 :].strip(" ,") + if text.lower().startswith(candidate.lower() + "."): + return text[len(candidate) + 1 :].strip(" ,") + if text.lower() == candidate.lower(): + return "" + return text + + +def _with_trigger(text: str, trigger: str, include_trigger: bool) -> str: + text = _join_sentences([text]) if "." not in text else _clean_text(text) + trigger = _clean_text(trigger or DEFAULT_TRIGGER) + if not include_trigger or not trigger: + return text + if text.lower().startswith(trigger.lower() + "."): + return text + return f"{trigger}. {text}" + + +def _maybe_json(text: str) -> dict[str, Any] | None: + text = _clean_text(text) + if not text or not text.startswith("{"): + return None + try: + value = json.loads(text) + except json.JSONDecodeError: + return None + return value if isinstance(value, dict) else None + + +def _row_from_inputs(source_text: str, metadata_json: str, input_hint: str) -> tuple[dict[str, Any] | None, str]: + candidates: list[tuple[str, str]] = [] + if input_hint in ("auto", "metadata_json"): + candidates.append((metadata_json, "metadata_json")) + candidates.append((source_text, "source_json")) + for text, method in candidates: + row = _maybe_json(text) + if row is not None: + return row, method + return None, "text" + + +def _prompt_field(text: str, label: str) -> str: + text = _clean_text(text) + if not text: + return "" + labels = "|".join(re.escape(name) for name in PROMPT_FIELD_LABELS) + pattern = rf"{re.escape(label)}:\s*(.*?)(?=\. (?:{labels}):|\. Use\b|\. Avoid\b|$)" + match = re.search(pattern, text) + if not match: + return "" + return _clean_text(match.group(1)).rstrip(".") + + +def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str: + value = _clean_text(row.get(key, "")) + if value: + return value + prompt = _clean_text(row.get("prompt", "")) + for label in labels: + value = _prompt_field(prompt, label) + if value: + return value + return "" + + +def _field_from_any_prompt(text: str, labels: tuple[str, ...]) -> str: + for label in labels: + value = _prompt_field(text, label) + if value: + return value + return "" + + +def _normalize_composition(text: str) -> str: + return re.sub(r"^vertical\s+", "", _clean_text(text), flags=re.IGNORECASE) + + +def _clean_clothing(text: str) -> str: + text = _clean_text(text) + text = re.sub(r",?\s*fashion editorial styling$", "", text, flags=re.IGNORECASE) + text = re.sub(r",?\s*resort styling$", "", text, flags=re.IGNORECASE) + return text.strip(" ,") + + +def _single_caption_front(row: dict[str, Any]) -> dict[str, str]: + caption = _clean_text(row.get("caption")) + if not caption: + return {} + caption = _remove_trigger(_strip_style_tail(caption), _clean_text(row.get("trigger")) or DEFAULT_TRIGGER) + caption = _remove_trigger(caption, OLD_TRIGGER) + subject = _clean_text(row.get("primary_subject")) + age = _clean_text(row.get("age_band") or row.get("age")) + body_phrase = _clean_text(row.get("body_phrase")) + if not body_phrase: + body = _clean_text(row.get("body_type") or row.get("body")) + figure = _clean_text(row.get("figure")) + body_phrase = f"{body} figure with {figure}" if body and figure else f"{body} figure".strip() + front = f"{subject}, {age}, {body_phrase}, " + if subject in ("woman", "man") and age and body_phrase and caption.startswith(front): + try: + skin, hair, eyes, _rest = caption[len(front) :].split(", ", 3) + except ValueError: + return {} + else: + pieces = [piece.strip() for piece in caption.split(", ", 6)] + if len(pieces) < 7: + return {} + subject, age, body_phrase, skin, hair, eyes, _rest = pieces + if subject not in ("woman", "man"): + return {} + return { + "caption_subject": subject, + "caption_age": age, + "caption_body_phrase": body_phrase, + "caption_skin": skin, + "caption_hair": hair, + "caption_eyes": eyes, + } + + +def _pose_clause(pose: str) -> str: + pose = _clean_text(pose) + if not pose: + return "" + first = pose.split(None, 1)[0].lower() + if first.endswith("ing") or first in ("seated", "reclined", "posed"): + return pose + return f"posing in {pose}" + + +def _age_subject(age: str, subject: str) -> str: + age = _clean_text(age) + subject = _clean_text(subject) or "person" + if not age: + return f"An adult {subject}" + clean_age = re.sub(r"\s+adults?$", "", age).strip() + if "year-old" in clean_age: + return f"A {clean_age} adult {subject}" + if re.search(r"\d", clean_age): + poss = "her" if subject == "woman" else "his" + return f"An adult {subject} in {poss} {clean_age}" + return f"An adult {clean_age} {subject}" + + +def _clean_age_phrase(age: str) -> str: + age = _clean_text(age) + age = re.sub(r"\s+adults?$", "", age).strip() + return age.replace("-year-old", " years old") + + +def _subject_phrase_from_counts(row: dict[str, Any]) -> str: + subject = _clean_text(row.get("subject_phrase")) + if subject: + return subject + try: + women = int(row.get("women_count") or 0) + men = int(row.get("men_count") or 0) + except (TypeError, ValueError): + return _clean_text(row.get("primary_subject")) or "adult scene" + parts = [] + if women: + parts.append(f"{women} adult {'woman' if women == 1 else 'women'}") + if men: + parts.append(f"{men} adult {'man' if men == 1 else 'men'}") + if not parts: + return _clean_text(row.get("primary_subject")) or "adult scene" + return " and ".join(parts) + + +def _verb_for_row(row: dict[str, Any]) -> str: + try: + return "is" if int(row.get("person_count") or 0) == 1 else "are" + except (TypeError, ValueError): + return "are" + + +def _detail_allows(level: str, dense_only: bool = False) -> bool: + level = (level or "balanced").strip().lower() + if dense_only: + return level == "dense" + return level != "concise" + + +def _single_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None: + subject = _clean_text(row.get("primary_subject") or row.get("subject") or "") + if subject not in ("woman", "man"): + return None + + caption_front = _single_caption_front(row) + age = _clean_text(row.get("age") or row.get("age_band") or caption_front.get("caption_age") or "") + body_phrase = _row_value(row, "body_phrase") or caption_front.get("caption_body_phrase", "") + if not body_phrase: + body = _clean_text(row.get("body_type") or row.get("body") or "") + figure = _clean_text(row.get("figure")) + body_phrase = f"{body} figure with {figure}" if body and figure else f"{body} figure".strip() + + skin = _row_value(row, "skin") or caption_front.get("caption_skin", "") + hair = _row_value(row, "hair") or caption_front.get("caption_hair", "") + eyes = _row_value(row, "eyes") or caption_front.get("caption_eyes", "") + item = _row_value(row, "item", ITEM_LABELS) + if item: + item = _clean_clothing(item) + if not item: + item = _clean_clothing(_row_value(row, "clothing", ("Clothing", "Erotic outfit"))) + scene = _row_value(row, "scene_text", ("Scene", "Setting")) + pose = _row_value(row, "pose", ("Pose",)) + expression = _row_value(row, "expression", ("Facial expression", "Facial expressions")) + composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) + prop = _row_value(row, "prop", ("Prop/detail",)) + style = _row_value(row, "style") if keep_style else "" + + parts = [] + opener = _age_subject(age, subject) + appearance_details = [piece for piece in (skin, hair, eyes) if piece] + if body_phrase: + parts.append(f"{opener} has {_article(body_phrase)} {body_phrase}") + elif appearance_details: + parts.append(f"{opener} has {_human_join(appearance_details)}") + else: + parts.append(opener) + if body_phrase and appearance_details: + parts.append(f"{pronoun(subject)} has {_human_join(appearance_details)}") + if item: + verb = "wears" if subject == "woman" else "is dressed in" + parts.append(f"{pronoun(subject)} {verb} {item}") + if prop: + parts.append(f"{pronoun(subject)} is {prop}") + if pose: + parts.append(f"{pronoun(subject)} is {_pose_clause(pose)}") + if expression: + parts.append(f"{possessive_pronoun(subject)} expression is {expression}") + if scene: + parts.append(f"The setting is {scene}") + if _detail_allows(detail_level) and composition: + parts.append(f"The composition is {composition}") + if keep_style and style: + parts.append(f"The visual style is {style}") + return _join_sentences(parts), "metadata(single)" + + +def pronoun(subject: str) -> str: + return "She" if subject == "woman" else "He" + + +def possessive_pronoun(subject: str) -> str: + return "Her" if subject == "woman" else "His" + + +def _couple_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None: + subject = _clean_text(row.get("subject_phrase") or row.get("primary_subject")) + primary = _clean_text(row.get("primary_subject")) + if "couple" not in primary and subject not in ("two women", "two men", "a woman and a man"): + if not primary.startswith("two ") and " and " not in subject: + return None + if subject == "woman and man": + subject = "a woman and a man" + + ages = _row_value(row, "age", ("Ages",)) or _clean_text(row.get("age_band")) + body = _row_value(row, "body", ("Body types",)) or _clean_text(row.get("body_type")) + pose = _row_value(row, "pose", ("Pose",)) + pose = pose.replace(", affectionate and flirtatious but non-explicit", "") + clothing = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",))) + scene = _row_value(row, "scene_text", ("Scene", "Setting")) + expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) + composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) + style = _row_value(row, "style") if keep_style else "" + + parts = [f"{_cap_first(subject)} are adults"] + if ages: + parts.append(f"The age detail is {_clean_age_phrase(ages)}") + if body: + parts.append(f"Their body types are {body}") + if clothing: + parts.append(f"They wear {clothing}") + if pose: + parts.append(f"The pose is {pose}") + if scene: + parts.append(f"The setting is {scene}") + if expression: + parts.append(f"Their expressions are {expression}") + if _detail_allows(detail_level) and composition: + parts.append(f"The composition is {composition}") + if keep_style and style: + parts.append(f"The visual style is {style}") + return _join_sentences(parts), "metadata(couple)" + + +def _configured_cast_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None: + if _clean_text(row.get("subject_type")) != "configured_cast" and not _clean_text(row.get("women_count")): + if "hardcore sexual poses" not in _clean_text(row.get("main_category")).lower(): + return None + + subject = _subject_phrase_from_counts(row) + verb = _verb_for_row(row) + cast = _row_value(row, "cast_summary", ("Cast",)) + role_graph = _row_value(row, "role_graph", ("Role graph",)) + item = _row_value(row, "item", ITEM_LABELS) + scene = _row_value(row, "scene_text", ("Setting", "Scene")) + expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) + composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) + scene_kind = _row_value(row, "scene_kind") or "explicit adult sex scene" + style = _row_value(row, "style") if keep_style else "" + + parts = [f"{_cap_first(subject)} {verb} shown as a consensual {scene_kind}, with all participants 21+"] + if cast: + parts.append(f"The cast is {cast}") + if role_graph: + parts.append(role_graph) + if item: + parts.append(f"The sexual pose is {item}") + scene_bits = [] + if scene: + scene_bits.append(f"set in {scene}") + if expression: + scene_bits.append(f"with {expression}") + if composition: + scene_bits.append(f"framed as {composition}") + if scene_bits and _detail_allows(detail_level): + parts.append(", ".join(scene_bits)) + if keep_style and style: + parts.append(f"The visual style is {style}") + return _join_sentences(parts), "metadata(configured_cast)" + + +def _group_or_layout_from_row(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str] | None: + primary = _clean_text(row.get("primary_subject")) + if "group" not in primary and primary != "layout scene": + return None + + subject = _row_value(row, "subject_phrase") or primary + age = _row_value(row, "age", ("Ages",)) or _clean_text(row.get("age_band")) + item = _clean_clothing(_row_value(row, "item", ITEM_LABELS) or _row_value(row, "clothing", ("Clothing",))) + scene = _row_value(row, "scene_text", ("Scene", "Setting")) + expression = _row_value(row, "expression", ("Facial expressions", "Facial expression")) + composition = _normalize_composition(_row_value(row, "composition", ("Composition",))) + style = _row_value(row, "style") if keep_style else "" + + if primary == "layout scene": + parts = [f"{_cap_first(subject)} is arranged as an adults-only designed illustration layout"] + if expression: + parts.append(f"The featured expression is {expression}") + else: + parts = [f"{_cap_first(subject)} includes adults"] + if age: + parts[0] += f" ages {age}" + if item: + parts.append(f"They wear {item}") + if expression: + parts.append(f"They show {expression}") + if scene: + parts.append(f"The setting is {scene}") + if _detail_allows(detail_level) and composition: + parts.append(f"The composition is {composition}") + if keep_style and style: + parts.append(f"The visual style is {style}") + return _join_sentences(parts), "metadata(group_layout)" + + +def _metadata_to_prose(row: dict[str, Any], detail_level: str, keep_style: bool) -> tuple[str, str]: + for builder in ( + _configured_cast_from_row, + _single_from_row, + _couple_from_row, + _group_or_layout_from_row, + ): + result = builder(row, detail_level, keep_style) + if result: + return result + return _text_to_prose(_clean_text(row.get("caption") or row.get("prompt")), detail_level, keep_style) + + +def _prompt_to_prose(text: str, detail_level: str, keep_style: bool) -> tuple[str, str] | None: + if ":" not in text: + return None + cast = _field_from_any_prompt(text, ("Cast",)) + item = _field_from_any_prompt(text, ITEM_LABELS) + scene = _field_from_any_prompt(text, ("Setting", "Scene")) + pose = _field_from_any_prompt(text, ("Pose",)) + role_graph = _field_from_any_prompt(text, ("Role graph",)) + expression = _field_from_any_prompt(text, ("Facial expressions", "Facial expression")) + composition = _normalize_composition(_field_from_any_prompt(text, ("Composition",))) + if not any((cast, item, scene, pose, role_graph, expression, composition)): + return None + + subject = _clean_text(text.split(":", 1)[0]) + parts = [] + if subject: + parts.append(f"{_cap_first(subject)}") + if cast: + parts.append(f"The cast is {cast}") + if role_graph: + parts.append(role_graph) + if item: + item_label = "sexual pose" if _field_from_any_prompt(text, ("Sexual pose",)) else "key detail" + parts.append(f"The {item_label} is {item}") + elif pose: + parts.append(f"The pose is {pose}") + scene_bits = [] + if scene: + scene_bits.append(f"set in {scene}") + if expression: + scene_bits.append(f"with {expression}") + if composition: + scene_bits.append(f"framed as {composition}") + if scene_bits and _detail_allows(detail_level): + parts.append(", ".join(scene_bits)) + if keep_style: + style = _clean_text(text.split(":", 1)[1].split(".", 1)[0]) + if style: + parts.append(f"The visual style is {style}") + return _join_sentences(parts), "prompt(labels)" + + +def _parts_to_sentence(parts: list[str], detail_level: str) -> str: + parts = [part for part in (_clean_text(part).strip(" ,.") for part in parts) if part] + if not parts: + return "" + if len(parts) == 1: + return _sentence(parts[0]) + subject = parts[0] + trailing_style = "" + if parts[-1].lower().endswith("illustration"): + trailing_style = parts.pop() + composition = parts[-1] if len(parts) >= 2 else "" + scene = parts[-2] if len(parts) >= 3 else "" + details = parts[1:-2] if len(parts) >= 3 else parts[1:] + sentences = [f"{_cap_first(subject)} includes {', '.join(details)}" if details else _cap_first(subject)] + if _detail_allows(detail_level) and scene: + sentences.append(f"The setting is {scene}") + if _detail_allows(detail_level) and composition: + sentences.append(f"The composition is {composition}") + if trailing_style and _detail_allows(detail_level, dense_only=True): + sentences.append(f"The visual style is {trailing_style}") + return _join_sentences(sentences) + + +def _text_to_prose(text: str, detail_level: str, keep_style: bool) -> tuple[str, str]: + text = _clean_text(text) + prompt_result = _prompt_to_prose(text, detail_level, keep_style) + if prompt_result: + return prompt_result + text = _remove_trigger(_strip_style_tail(text), DEFAULT_TRIGGER) + text = _remove_trigger(text, OLD_TRIGGER) + parts = [part.strip() for part in text.split(",")] + prose = _parts_to_sentence(parts, detail_level) + return prose or _sentence(text), "text(fallback)" + + +def naturalize_caption( + source_text: str, + metadata_json: str = "", + input_hint: str = "auto", + trigger: str = DEFAULT_TRIGGER, + include_trigger: bool = True, + detail_level: str = "balanced", + style_policy: str = "drop_style_tail", +) -> tuple[str, str]: + """Rewrite tag-style prompt/caption text into compact natural language.""" + input_hint = input_hint if input_hint in ("auto", "metadata_json", "caption_or_prompt") else "auto" + detail_level = detail_level if detail_level in ("concise", "balanced", "dense") else "balanced" + keep_style = style_policy == "keep_style_terms" + row, row_method = _row_from_inputs(source_text, metadata_json, input_hint) + if row is not None: + prose, method = _metadata_to_prose(row, detail_level, keep_style) + return _with_trigger(prose, trigger, include_trigger), f"{row_method}:{method}" + prose, method = _text_to_prose(source_text, detail_level, keep_style) + return _with_trigger(prose, trigger, include_trigger), method diff --git a/categories/default_categories.json b/categories/default_categories.json new file mode 100644 index 0000000..be2b816 --- /dev/null +++ b/categories/default_categories.json @@ -0,0 +1,97 @@ +{ + "version": 1, + "categories": [ + { + "name": "Casual clothes", + "slug": "casual_clothes", + "weight": 1.0, + "subject_type": "woman", + "item_label": "Clothing", + "style": "tasteful adult fashion-editorial coloured-pencil comic illustration with casual everyday styling", + "positive_suffix": "Use crisp clean comic linework, soft fabric texture, detailed hatching, warm natural light, and tactile textured paper.", + "subcategories": [ + { + "name": "Streetwear", + "slug": "streetwear", + "weight": 1.0, + "items": [ + "oversized hoodie with slim jeans and clean sneakers", + "cropped bomber jacket with cargo pants and chunky trainers", + "graphic tee under an open flannel with fitted denim", + "sleek track jacket with joggers and minimal sneakers", + "denim-on-denim outfit with a fitted white tee" + ], + "scenes": [ + { + "slug": "city_crosswalk", + "prompt": "sunlit city crosswalk with storefront reflections" + }, + { + "slug": "subway_platform", + "prompt": "clean subway platform with tiled walls and soft overhead lights" + } + ], + "poses": [ + "standing with one hand in a pocket", + "leaning against a wall with relaxed confidence", + "walking forward with a casual runway stride" + ] + }, + { + "name": "Summer casual", + "slug": "summer_casual", + "weight": 1.0, + "items": [ + "light cotton sundress with simple sandals", + "linen shorts with a tucked-in sleeveless blouse", + "soft camp-collar shirt with relaxed trousers", + "ribbed tank top with a flowing midi skirt", + "breathable linen two-piece in pale summer colors" + ], + "scenes": [ + { + "slug": "sunny_market", + "prompt": "open-air weekend market with fruit stands and canvas awnings" + }, + { + "slug": "seaside_walk", + "prompt": "seaside promenade with bright sky and warm paving stones" + } + ], + "poses": [ + "turning slightly with fabric moving in the breeze", + "standing in warm sunlight with relaxed shoulders", + "sitting on a low wall with one knee bent" + ] + }, + { + "name": "Cozy lounge", + "slug": "cozy_lounge", + "weight": 1.0, + "items": [ + "soft knit cardigan with a fitted tee and lounge trousers", + "oversized sweater with leggings and wool socks", + "relaxed sweatshirt with drawstring joggers", + "ribbed lounge set with a long open cardigan", + "simple cotton tee with loose pajama-style trousers" + ], + "scenes": [ + { + "slug": "sunny_apartment", + "prompt": "sunny apartment corner with bookshelves and a warm rug" + }, + { + "slug": "window_seat", + "prompt": "comfortable window seat with soft curtains and afternoon light" + } + ], + "poses": [ + "curled comfortably on a sofa", + "standing barefoot near a window with a relaxed smile", + "sitting cross-legged with a casual magazine nearby" + ] + } + ] + } + ] +} diff --git a/categories/erotic_clothes.json b/categories/erotic_clothes.json new file mode 100644 index 0000000..3f0ff87 --- /dev/null +++ b/categories/erotic_clothes.json @@ -0,0 +1,1365 @@ +{ + "version": 2, + "categories": [ + { + "name": "Provocative erotic clothes", + "slug": "provocative_erotic_clothes", + "weight": 1.0, + "subject_type": "woman", + "item_label": "Erotic outfit", + "style": "explicit adult erotic fashion illustration, sensual pin-up coloured-pencil comic style, adults only", + "positive_suffix": "Use crisp clean comic linework, detailed hatching, soft skin shading, tactile fabric texture, warm intimate lighting, and textured paper.", + "negative_prompt": "minors, childlike appearance, schoolgirl, childlike costume, non-consensual, coercion, violence, injury, watermark", + "expressions": [ + "heavy-lidded seductive gaze", + "direct erotic stare", + "confident teasing smile", + "open-mouthed breathy expression", + "smoldering side glance", + "playful bitten-lip smile", + "intense bedroom eyes", + "relaxed post-orgasmic glow", + "shameless confident grin", + "slow teasing half-smile", + "flushed cheeks with parted lips", + "commanding sensual stare", + "soft lustful gaze", + "mischievous erotic smile", + "dreamy aroused expression", + "bold inviting eye contact" + ], + "compositions": [ + "intimate full-body pin-up pose", + "three-quarter body fashion pose", + "low-angle glamour pose emphasizing legs and hips", + "mirror-view boudoir composition", + "close crop from thighs to face", + "reclining full-body composition", + "standing center-frame poster composition", + "over-the-shoulder rear-view composition", + "kneeling pin-up composition", + "seated legs-forward composition", + "arched-back side-profile composition", + "editorial crop emphasizing lingerie details" + ], + "subcategories": [ + { + "name": "Provocative lingerie", + "slug": "provocative_lingerie", + "weight": 1.0, + "item_templates": [ + "{color} {fabric} {bra_style} with {bottom_style}, {garter_detail}, {stocking_style}, and {shoe_style}", + "{color} {bodywear} with {neckline}, {hip_cut}, {back_detail}, and {trim_detail}", + "{color} {fabric} corset with {cup_detail}, {bottom_style}, {stocking_style}, and {accessory}", + "{color} sheer robe worn open over {bra_style}, {bottom_style}, {garter_detail}, and {shoe_style}", + "{color} {fabric} lingerie set with {exposure_detail}, {trim_detail}, {stocking_style}, and {accessory}", + "{color} {bodywear} with {strap_detail}, {cup_detail}, {hip_cut}, and {shoe_style}", + "{color} bridal-style lingerie with {fabric}, {bra_style}, {garter_detail}, {stocking_style}, and {jewelry}", + "{color} boudoir set combining {bra_style}, {bottom_style}, {back_detail}, {trim_detail}, and {shoe_style}", + "{color} {fabric} teddy with {neckline}, {exposure_detail}, {hip_cut}, and {stocking_style}", + "{color} balconette set with {cup_detail}, {bottom_style}, {garter_detail}, {accessory}, and {shoe_style}" + ], + "item_axes": { + "accessory": [ + "a satin blindfold used as styling", + "a velvet choker", + "long lace gloves", + "a pearl body chain", + "a delicate waist chain", + "a silk ribbon at the throat", + "a jeweled collar", + "a lace eye mask", + "a loose satin sash", + "a gold belly chain", + "a small garter charm", + "a sheer shoulder shawl" + ], + "back_detail": [ + "an open back", + "crisscross back straps", + "a low scooped back", + "a lace-up back", + "a bare back framed by straps", + "a halter back", + "a keyhole back cutout", + "a plunging back" + ], + "bodywear": [ + "lace teddy", + "transparent bodysuit", + "high-cut bodysuit", + "open-back teddy", + "strappy bodysuit", + "mesh bodysuit", + "satin teddy", + "lace bodystocking", + "plunging body", + "garter teddy", + "corset bodysuit", + "cutout bodysuit" + ], + "bottom_style": [ + "a matching thong", + "a tiny lace thong", + "a high-cut g-string", + "a satin thong", + "a sheer thong", + "a side-tie thong", + "a micro thong", + "high-waist lace panties", + "a strappy thong", + "a pearl-string thong" + ], + "bra_style": [ + "balconette bra", + "open-cup bra", + "quarter-cup bra", + "lace demi bra", + "transparent mesh bra", + "strappy cage bra", + "satin push-up bra", + "plunging bra", + "sheer triangle bra", + "cupless harness bra", + "lace bralette", + "underwire shelf bra" + ], + "color": [ + "black", + "deep red", + "burgundy", + "ivory", + "blush pink", + "midnight blue", + "emerald green", + "champagne", + "plum", + "charcoal", + "pearl white", + "ruby", + "soft gold", + "smoky violet" + ], + "cup_detail": [ + "open cups exposing the nipples", + "sheer cups showing the nipples", + "low-cut cups exposing most of the breasts", + "embroidered cups barely covering the nipples", + "transparent cups with visible areolas", + "quarter cups lifting bare breasts", + "shelf cups framing the underside of the breasts", + "lace cups stretched transparently over the nipples" + ], + "exposure_detail": [ + "visible nipples through sheer lace", + "bare underboob", + "deep cleavage", + "sideboob exposed by narrow straps", + "bare hips framed by straps", + "a nearly nude silhouette", + "transparent panels over the breasts", + "a plunging open front", + "high-cut sides exposing the hips", + "bare breasts framed by lace" + ], + "fabric": [ + "lace", + "satin", + "transparent mesh", + "embroidered tulle", + "silk", + "floral lace", + "stretch mesh", + "chiffon", + "wet-look satin", + "scalloped lace", + "transparent organza", + "fishnet lace" + ], + "garter_detail": [ + "a garter belt", + "thin garter straps", + "wide lace garters", + "satin garter straps", + "crisscross thigh garters", + "ribbon garters", + "metal-ring garters", + "high-waist garter belt", + "detachable garter straps", + "minimal elastic garters" + ], + "hip_cut": [ + "high-cut hips", + "deep side cutouts", + "bare hip cutouts", + "thin side straps", + "a thong back", + "an ultra-high leg line", + "open side panels", + "narrow hip straps" + ], + "jewelry": [ + "pearl earrings", + "a thin gold body chain", + "sparkling nipple jewelry", + "a crystal waist chain", + "layered necklaces", + "a delicate ankle chain", + "silver body jewelry", + "a jeweled navel chain" + ], + "neckline": [ + "a plunging neckline", + "a halter neckline", + "a deep V neckline", + "an open front", + "a keyhole neckline", + "a sweetheart neckline", + "a barely held together neckline", + "a low scooped neckline" + ], + "shoe_style": [ + "stiletto heels", + "strappy high heels", + "platform heels", + "patent pumps", + "lace-up heels", + "transparent heels", + "ankle-strap heels", + "open-toe stilettos", + "black high heels", + "red lacquer heels" + ], + "stocking_style": [ + "sheer thigh-high stockings", + "black seamed stockings", + "lace-top stockings", + "fishnet stockings", + "nude sheer stockings", + "white thigh-high stockings", + "glossy stockings", + "patterned lace stockings", + "back-seam stockings", + "transparent hold-up stockings", + "wide-band stockings", + "mesh thigh-high stockings" + ], + "strap_detail": [ + "thin straps crossing the breasts", + "multiple body straps", + "straps framing the nipples", + "a cage strap design", + "delicate shoulder straps", + "straps around the waist", + "crisscross chest straps", + "barely-there side straps" + ], + "trim_detail": [ + "scalloped lace trim", + "satin bows", + "embroidered floral trim", + "metal ring details", + "pearl trim", + "tiny ribbon details", + "transparent lace trim", + "gold chain trim", + "velvet piping", + "delicate beadwork" + ] + }, + "scenes": [ + { + "slug": "boudoir_bedroom", + "prompt": "warm boudoir bedroom with satin sheets, soft curtains, and low lamplight" + }, + { + "slug": "velvet_dressing_room", + "prompt": "private velvet dressing room with a mirror, perfume bottles, and warm bulbs" + }, + { + "slug": "hotel_suite_evening", + "prompt": "luxury hotel suite at evening with city lights beyond the window" + }, + { + "slug": "vanity_corner", + "prompt": "intimate vanity corner with makeup lights, perfume bottles, and silk fabric" + }, + { + "slug": "canopy_bed", + "prompt": "canopy bed with translucent curtains and warm candlelike lighting" + } + ], + "poses": [ + "standing with one hip lifted and fingers resting on a garter strap", + "sitting on the edge of a bed with crossed legs and relaxed shoulders", + "leaning against a vanity mirror with a confident erotic gaze", + "turning partly away to show the open back and stockings", + "reclining on satin sheets with one knee bent", + "kneeling on the bed while adjusting a stocking", + "standing in front of a mirror while tugging lightly at a thong strap", + "arching the back while seated on a velvet stool", + "holding the robe open with relaxed confidence", + "looking over one shoulder while showing garter details" + ] + }, + { + "name": "Sheer exposed", + "slug": "sheer_exposed", + "weight": 1.0, + "item_templates": [ + "{color} {sheer_fabric} {sheer_garment} with {exposed_breast_detail}, {hip_detail}, and {shoe_style}", + "{color} open sheer robe exposing {exposed_breast_detail}, {lower_exposure}, and {jewelry}", + "{color} transparent {bodywear} with {panel_detail}, {nipple_detail}, {hip_detail}, and {stocking_style}", + "{color} wet-look {sheer_garment} clinging to {exposed_breast_detail} and {thigh_detail}", + "{color} see-through {sheer_fabric} wrap with {drape_detail}, {nipple_detail}, and {shoe_style}", + "{color} transparent dress with {neckline}, {exposed_breast_detail}, {hip_detail}, and {accessory}", + "{color} mesh catsuit with {panel_detail}, {nipple_detail}, {lower_exposure}, and {boot_style}", + "{color} diaphanous {sheer_fabric} slip with {drape_detail}, {exposed_breast_detail}, and {stocking_style}", + "{color} open-front sheer garment revealing {nipple_detail}, {belly_detail}, {hip_detail}, and {shoe_style}", + "{color} translucent layered look with {sheer_garment}, {body_chain}, {lower_exposure}, and {accessory}" + ], + "item_axes": { + "accessory": [ + "a silk ribbon choker", + "long transparent gloves", + "a satin blindfold as styling", + "a pearl waist chain", + "a loose chiffon scarf", + "a delicate collar", + "sparkling earrings", + "a sheer shoulder wrap" + ], + "belly_detail": [ + "a bare stomach", + "a visible navel", + "a waist chain across bare skin", + "transparent fabric stretched over the stomach", + "an exposed waist", + "bare ribs under sheer mesh" + ], + "body_chain": [ + "a gold body chain", + "a silver body chain", + "a pearl body chain", + "a crystal body chain", + "a delicate chain harness", + "a thin waist chain" + ], + "bodywear": [ + "bodystocking", + "mesh bodysuit", + "lace catsuit", + "transparent teddy", + "fishnet bodysuit", + "open-panel bodysuit", + "sheer halter bodysuit", + "diaphanous body" + ], + "boot_style": [ + "thigh-high patent boots", + "lace-up boots", + "black heeled boots", + "white vinyl boots", + "stiletto ankle boots", + "over-the-knee boots" + ], + "color": [ + "black", + "smoky gray", + "nude-toned", + "ivory", + "deep red", + "wet black", + "clear white", + "midnight blue", + "soft blush", + "transparent gold" + ], + "drape_detail": [ + "fabric falling open across the breasts", + "fabric gathered below the bare breasts", + "one shoulder slipping down", + "cloth barely crossing the nipples", + "a loose wrap exposing one breast", + "fabric pulled tight across visible nipples", + "a thin layer draped over bare hips", + "fabric falling between the thighs" + ], + "exposed_breast_detail": [ + "bare breasts and visible nipples", + "the full breasts through transparent fabric", + "bare nipples under sheer mesh", + "one exposed breast and one barely covered breast", + "bare underboob and visible nipples", + "breasts outlined by wet transparent fabric", + "nipples visible through lace", + "bare breasts framed by sheer fabric" + ], + "hip_detail": [ + "bare hips", + "high-cut bare hips", + "transparent fabric over the hips", + "thin side straps at the hips", + "open side panels", + "a tiny thong at the hips", + "bare hip bones", + "a sheer thong line" + ], + "jewelry": [ + "a pearl body chain", + "gold body jewelry", + "silver nipple jewelry", + "a crystal waist chain", + "a delicate belly chain", + "thin layered necklaces", + "small sparkling nipple piercings", + "a jeweled collar" + ], + "lower_exposure": [ + "bare hips", + "a tiny thong", + "a visible thong back", + "bare thighs", + "a sheer g-string", + "high-cut bare sides", + "transparent fabric over the pelvis", + "a narrow strip of fabric between the thighs" + ], + "neckline": [ + "a plunging neckline", + "a completely open front", + "a halter neckline", + "a deep scoop neckline", + "a slit front", + "a loose neckline falling off one shoulder" + ], + "nipple_detail": [ + "visible nipples", + "bare nipples", + "nipples clearly visible through mesh", + "nipples framed by transparent lace", + "erect nipples under sheer fabric", + "nipple jewelry visible through mesh" + ], + "panel_detail": [ + "open breast panels", + "transparent breast panels", + "open side panels", + "strategic mesh panels", + "cutouts across the chest", + "thin transparent panels over the body", + "open hip panels", + "lace panels revealing bare skin" + ], + "sheer_fabric": [ + "transparent mesh", + "wet-look mesh", + "diaphanous chiffon", + "thin lace", + "fishnet", + "transparent tulle", + "stretch mesh", + "clear organza", + "gauzy silk", + "see-through lace", + "transparent nylon", + "fine netting" + ], + "sheer_garment": [ + "mesh dress", + "transparent chemise", + "sheer slip dress", + "open robe", + "body-hugging mesh gown", + "transparent mini dress", + "sheer wrap dress", + "thin lace gown", + "see-through halter dress", + "transparent nightgown", + "mesh cover-up", + "open-front sheer dress" + ], + "shoe_style": [ + "black stilettos", + "transparent heels", + "strappy high heels", + "metallic heels", + "ankle-strap stilettos", + "open-toe heels", + "patent pumps", + "platform heels" + ], + "stocking_style": [ + "sheer thigh-high stockings", + "fishnet stockings", + "black seamed stockings", + "nude hold-up stockings", + "lace-top stockings", + "transparent stockings", + "mesh stockings", + "glossy thigh-high stockings" + ], + "thigh_detail": [ + "bare thighs", + "wet fabric over the thighs", + "thigh-high stockings", + "a slit exposing the upper thigh", + "fabric clinging between the thighs", + "open side slits", + "bare legs", + "stockings pulled high on the thighs" + ] + }, + "scenes": [ + { + "slug": "moonlit_window", + "prompt": "moonlit bedroom window with sheer curtains and a soft reflective floor" + }, + { + "slug": "steamy_bathroom", + "prompt": "steamy private bathroom with marble tile, mirror haze, and warm lights" + }, + { + "slug": "soft_studio_backdrop", + "prompt": "minimal studio backdrop with soft amber light and a velvet stool" + }, + { + "slug": "rainy_window", + "prompt": "private room beside a rainy window with reflections and dim amber lamps" + }, + { + "slug": "silk_screen", + "prompt": "room with a silk privacy screen, low bed, and soft rim light" + } + ], + "poses": [ + "standing in profile with one arm lifted and sheer fabric falling open", + "kneeling on satin sheets with an arched back and direct gaze", + "holding the sheer robe open with relaxed confidence", + "sitting with one leg extended and translucent fabric draped across the thighs", + "turning toward the viewer while the transparent fabric catches the light", + "standing against a window with visible nipples through sheer fabric", + "leaning forward with the sheer dress clinging to bare breasts", + "reclining with transparent fabric gathered below the breasts", + "looking over one shoulder while the mesh catsuit reveals the body", + "lifting one knee while the sheer wrap falls open" + ] + }, + { + "name": "Fetish inspired", + "slug": "fetish_inspired", + "weight": 1.0, + "item_templates": [ + "{color} {material} {latex_piece} with {zipper_detail}, {boot_style}, and {glove_style}", + "{color} {harness_style} over {breast_exposure} with {bottom_detail}, {hardware}, and {shoe_style}", + "{color} {corset_style} with {cup_detail}, {garter_detail}, {stocking_style}, and {accessory}", + "{color} wet-look catsuit with {panel_detail}, {zipper_detail}, {breast_exposure}, and {boot_style}", + "{color} leather body harness with {strap_layout}, {bottom_detail}, {hardware}, and {glove_style}", + "{color} vinyl lingerie set with {bra_detail}, {bottom_detail}, {garter_detail}, and {shoe_style}", + "{color} latex bikini with {strap_layout}, {hardware}, {collar_detail}, and {boot_style}", + "{color} open-cup leather outfit with {cup_detail}, {waist_detail}, {stocking_style}, and {accessory}", + "{color} buckled erotic outfit with {corset_style}, {bottom_detail}, {hardware}, and {shoe_style}", + "{color} glossy fetish look combining {latex_piece}, {harness_style}, {breast_exposure}, and {boot_style}" + ], + "item_axes": { + "accessory": [ + "a black collar", + "a silver chain belt", + "leather wrist cuffs as styling accessories", + "a patent choker", + "a metal-ring body chain", + "a glossy waist cincher", + "a lace-up collar", + "a thin leash-like chain used as jewelry" + ], + "bottom_detail": [ + "a tiny leather thong", + "a latex thong", + "a high-cut vinyl bottom", + "a strappy g-string", + "open hip straps", + "a buckled thong", + "a micro bikini bottom", + "a bare-hip harness bottom" + ], + "boot_style": [ + "thigh-high latex boots", + "black patent thigh-high boots", + "lace-up stiletto boots", + "over-the-knee leather boots", + "platform fetish boots", + "glossy ankle boots", + "red patent boots", + "high-heeled riding boots" + ], + "bra_detail": [ + "open-cup bra", + "latex shelf bra", + "strappy cage bra", + "vinyl triangle bra", + "cupless leather bra", + "metal-ring bra", + "transparent vinyl bra", + "plunging latex bra" + ], + "breast_exposure": [ + "bare breasts", + "visible nipples", + "open cups exposing the nipples", + "bare underboob", + "breasts framed by straps", + "nipples visible through mesh", + "a deep exposed cleavage", + "one breast partly exposed by straps" + ], + "collar_detail": [ + "a ring collar", + "a patent choker", + "a buckled collar", + "a jeweled collar", + "a thin leather collar", + "a chain collar" + ], + "color": [ + "black", + "blood red", + "midnight blue", + "deep purple", + "glossy white", + "gunmetal", + "wet black", + "burgundy", + "dark emerald", + "charcoal" + ], + "corset_style": [ + "vinyl corset", + "leather corset", + "latex waist cincher", + "buckled corset", + "underbust corset", + "overbust corset", + "lace-up corset", + "glossy hourglass corset" + ], + "cup_detail": [ + "open cups exposing bare nipples", + "quarter cups lifting bare breasts", + "transparent cups showing nipples", + "cupless framing straps", + "shelf cups under bare breasts", + "metal-ring cups framing the nipples" + ], + "garter_detail": [ + "latex garters", + "leather garter straps", + "metal-ring garters", + "buckled garters", + "wide garter belt", + "thin elastic garters", + "crossed thigh garters", + "vinyl garter belt" + ], + "glove_style": [ + "long latex gloves", + "black leather gloves", + "fingerless gloves", + "vinyl opera gloves", + "glossy elbow gloves", + "lace-up gloves", + "red patent gloves", + "short wrist gloves" + ], + "hardware": [ + "silver rings", + "gold rings", + "black buckles", + "chrome buckles", + "small chain details", + "metal studs", + "front zipper pulls", + "polished O-rings" + ], + "harness_style": [ + "strappy leather harness", + "latex body harness", + "metal-ring chest harness", + "thin cage harness", + "wide leather body harness", + "chain-link harness", + "buckled chest harness", + "minimal strap harness" + ], + "latex_piece": [ + "plunging bodysuit", + "high-cut leotard", + "open-front bodysuit", + "zippered catsuit", + "cutout bodysuit", + "halter bodysuit", + "micro bikini", + "strapless latex top" + ], + "material": [ + "latex", + "vinyl", + "patent leather", + "wet-look PVC", + "glossy rubber", + "black leather", + "transparent vinyl", + "shiny spandex", + "rubberized fabric", + "lacquered leather" + ], + "panel_detail": [ + "open breast panels", + "transparent side panels", + "cutouts across the hips", + "a front cutout", + "mesh panels over the breasts", + "open thigh panels", + "a low unzipped front", + "bare side cutouts" + ], + "shoe_style": [ + "stiletto heels", + "patent pumps", + "platform heels", + "ankle-strap heels", + "black lacquer heels", + "red high heels", + "metallic stilettos", + "transparent heels" + ], + "stocking_style": [ + "fishnet stockings", + "black seamed stockings", + "latex stockings", + "sheer thigh-high stockings", + "wide-band stockings", + "mesh stockings", + "glossy thigh-high stockings", + "lace-top stockings" + ], + "strap_layout": [ + "straps crossing the breasts", + "straps framing the hips", + "a cage strap layout", + "crisscross body straps", + "thin straps over bare skin", + "wide straps across the waist", + "minimal straps over the nipples", + "straps wrapping the thighs" + ], + "waist_detail": [ + "a cinched waist", + "a buckled waist belt", + "a narrow latex waist belt", + "a chain belt", + "a lace-up waist", + "a polished waist cincher" + ], + "zipper_detail": [ + "a zipper opened low", + "a zipper opened to the navel", + "a deep unzipped front", + "a zipper pulled down below the breasts", + "a front zipper exposing cleavage", + "a side zipper detail" + ] + }, + "scenes": [ + { + "slug": "dark_studio", + "prompt": "dark private studio with glossy floor reflections and controlled rim light" + }, + { + "slug": "red_velvet_room", + "prompt": "red velvet room with black lacquer furniture and warm spotlighting" + }, + { + "slug": "industrial_loft", + "prompt": "private industrial loft with brick walls, metal beams, and dramatic light" + }, + { + "slug": "black_lacquer_room", + "prompt": "black lacquer room with glossy furniture, velvet curtains, and sharp rim light" + }, + { + "slug": "neon_private_studio", + "prompt": "private neon-lit studio with magenta highlights and reflective floor" + } + ], + "poses": [ + "standing tall with hands on hips and confident dominant posture", + "leaning forward slightly with glossy latex catching the light", + "sitting on a low chair with legs crossed and shoulders back", + "turning to show the harness straps across the body", + "walking forward with a slow runway stride and intense gaze", + "kneeling with the back arched and hands resting on thighs", + "standing with one boot on a low stool", + "pulling a zipper down with direct eye contact", + "looking over one shoulder to show buckles and straps", + "posing in a mirror with leather straps visible from behind" + ] + }, + { + "name": "Nude accessories", + "slug": "nude_accessories", + "weight": 1.0, + "item_templates": [ + "fully nude body styled only with {stocking_style}, {shoe_style}, and {jewelry}", + "bare breasts and bare hips framed by {accessory_set}, {stocking_style}, and {shoe_style}", + "nude body with {body_jewelry}, {glove_style}, {shoe_style}, and {cover_detail}", + "bare body partly covered by {cover_detail}, with {stocking_style}, {jewelry}, and {shoe_style}", + "fully nude pin-up styling with {glove_style}, {body_jewelry}, {stocking_style}, and {hair_accessory}", + "bare breasts, bare hips, and {accessory_set} with {shoe_style} and {jewelry}", + "nude body wrapped only in {drape_detail}, with {body_jewelry}, {stocking_style}, and {shoe_style}", + "nothing but {accessory_set}, {glove_style}, {shoe_style}, and {jewelry} on a nude body", + "bare body with {cover_detail} slipping away, {stocking_style}, {body_jewelry}, and {shoe_style}", + "nude glamour look with {hair_accessory}, {accessory_set}, {jewelry}, and {shoe_style}" + ], + "item_axes": { + "accessory_set": [ + "a garter belt", + "a pearl body chain", + "a leather harness", + "a satin waist sash", + "a delicate chain harness", + "a corset belt", + "a crystal belly chain", + "a black collar", + "thin thigh garters", + "a lace waist cincher", + "a jeweled body harness", + "a silk ribbon harness" + ], + "body_jewelry": [ + "gold body chains", + "silver body chains", + "pearl body jewelry", + "crystal nipple jewelry", + "a delicate waist chain", + "a chain harness", + "sparkling belly jewelry", + "layered body chains", + "a jeweled navel chain", + "thin breast chains", + "a pearl waist chain", + "a gold chain bra" + ], + "cover_detail": [ + "a satin sheet held at the hips", + "an open robe falling behind the shoulders", + "a translucent shawl barely covering the breasts", + "a silk scarf crossing one nipple", + "a loose towel low around the hips", + "a chiffon wrap falling open", + "a fur stole under the elbows", + "a satin sheet slipping below the breasts", + "a transparent veil over bare skin", + "a loose shirt falling off both shoulders" + ], + "drape_detail": [ + "a translucent chiffon veil", + "a satin sheet", + "a sheer silk scarf", + "a loose robe", + "a transparent shawl", + "a soft fur stole", + "a thin gauze wrap", + "a silk bedsheet" + ], + "glove_style": [ + "long opera gloves", + "black lace gloves", + "transparent mesh gloves", + "satin gloves", + "fingerless lace gloves", + "red opera gloves", + "ivory silk gloves", + "latex gloves" + ], + "hair_accessory": [ + "a loose silk ribbon in the hair", + "a jeweled hair clip", + "a black bow", + "a pearl hairpin", + "a delicate head chain", + "a flower tucked behind one ear", + "a satin headband", + "a crystal hair comb" + ], + "jewelry": [ + "pearl earrings", + "large gold hoops", + "a crystal necklace", + "a velvet choker", + "layered gold necklaces", + "sparkling bracelets", + "ankle chains", + "a jeweled collar", + "silver earrings", + "a thin black choker", + "a gold belly chain", + "a pearl necklace" + ], + "shoe_style": [ + "stiletto heels", + "strappy high heels", + "transparent heels", + "platform heels", + "black pumps", + "red lacquer heels", + "ankle-strap heels", + "open-toe heels", + "metallic heels", + "white high heels" + ], + "stocking_style": [ + "black thigh-high stockings", + "sheer thigh-high stockings", + "fishnet stockings", + "white lace stockings", + "nude hold-up stockings", + "back-seam stockings", + "glossy stockings", + "mesh stockings", + "lace-top stockings", + "transparent stockings", + "wide-band stockings", + "patterned stockings" + ] + }, + "scenes": [ + { + "slug": "satin_bed", + "prompt": "private satin bed scene with rumpled sheets and warm bedside light" + }, + { + "slug": "antique_mirror", + "prompt": "antique mirror corner with dark wood, perfume bottles, and golden lamplight" + }, + { + "slug": "warm_private_studio", + "prompt": "warm private art studio with fabric drapes and a soft spotlight" + }, + { + "slug": "velvet_stool", + "prompt": "private studio with a velvet stool, black curtains, and golden rim light" + }, + { + "slug": "bedside_lamplight", + "prompt": "intimate bedroom with bedside lamplight, silk sheets, and soft shadows" + } + ], + "poses": [ + "reclining on a satin sheet with one arm above the head", + "standing with the satin sheet held loosely at the hips", + "sitting on a velvet stool with crossed ankles and a confident gaze", + "turning partly away while looking back over one shoulder", + "lying on one side with stockings and heels emphasized", + "kneeling with bare breasts forward and hands on thighs", + "standing in front of a mirror while jewelry frames the nude body", + "arching the back while the sheet slips below the breasts", + "sitting with legs extended and heels pointed toward the viewer", + "holding a sheer scarf across one nipple while keeping direct eye contact" + ] + }, + { + "name": "Microwear and body tape", + "slug": "microwear_body_tape", + "weight": 1.0, + "item_templates": [ + "{color} micro bikini with {top_detail}, {bottom_detail}, {body_tape}, and {shoe_style}", + "{color} body tape design covering only {tape_coverage}, with {bottom_detail}, {jewelry}, and {shoe_style}", + "{color} tiny swimwear look with {top_detail}, {hip_detail}, {body_chain}, and {stocking_style}", + "{color} barely-there festival outfit with {body_tape}, {nipple_detail}, {bottom_detail}, and {boot_style}", + "{color} metallic tape outfit with {tape_pattern}, {hip_detail}, {jewelry}, and {shoe_style}", + "{color} erotic beach look with {top_detail}, {bottom_detail}, {body_chain}, and {shoe_style}", + "{color} extreme cutout bodysuit with {cutout_detail}, {nipple_detail}, {hip_detail}, and {boot_style}", + "{color} adhesive lingerie look with {tape_pattern}, {tape_coverage}, {bottom_detail}, and {jewelry}", + "{color} tiny clubwear set with {top_detail}, {hip_detail}, {body_chain}, and {shoe_style}", + "{color} exposed cutout look with {cutout_detail}, {nipple_detail}, {body_tape}, and {stocking_style}" + ], + "item_axes": { + "body_chain": [ + "a gold body chain", + "a silver body chain", + "a crystal waist chain", + "a pearl body chain", + "a delicate chain harness", + "a belly chain", + "thin breast chains", + "a jeweled body chain" + ], + "body_tape": [ + "crossed black body tape", + "metallic gold body tape", + "silver body tape", + "thin adhesive strips", + "X-shaped nipple tape", + "glitter body tape", + "wet-look black tape", + "geometric body tape" + ], + "boot_style": [ + "thigh-high boots", + "white platform boots", + "black patent boots", + "metallic ankle boots", + "lace-up boots", + "stiletto boots" + ], + "bottom_detail": [ + "a micro thong", + "a barely-there g-string", + "a high-cut micro bottom", + "a transparent thong", + "a side-tie string bottom", + "a metallic thong", + "an open-side bottom", + "a tiny triangle bottom" + ], + "color": [ + "black", + "gold", + "silver", + "white", + "neon pink", + "blood red", + "electric blue", + "chrome", + "nude-toned", + "purple" + ], + "cutout_detail": [ + "large breast cutouts", + "open nipple cutouts", + "deep side cutouts", + "bare stomach cutouts", + "open hip cutouts", + "a plunging center cutout", + "strap-only side panels", + "bare underboob cutouts" + ], + "hip_detail": [ + "bare hips", + "high-cut hips", + "thin hip strings", + "open side straps", + "hip chains", + "transparent side panels", + "a visible thong back", + "bare hip bones" + ], + "jewelry": [ + "nipple jewelry", + "a gold belly chain", + "sparkling earrings", + "a jeweled choker", + "crystal body jewelry", + "silver bracelets", + "a thin waist chain", + "ankle chains" + ], + "nipple_detail": [ + "visible nipples", + "nipple tape only", + "bare nipples framed by straps", + "tiny pasties over the nipples", + "metallic tape over the nipples", + "nipples visible through transparent panels" + ], + "shoe_style": [ + "stiletto heels", + "transparent heels", + "platform sandals", + "metallic heels", + "black high heels", + "white strappy heels", + "red lacquer heels", + "ankle-strap heels" + ], + "stocking_style": [ + "fishnet stockings", + "sheer thigh-high stockings", + "glitter stockings", + "nude stockings", + "black seamed stockings", + "mesh thigh-high stockings" + ], + "tape_coverage": [ + "the nipples", + "the nipples and lower belly", + "the breasts in a cross pattern", + "the bare breasts in thin strips", + "the nipples and hips", + "the breasts with minimal strips", + "the nipples and waist", + "the breast curves" + ], + "tape_pattern": [ + "crossed tape strips", + "geometric tape lines", + "star-shaped nipple tape", + "thin vertical tape strips", + "diagonal tape over the breasts", + "metallic tape curves", + "symmetrical tape bands", + "minimal X-shaped tape" + ], + "top_detail": [ + "tiny triangle cups", + "micro straps over the nipples", + "a barely-there top", + "a transparent micro top", + "nipple-covering triangles", + "ultra-thin chest straps", + "an underboob micro top", + "a metallic micro top" + ] + }, + "scenes": [ + { + "slug": "neon_club_private", + "prompt": "private neon club room with glossy floor, magenta light, and dark curtains" + }, + { + "slug": "night_pool", + "prompt": "private night pool with blue reflections and warm lantern light" + }, + { + "slug": "festival_backstage", + "prompt": "adult festival backstage dressing area with neon mirrors and costume racks" + }, + { + "slug": "beach_after_dark", + "prompt": "private beach after dark with moonlight, wet sand, and soft lanterns" + }, + { + "slug": "chrome_studio", + "prompt": "chrome studio set with reflective panels and controlled erotic lighting" + } + ], + "poses": [ + "standing with arms raised so the tiny top and body tape are visible", + "walking forward with hip strings and body chain catching the light", + "leaning against a neon wall with one knee bent", + "turning partly away to show the micro thong and bare hips", + "kneeling with the body tape emphasized by glossy light", + "posing beside a pool with wet skin and direct eye contact", + "arching the back while holding one hip strap", + "sitting on a chrome stool with legs apart in a confident fashion pose", + "pulling lightly at a micro bikini string", + "standing in profile with visible nipple tape and bare hips" + ] + }, + { + "name": "Erotic costumes", + "slug": "erotic_costumes", + "weight": 1.0, + "item_templates": [ + "{color} adult {costume_role} look with {top_detail}, {bottom_detail}, {stocking_style}, and {shoe_style}", + "{color} fantasy {costume_role} costume with {corset_detail}, {exposure_detail}, {accessory}, and {shoe_style}", + "{color} erotic {costume_role} outfit with {bra_detail}, {bottom_detail}, {prop_detail}, and {stocking_style}", + "{color} adult cabaret-inspired look with {corset_detail}, {leg_detail}, {accessory}, and {shoe_style}", + "{color} adult maid-inspired lingerie with {top_detail}, {bottom_detail}, {stocking_style}, and {accessory}", + "{color} adult secretary-inspired erotic outfit with {shirt_detail}, {bottom_detail}, {exposure_detail}, and {shoe_style}", + "{color} vampire-inspired lingerie costume with {corset_detail}, {cape_detail}, {breast_detail}, and {shoe_style}", + "{color} showgirl-inspired erotic costume with {bra_detail}, {bottom_detail}, {leg_detail}, and {accessory}", + "{color} adult nurse-inspired lingerie costume with {top_detail}, {bottom_detail}, {prop_detail}, and {stocking_style}", + "{color} pin-up fantasy costume with {corset_detail}, {breast_detail}, {bottom_detail}, and {shoe_style}" + ], + "item_axes": { + "accessory": [ + "long gloves", + "a lace choker", + "a tiny hat", + "a pearl necklace", + "a satin bow", + "a feather boa", + "a jeweled collar", + "a waist chain" + ], + "bottom_detail": [ + "a tiny thong", + "a micro skirt", + "a high-cut g-string", + "a sheer thong", + "open-side panties", + "a lace garter bottom", + "a vinyl thong", + "a barely-there bikini bottom" + ], + "bra_detail": [ + "an open-cup bra", + "a transparent lace bra", + "a push-up bra", + "a cupless bra", + "a tiny triangle bra", + "a shelf bra", + "a satin balconette bra", + "a strappy cage bra" + ], + "breast_detail": [ + "visible nipples", + "bare underboob", + "bare breasts framed by costume trim", + "deep cleavage", + "transparent fabric over the breasts", + "open cups exposing nipples", + "nipple covers shaped like costume accents", + "sideboob exposed by the cut" + ], + "cape_detail": [ + "a short sheer cape", + "a velvet capelet", + "a transparent black cape", + "a satin-lined cape", + "a lace shoulder cape", + "a tiny red capelet" + ], + "color": [ + "black", + "red", + "white", + "pink", + "gold", + "silver", + "midnight blue", + "purple", + "emerald", + "burgundy" + ], + "corset_detail": [ + "a tight corset", + "an underbust corset", + "a lace-up corset", + "a vinyl corset", + "a satin corset", + "a transparent corset", + "a buckled corset", + "a high-cut corset bodysuit" + ], + "costume_role": [ + "bunny", + "cabaret dancer", + "maid", + "secretary", + "nurse", + "vampire", + "showgirl", + "spy", + "lounge singer", + "burlesque dancer" + ], + "exposure_detail": [ + "visible nipples", + "bare breasts", + "bare hips", + "deep cleavage", + "bare underboob", + "transparent breast panels", + "an open front", + "high-cut sides" + ], + "leg_detail": [ + "fishnet stockings", + "thigh-high stockings", + "back-seam stockings", + "bare thighs", + "garter straps", + "lace stockings", + "glossy stockings", + "mesh stockings" + ], + "prop_detail": [ + "a feather duster as styling", + "a clipboard held loosely", + "a cocktail glass", + "a silk fan", + "a riding crop used as a costume prop", + "a tiny tray", + "a lipstick tube", + "a masquerade mask" + ], + "shirt_detail": [ + "an open blouse exposing lingerie", + "a cropped blouse tied under the breasts", + "a sheer blouse with visible nipples", + "an unbuttoned shirt", + "a tight blouse pulled open", + "a transparent secretary blouse" + ], + "shoe_style": [ + "stiletto heels", + "platform heels", + "patent pumps", + "ankle-strap heels", + "thigh-high boots", + "lace-up heels", + "red high heels", + "black pumps" + ], + "stocking_style": [ + "fishnet stockings", + "lace-top stockings", + "black seamed stockings", + "white thigh-high stockings", + "nude stockings", + "mesh stockings", + "back-seam stockings", + "glossy thigh-high stockings" + ], + "top_detail": [ + "a transparent bra", + "an open-cup top", + "a tiny cropped top", + "a sheer bustier", + "a plunging bodice", + "a cupless top", + "a lace bralette", + "a satin bra top" + ] + }, + "scenes": [ + { + "slug": "costume_dressing_room", + "prompt": "private costume dressing room with racks, warm mirror bulbs, and velvet curtains" + }, + { + "slug": "burlesque_stage", + "prompt": "small private burlesque stage with red curtains and warm spotlights" + }, + { + "slug": "cabaret_backstage", + "prompt": "cabaret backstage corner with feather props, vanity mirrors, and dark wood" + }, + { + "slug": "office_after_dark", + "prompt": "stylized private office after dark with blinds, desk lamp, and city glow" + }, + { + "slug": "fantasy_parlor", + "prompt": "dark fantasy parlor with velvet chair, candlelight, and antique wallpaper" + } + ], + "poses": [ + "standing in a theatrical pin-up pose with one hand on the hip", + "sitting on a desk edge with legs crossed and direct eye contact", + "leaning against a vanity while adjusting a stocking", + "holding a costume prop while the outfit exposes the body", + "turning to show the thong back and costume details", + "kneeling on a stage chair with an arched back", + "standing under a warm spotlight with the costume pulled open", + "looking over one shoulder with a teasing smile", + "sitting with one heel on the chair and shoulders back", + "walking forward like a private runway costume reveal" + ] + } + ] + } + ] +} diff --git a/categories/sexual_poses.json b/categories/sexual_poses.json new file mode 100644 index 0000000..c94673a --- /dev/null +++ b/categories/sexual_poses.json @@ -0,0 +1,1086 @@ +{ + "version": 1, + "categories": [ + { + "name": "Hardcore sexual poses", + "slug": "hardcore_sexual_poses", + "weight": 1.0, + "subject_type": "configured_cast", + "item_label": "Sexual pose", + "style": "explicit consensual adult hardcore sex illustration, anatomically clear erotic comic pin-up style, adults only", + "positive_suffix": "Use clear adult anatomy, visible sexual contact, intense body language, crisp comic linework, detailed hatching, warm erotic lighting, and tactile textured paper.", + "negative_prompt": "minors, childlike appearance, teen, schoolgirl, incest, bestiality, non-consensual, coercion, rape, violence, injury, blood, gore, watermark", + "prompt_template": "{subject_phrase}, all 21+ consenting adults: {style}. Cast: {cast_summary}. Role graph: {role_graph} Sexual pose: {item}. Setting: {scene}. Composition: {composition}. Facial expressions: {expression}. Make the scene explicit, hardcore, and anatomically clear, with visible genital contact and adult bodies only. {positive_suffix} Avoid: {negative_prompt}.", + "caption_template": "{trigger}, {scene_kind}, {cast_summary}, {role_graph}, {item}, {scene}, {composition}, explicit consensual adult hardcore sex illustration", + "expressions": [ + "open-mouthed orgasmic expression", + "heavy-lidded lustful gaze", + "flushed faces and parted lips", + "intense eye contact during sex", + "breathless pleasure expression", + "commanding erotic stare", + "shameless aroused grin", + "eyes closed in climax", + "teasing dominant smile", + "desperate hungry gaze", + "sweaty post-orgasmic glow", + "bitten-lip pleasure expression", + "wild aroused expression", + "soft moaning expression", + "focused intimate eye contact", + "overwhelmed climax expression" + ], + "compositions": [ + "full-body bed scene with all bodies visible", + "low-angle explicit penetration view", + "overhead view of intertwined bodies", + "side-profile view showing genital contact", + "mirror-view sex composition", + "close crop on hips, thighs, and faces", + "wide orgy composition with all participants visible", + "centered threesome composition", + "kneeling and standing composition", + "reclining full-body composition", + "floor-level explicit sex composition", + "front-facing explicit pin-up composition" + ], + "scenes": [ + { + "slug": "rumpled_bed", + "prompt": "private bedroom with rumpled sheets, warm lamps, pillows, and erotic after-dark lighting" + }, + { + "slug": "hotel_sex_suite", + "prompt": "luxury hotel suite with city lights, messy satin bedding, and low amber light" + }, + { + "slug": "mirror_bedroom", + "prompt": "bedroom with a large mirror reflecting the explicit adult sex scene" + }, + { + "slug": "private_studio_mattress", + "prompt": "private photo studio with a low mattress, fabric drapes, and controlled warm spotlighting" + }, + { + "slug": "velvet_sex_room", + "prompt": "private velvet room with dark curtains, soft cushions, and warm red light" + }, + { + "slug": "shower_sex_room", + "prompt": "large private shower room with steam, wet tile, and warm reflected light" + }, + { + "slug": "couch_after_dark", + "prompt": "dim private lounge with a wide couch, scattered clothing, and soft golden shadows" + }, + { + "slug": "floor_cushions", + "prompt": "intimate room with floor cushions, silk sheets, and candlelike side lighting" + } + ], + "subcategories": [ + { + "name": "Penetrative sex", + "slug": "penetrative_sex", + "min_people": 2, + "weight": 1.0, + "item_templates": [ + "{penetration_act} in {position}, with {body_contact}, {intensity}, and {visibility}", + "{position} while {penetration_act}, {hand_detail}, {mouth_detail}, and {visibility}", + "{penetration_act} from {angle}, with {leg_detail}, {body_contact}, and {intensity}", + "hardcore {position} featuring {penetration_act}, {thrust_detail}, {hand_detail}, and {visibility}", + "{penetration_act} on {surface}, with {body_contact}, {mouth_detail}, and {climax_hint}", + "{angle} view of {penetration_act}, {leg_detail}, {thrust_detail}, and {visibility}", + "{position} with explicit {penetration_act}, {body_contact}, {intensity}, and {climax_hint}", + "deep {penetration_act} with {hand_detail}, {mouth_detail}, {leg_detail}, and {visibility}" + ], + "item_axes": { + "angle": [ + "front-facing", + "side-profile", + "rear-view", + "overhead", + "mirror-reflected", + "low-angle", + "close-up", + "wide full-body" + ], + "body_contact": [ + "chests pressed together", + "hips locked tightly together", + "bodies tangled on the sheets", + "hands gripping hips", + "thighs wrapped around the penetrating partner", + "sweaty bodies pressed close", + "one body pinned under another", + "bodies arched into each other" + ], + "climax_hint": [ + "visible arousal and flushed skin", + "cum dripping on thighs", + "post-climax wetness on the body", + "a messy creampie implied by fluid on the thighs", + "sweat and sexual fluids on skin", + "cum visible on the belly", + "wet sheets beneath the bodies", + "a near-orgasm expression" + ], + "hand_detail": [ + "hands gripping the ass", + "fingers tangled in hair", + "hands spreading the thighs", + "one hand pressing into the mattress", + "hands pulling bodies closer", + "fingers digging into hips", + "hands holding wrists above the head", + "one hand cupping a breast" + ], + "intensity": [ + "rough fast rhythm", + "slow deep thrusting", + "hard passionate rhythm", + "desperate grinding motion", + "deep intimate pressure", + "sweaty urgent movement", + "forceful consensual thrusting", + "climax-building intensity" + ], + "leg_detail": [ + "legs spread wide", + "one leg lifted high", + "thighs wrapped around the waist", + "knees pressed to the chest", + "legs crossed behind the back", + "one foot planted on the bed", + "thighs open toward the viewer", + "legs draped over shoulders" + ], + "mouth_detail": [ + "open-mouthed moaning", + "deep kissing", + "bitten lips", + "mouth close to the ear", + "tongues visible while kissing", + "breathless parted lips", + "neck kissing", + "a gasp of pleasure" + ], + "penetration_act": [ + { + "text": "strap-on vaginal penetration with visible genital contact", + "cast": "women_only" + }, + { + "text": "toy-assisted pussy penetration between women", + "cast": "women_only" + }, + { + "text": "strap-on and finger penetration of pussy", + "cast": "women_only" + }, + { + "text": "cock entering ass between men", + "cast": "men_only" + }, + { + "text": "deep anal sex between men", + "cast": "men_only" + }, + { + "text": "male/male penetrative anal sex", + "cast": "men_only" + }, + "vaginal penetration with visible genital contact", + "deep vaginal sex", + "explicit penetrative sex", + "cock entering pussy", + "pussy stretched around a cock", + "hardcore vaginal thrusting", + "full-body penetrative sex", + "close-contact vaginal sex" + ], + "position": [ + "missionary position", + "cowgirl position", + "reverse cowgirl position", + "doggy style position", + "standing sex position", + "spooning sex position", + "edge-of-bed position", + "kneeling straddle position", + "lotus sex position", + "bent-over position" + ], + "surface": [ + "rumpled bed sheets", + "a wide couch", + "floor cushions", + "a low mattress", + "a velvet chaise", + "a shower bench", + "a hotel bed", + "a soft rug" + ], + "thrust_detail": [ + "hips driving forward", + "deep rhythmic thrusts", + "pelvis pressed tight", + "hard grinding motion", + "bodies rocking together", + "thigh muscles tense", + "ass lifted into each thrust", + "wet skin sliding together" + ], + "visibility": [ + "genitals clearly visible", + "penetration clearly visible", + "pussy and cock visible", + "explicit genital contact visible", + "wetness visible between the thighs", + "hips framed in the foreground", + "open thighs exposing penetration", + "anatomically clear penetration" + ] + } + }, + { + "name": "Oral sex", + "slug": "oral_sex", + "min_people": 2, + "weight": 1.0, + "item_templates": [ + "{oral_act} in {position}, with {hand_detail}, {expression_detail}, and {visibility}", + "{position} featuring {oral_act}, {body_contact}, {saliva_detail}, and {climax_hint}", + "{oral_act} from {angle}, with {mouth_detail}, {hand_detail}, and {visibility}", + "hardcore oral scene with {oral_act}, {body_contact}, {saliva_detail}, and {expression_detail}", + "{oral_act} on {surface}, {hand_detail}, {mouth_detail}, and {climax_hint}", + "{angle} view of {oral_act}, with {visibility}, {body_contact}, and {expression_detail}", + "{position} while {oral_act}, with {saliva_detail}, {hand_detail}, and {climax_hint}", + "explicit mouth-to-genitals pose: {oral_act}, {mouth_detail}, {body_contact}, and {visibility}" + ], + "item_axes": { + "angle": [ + "front-facing", + "side-profile", + "overhead", + "low-angle", + "mirror-reflected", + "close-up", + "wide full-body", + "kneeling eye-level" + ], + "body_contact": [ + "hands on thighs", + "hips pushed toward the mouth", + "legs held open", + "fingers tangled in hair", + "one body kneeling between spread legs", + "chest pressed to thighs", + "ass lifted toward the mouth", + "bodies stacked close together" + ], + "climax_hint": [ + "cum on lips", + "cum on the tongue", + "cum dripping from the mouth", + "cum on breasts", + "cum on the belly", + "visible orgasmic tension", + "messy wet mouth", + "post-climax fluids on skin" + ], + "expression_detail": [ + "eyes looking up", + "eyes closed in pleasure", + "open-mouthed moaning", + "flushed cheeks", + "intense eye contact", + "breathless parted lips", + "hungry focused gaze", + "orgasmic expression" + ], + "hand_detail": [ + "hands spreading thighs", + "hands holding hips", + "one hand stroking a cock", + "fingers gripping sheets", + "hands holding the head gently in place", + "hands cupping breasts", + "fingers pressing into thighs", + "one hand holding the ass" + ], + "mouth_detail": [ + "tongue visible", + "lips wrapped tightly", + "mouth open wide", + "wet lips", + "deep mouth contact", + "tongue pressed to genitals", + "saliva shining on lips", + "mouth stretched around a cock" + ], + "oral_act": [ + "fellatio with cock in mouth", + "deepthroat blowjob", + "cunnilingus with tongue on pussy", + "face-sitting cunnilingus", + "sixty-nine oral sex", + "blowjob while another partner watches", + "pussy licking with thighs spread", + "cock sucking with visible saliva", + "oral sex with tongue and fingers", + "mouth on genitals with explicit contact" + ], + "position": [ + "kneeling oral position", + "face-sitting position", + "sixty-nine position", + "edge-of-bed oral position", + "standing blowjob position", + "reclining cunnilingus position", + "straddled oral position", + "side-lying oral position", + "spread-leg oral position", + "chair oral position" + ], + "saliva_detail": [ + "visible saliva", + "wet lips and tongue", + "saliva strings", + "slick wet mouth", + "drool on the chin", + "wet shine on genitals", + "saliva dripping onto skin", + "messy oral wetness" + ], + "surface": [ + "rumpled bed sheets", + "a wide couch", + "a velvet chair", + "floor cushions", + "a hotel bed", + "a shower bench", + "a soft rug", + "a low mattress" + ], + "visibility": [ + "mouth and genitals clearly visible", + "tongue contact clearly visible", + "cock and lips visible", + "pussy and tongue visible", + "saliva and arousal visible", + "face pressed to genitals", + "explicit oral contact visible", + "close-up oral detail" + ] + } + }, + { + "name": "Anal and double penetration", + "slug": "anal_double_penetration", + "min_people": 2, + "weight": 1.0, + "item_templates": [ + "{anal_act} in {position}, with {leg_detail}, {hand_detail}, and {visibility}", + "{double_act} with {body_arrangement}, {intensity}, {mouth_detail}, and {visibility}", + "{anal_act} from {angle}, with {body_contact}, {thrust_detail}, and {climax_hint}", + "hardcore {position} featuring {anal_act}, {body_contact}, {hand_detail}, and {visibility}", + "{double_act} on {surface}, with {leg_detail}, {intensity}, and {climax_hint}", + "{angle} view of {double_act}, {body_arrangement}, {mouth_detail}, and {visibility}", + "{anal_act} with {thrust_detail}, {hand_detail}, {body_contact}, and {climax_hint}", + "explicit double-contact sex pose: {double_act}, {leg_detail}, {visibility}, and {intensity}" + ], + "item_axes": { + "anal_act": [ + { + "text": "strap-on anal penetration with visible contact", + "cast": "women_only" + }, + { + "text": "toy-assisted anal penetration between women", + "cast": "women_only" + }, + { + "text": "deep strap-on anal sex", + "cast": "women_only" + }, + "anal penetration with visible genital contact", + "deep anal sex", + "cock entering ass", + "ass stretched around a cock", + "hardcore anal thrusting", + "bent-over anal sex", + "rear-entry anal penetration", + "anal sex with spread cheeks" + ], + "angle": [ + "rear-view", + "side-profile", + "low-angle", + "mirror-reflected", + "overhead", + "close-up", + "wide full-body", + "front-facing with hips turned" + ], + "body_arrangement": [ + { + "text": "one body between two partners", + "min_people": 3 + }, + { + "text": "one partner behind and one partner in front", + "min_people": 3 + }, + { + "text": "two partners penetrating at once", + "min_people": 3 + }, + "stacked bodies on the bed", + "kneeling center partner", + "one partner held between two bodies", + "front-and-back contact", + "three bodies locked together" + ], + "body_contact": [ + "hands spreading the ass", + "hips pressed tight", + "bodies locked at the waist", + "one body bent over", + "chests pressed to the back", + "thighs held open", + "ass lifted high", + "sweaty bodies touching" + ], + "climax_hint": [ + "cum dripping down thighs", + "cum on the ass", + "cum on the lower back", + "post-climax wetness", + "messy sexual fluids", + "visible orgasmic tension", + "cum on the belly", + "wet sheets beneath the bodies" + ], + "double_act": [ + { + "text": "double strap-on penetration with pussy and ass filled", + "cast": "women_only", + "min_people": 3 + }, + { + "text": "toy and strap-on double penetration", + "cast": "women_only", + "min_people": 2 + }, + { + "text": "cock and toy double penetration", + "cast": "mixed", + "min_people": 2 + }, + { + "text": "toy-assisted vaginal and anal penetration at the same time", + "cast": "mixed", + "min_people": 2 + }, + { + "text": "double anal penetration between men", + "cast": "men_only", + "min_people": 3 + }, + "double penetration with pussy and ass filled", + "vaginal and anal penetration at the same time", + "front-and-back double penetration", + "one cock in pussy and one cock in ass", + "hardcore double penetration", + "kneeling double penetration", + "standing supported double penetration", + "deep double penetration with all genitals visible" + ], + "hand_detail": [ + "hands gripping hips", + "hands holding the waist", + "one hand spreading cheeks", + "hands braced on the bed", + "fingers digging into thighs", + "hands holding shoulders", + "one hand cupping a breast", + "hands pulling the body back" + ], + "intensity": [ + "rough consensual rhythm", + "deep synchronized thrusting", + "slow intense pressure", + "hard fast thrusting", + "climax-building pace", + "sweaty urgent movement", + "full-body pressure", + "heavy grinding rhythm" + ], + "leg_detail": [ + "legs spread wide", + "knees pressed to chest", + "one leg lifted high", + "thighs held open", + "legs draped over shoulders", + "kneeling with thighs apart", + "standing with legs braced", + "one foot planted on the bed" + ], + "mouth_detail": [ + "mouth open in a moan", + "bitten lips", + "deep kissing", + "tongue visible", + "breathless parted lips", + "neck kissing", + "eyes locked with a partner", + "open-mouthed climax" + ], + "position": [ + "doggy style position", + "bent-over position", + "spooning anal position", + "edge-of-bed anal position", + "kneeling anal position", + "standing anal position", + "face-down ass-up position", + "side-lying anal position" + ], + "surface": [ + "rumpled bed sheets", + "a low mattress", + "a wide couch", + "floor cushions", + "a velvet chaise", + "a shower bench", + "a soft rug", + "a hotel bed" + ], + "thrust_detail": [ + "deep rear thrusts", + "hips slapping together", + "ass pushed back into each thrust", + "pelvis pressed tight", + "hard grinding motion", + "bodies rocking together", + "thighs shaking from pressure", + "wet skin sliding together" + ], + "visibility": [ + "ass and cock clearly visible", + "anal penetration clearly visible", + "double penetration clearly visible", + "pussy, ass, and cocks visible", + "genital contact anatomically clear", + "spread cheeks exposing penetration", + "open thighs exposing both entries", + "explicit hardcore contact visible" + ] + } + }, + { + "name": "Threesomes", + "slug": "threesomes", + "min_people": 3, + "weight": 1.0, + "item_templates": [ + "{threesome_act} with {body_arrangement}, {oral_detail}, {penetration_detail}, and {visibility}", + "{body_arrangement} while {threesome_act}, with {hand_detail}, {mouth_detail}, and {climax_hint}", + "{angle} threesome view featuring {threesome_act}, {body_contact}, {penetration_detail}, and {visibility}", + "hardcore threesome pose: {threesome_act}, {body_arrangement}, {oral_detail}, and {climax_hint}", + "{threesome_act} on {surface}, with {hand_detail}, {body_contact}, and {visibility}", + "{angle} view of {body_arrangement}, {penetration_detail}, {mouth_detail}, and {intensity}", + "three-body explicit sex pose with {threesome_act}, {oral_detail}, {hand_detail}, and {visibility}", + "{body_arrangement} with {threesome_act}, {intensity}, {body_contact}, and {climax_hint}" + ], + "item_axes": { + "angle": [ + "overhead", + "wide full-body", + "mirror-reflected", + "side-profile", + "low-angle", + "front-facing", + "close-up", + "bed-level" + ], + "body_arrangement": [ + "one person between two partners", + { + "text": "one partner in front and one behind", + "min_people": 3 + }, + "three bodies tangled on the bed", + "one person kneeling between two bodies", + "two partners touching one center partner", + "one person straddled while another penetrates", + "a triangle of bodies on the mattress", + "front-and-back threesome arrangement" + ], + "body_contact": [ + "hands everywhere on breasts, hips, and thighs", + "bodies pressed together from both sides", + "one body pinned between two partners", + "hips grinding from multiple angles", + "hands spreading thighs and ass", + "sweaty skin pressed together", + "legs tangled around both partners", + "one partner holding another's waist" + ], + "climax_hint": [ + "cum on breasts and belly", + "cum dripping on thighs", + "messy wet mouths", + "post-climax fluids on skin", + "visible arousal on all bodies", + "cum on the ass", + "wet sheets under the bodies", + "near-climax expressions" + ], + "hand_detail": [ + "hands gripping hips", + "hands holding breasts", + "fingers spreading thighs", + "hands tangled in hair", + "one hand stroking a cock", + "hands braced on the mattress", + "fingers digging into ass", + "hands pulling bodies closer" + ], + "intensity": [ + "rough consensual rhythm", + "slow deep synchronized motion", + "hard sweaty movement", + "urgent grinding from both sides", + "climax-building intensity", + "full-body erotic pressure", + "desperate hungry motion", + "deep rhythmic thrusting" + ], + "mouth_detail": [ + "open-mouthed moaning", + "deep kissing between two partners", + "tongue visible during oral sex", + "bitten lips", + "breathless gasping", + "mouth on a nipple", + "mouth close to a partner's ear", + "wet lips and saliva" + ], + "oral_detail": [ + { + "text": "one partner licking pussy", + "cast": "women_only" + }, + { + "text": "one partner sucking a strap-on while another partner watches", + "cast": "women_only" + }, + { + "text": "one partner giving a blowjob", + "cast": "men_only" + }, + "one partner giving a blowjob", + "one partner licking pussy", + "sixty-nine oral contact", + "mouth on genitals while penetration happens", + "one partner sucking cock from the front", + "one partner licking from behind", + "oral sex and penetration at the same time", + "a mouth full of cock with saliva visible" + ], + "penetration_detail": [ + { + "text": "visible strap-on vaginal penetration", + "cast": "women_only" + }, + { + "text": "visible toy-assisted pussy penetration", + "cast": "women_only" + }, + { + "text": "visible male/male anal penetration", + "cast": "men_only" + }, + "visible vaginal penetration", + "visible anal penetration", + "front-and-back penetration", + "one cock in pussy", + "one cock in ass", + "penetration while another partner uses their mouth", + "deep thrusting into the center partner", + "explicit genital contact" + ], + "surface": [ + "rumpled bed sheets", + "a wide couch", + "floor cushions", + "a low mattress", + "a hotel bed", + "a soft rug", + "a velvet chaise", + "a shower bench" + ], + "threesome_act": [ + { + "text": "strap-on penetration and cunnilingus at the same time", + "cast": "women_only" + }, + { + "text": "one woman riding a strap-on while another licks pussy", + "cast": "women_only" + }, + { + "text": "male/male oral and anal contact in a three-way scene", + "cast": "men_only" + }, + "oral sex and penetration at the same time", + "double penetration threesome", + "one partner riding while another receives oral", + "one partner penetrated from behind while giving oral", + "three-way oral and genital contact", + "one person between two partners during sex", + "front-and-back hardcore threesome", + "one partner straddling a face while being penetrated" + ], + "visibility": [ + "all genitals clearly visible", + "penetration and oral contact visible", + "pussy, cock, and mouth contact visible", + "explicit genital contact from two angles", + "open thighs and spread bodies visible", + "anatomically clear hardcore contact", + "wet skin and sexual fluids visible", + "the center body and both partners visible" + ] + } + }, + { + "name": "Group sex and orgy", + "slug": "group_sex_orgy", + "min_people": 4, + "weight": 1.0, + "item_templates": [ + "{group_act} with {arrangement}, {contact_detail}, {fluid_detail}, and {visibility}", + "{arrangement} featuring {group_act}, {oral_detail}, {penetration_detail}, and {intensity}", + "{angle} group-sex view with {group_act}, {contact_detail}, {climax_detail}, and {visibility}", + "hardcore orgy pose: {arrangement}, {group_act}, {oral_detail}, and {fluid_detail}", + "{group_act} on {surface}, with {penetration_detail}, {contact_detail}, and {visibility}", + "{angle} view of {arrangement}, {fluid_detail}, {intensity}, and {climax_detail}", + "explicit adult group pile with {group_act}, {oral_detail}, {penetration_detail}, and {visibility}", + "{arrangement} while {group_act}, {contact_detail}, {fluid_detail}, and {intensity}" + ], + "item_axes": { + "angle": [ + "wide full-body", + "overhead", + "mirror-reflected", + "floor-level", + "low-angle", + "front-facing", + "side-profile", + "bed-level" + ], + "arrangement": [ + "bodies tangled across the bed", + "a center person surrounded by partners", + "multiple couples having sex in one frame", + "a ring of bodies on floor cushions", + { + "text": "one person kneeling while partners touch from both sides", + "min_people": 3 + }, + "a layered pile of adult bodies", + "partners arranged front, back, and side", + "a wide orgy scene with all participants visible" + ], + "climax_detail": [ + "cum on breasts and bellies", + "cum dripping down thighs", + "multiple post-climax expressions", + "wet mouths and visible arousal", + "sexual fluids on sheets", + "cum on ass and lower back", + "messy bodies after climax", + "visible orgasmic tension across the group" + ], + "contact_detail": [ + "hands on breasts, hips, thighs, and asses", + "bodies pressed together from every side", + "legs spread and tangled", + "hands spreading thighs", + "fingers gripping sheets and skin", + "mouths on nipples and genitals", + "hips grinding in multiple directions", + "sweaty bodies touching everywhere" + ], + "fluid_detail": [ + "visible cum on skin", + "saliva and cum visible", + "wet genitals and thighs", + "cum dripping onto sheets", + "messy sexual fluids", + "shiny arousal on genitals", + "wet mouths and bodies", + "post-climax fluids on bellies" + ], + "group_act": [ + { + "text": "simultaneous strap-on penetration and cunnilingus", + "cast": "women_only" + }, + { + "text": "women-only group sex with toys, fingers, and oral contact", + "cast": "women_only" + }, + { + "text": "men-only group sex with oral and anal contact", + "cast": "men_only" + }, + "simultaneous oral sex and penetration", + "multiple penetrations happening at once", + "hardcore group sex", + "explicit orgy with visible genital contact", + "one center body receiving several partners", + "several adults giving and receiving oral sex", + "group sex with bodies layered together", + "an adult sex pile with penetration and oral contact" + ], + "intensity": [ + "rough consensual rhythm", + "urgent sweaty movement", + "slow deep group rhythm", + "climax-building chaos", + "heavy grinding from multiple bodies", + "intense full-body pressure", + "messy passionate motion", + "hard synchronized thrusting" + ], + "oral_detail": [ + { + "text": "one mouth on a pussy", + "cast": "women_only" + }, + { + "text": "multiple women giving cunnilingus", + "cast": "women_only" + }, + { + "text": "one mouth on a cock", + "cast": "men_only" + }, + "one mouth on a cock", + "one mouth on a pussy", + "multiple mouths giving oral sex", + "sixty-nine contact inside the group", + "mouths moving between genitals", + "deep blowjob visible", + "cunnilingus visible", + "wet tongues and saliva visible" + ], + "penetration_detail": [ + { + "text": "visible strap-on vaginal penetration", + "cast": "women_only" + }, + { + "text": "toy-assisted penetration visible", + "cast": "women_only" + }, + { + "text": "visible male/male anal penetration", + "cast": "men_only" + }, + "visible vaginal penetration", + "visible anal penetration", + "double penetration visible", + "multiple cocks penetrating", + "front-and-back penetration", + "genitals pressed together", + "open thighs exposing penetration", + "explicit penetrative contact" + ], + "surface": [ + "rumpled bed sheets", + "floor cushions", + "a wide couch", + "a low mattress", + "a soft rug", + "a hotel bed", + "a velvet lounge", + "a shower room floor" + ], + "visibility": [ + "all adult bodies visible", + "multiple genitals clearly visible", + "penetration and oral contact visible", + "explicit contact visible across the scene", + "wide view showing the whole group", + "anatomically clear group sex", + "open bodies and genitals visible", + "hardcore acts visible in one frame" + ] + } + }, + { + "name": "Cumshot and climax", + "slug": "cumshot_climax", + "min_people": 1, + "weight": 1.0, + "item_templates": [ + "{climax_act} with {fluid_location}, {body_position}, {expression_detail}, and {visibility}", + "{body_position} during {climax_act}, with {hand_detail}, {fluid_location}, and {fluid_detail}", + "{angle} climax view featuring {climax_act}, {body_contact}, {fluid_detail}, and {visibility}", + "hardcore post-climax scene with {fluid_location}, {body_position}, {expression_detail}, and {visibility}", + "{climax_act} on {surface}, with {body_contact}, {hand_detail}, and {fluid_detail}", + "{angle} view of {fluid_location}, {body_position}, {climax_act}, and {visibility}", + "explicit orgasm scene: {climax_act}, {fluid_detail}, {expression_detail}, and {body_contact}", + "{body_position} with {fluid_location}, {hand_detail}, {visibility}, and {climax_act}" + ], + "item_axes": { + "angle": [ + "front-facing", + "close-up", + "wide full-body", + "overhead", + "mirror-reflected", + "low-angle", + "side-profile", + "bed-level" + ], + "body_contact": [ + "bodies still pressed together", + "hands gripping hips", + "thighs spread open", + "chests heaving together", + "one body kneeling between legs", + "partners holding each other close", + "hands on breasts and belly", + "wet bodies tangled on sheets" + ], + "body_position": [ + "kneeling with mouth open", + "lying on the back with legs spread", + "sitting on the edge of the bed", + "bent over with ass raised", + "reclining with thighs open", + "standing with cum on the body", + "straddling a partner", + { + "text": "lying between two partners", + "min_people": 3 + } + ], + "climax_act": [ + { + "text": "squirting orgasm", + "cast": "women_only" + }, + { + "text": "wet orgasm during strap-on penetration", + "cast": "women_only" + }, + { + "text": "post-orgasm dripping arousal", + "cast": "women_only" + }, + "external cumshot", + "visible external ejaculation", + "messy post-climax release", + "orgasm during penetration", + "post-orgasm hardcore climax", + "visible orgasm aftermath", + "shared climax after penetration", + "hardcore ejaculation scene" + ], + "expression_detail": [ + "eyes closed in climax", + "open mouth and flushed cheeks", + "tongue out with cum on lips", + "heavy-lidded satisfied gaze", + "breathless post-orgasm expression", + "shameless satisfied smile", + "intense aroused eye contact", + "overwhelmed orgasmic expression" + ], + "fluid_detail": [ + { + "text": "clear arousal dripping on thighs", + "cast": "women_only" + }, + { + "text": "squirting wetness visible on skin", + "cast": "women_only" + }, + { + "text": "wet shine across inner thighs", + "cast": "women_only" + }, + "thick cum visible on skin", + "cum dripping in strands", + "wet shine on thighs", + "saliva and cum mixed on the mouth", + "messy cum across the torso", + "fluid dripping onto sheets", + "cum pooled on the belly", + "visible sexual fluids between thighs" + ], + "fluid_location": [ + { + "text": "clear wetness on thighs", + "cast": "women_only" + }, + { + "text": "squirting fluid on the sheets", + "cast": "women_only" + }, + { + "text": "arousal dripping from pussy", + "cast": "women_only" + }, + "cum on face and lips", + "cum across breasts", + "cum on belly and thighs", + "cum dripping from pussy", + "cum dripping from ass", + "cum on tongue and chin", + "cum on lower back and ass", + "cum on hands and body" + ], + "hand_detail": [ + "hands spreading thighs", + "one hand holding a cock", + "hands gripping sheets", + "hands rubbing cum into skin", + "fingers holding the mouth open", + "hands cupping breasts", + "hands pulling hips closer", + "one hand resting on the belly" + ], + "surface": [ + "rumpled bed sheets", + "a hotel bed", + "floor cushions", + "a wide couch", + "a shower bench", + "a low mattress", + "a soft rug", + "a velvet chaise" + ], + "visibility": [ + "cum clearly visible", + "genitals and fluids visible", + "post-climax fluids anatomically clear", + "face and body covered in visible cum", + "open thighs and wetness visible", + "explicit orgasm aftermath visible", + "hardcore climax detail visible", + "sexual fluids and body contact visible" + ] + } + } + ] + } + ] +} diff --git a/generate_prompt_batches.py b/generate_prompt_batches.py new file mode 100755 index 0000000..e425d74 --- /dev/null +++ b/generate_prompt_batches.py @@ -0,0 +1,3352 @@ +#!/usr/bin/env python3 +"""Generate sxcpinup_coloredpencil prompt batches for gradual image creation.""" + +from __future__ import annotations + +import argparse +import json +import random +import re +import shlex +import sys +from pathlib import Path + + +TRIGGER = "sxcpinup_coloredpencil" +BATCH_SIZE = 200 +DEFAULT_TOTAL = 600 +DEFAULT_START_INDEX = 41 +DEFAULT_RNG_SEED = 20260614 +# Kept intentionally minimal on purpose: extra terms can trip image-model +# content guardrails even when used as negatives. +NEGATIVE_PROMPT = ( + "minors, childlike appearance, watermark" +) + +# Facial expressions are drawn from a deck seeded independently from the main +# generator RNG so the expression axis can be expanded without disturbing the +# other category draws. +EXPRESSION_SEED = 90210 + + +# Appearance pool is explicitly adult and uses heritage cues in the skin field +# so the image model renders demographic variety reliably rather than defaulting +# ambiguous skin+hair combos. Keep "African" on Black/African-diaspora women +# entries so --no-black can filter them; keep East/Southeast/South/Central Asian labels +# so --ethnicity asian catches regional and mixed-heritage entries. +YOUNG_WOMEN = [ + # White / European + ("woman", "21-year-old adult", "slim", "fair Scandinavian skin", "long light-blonde waves", "ice blue eyes"), + ("woman", "23-year-old adult", "slim", "fair rosy skin", "long platinum-blonde waves", "pale blue eyes"), + ("woman", "21-year-old adult", "petite adult", "fair freckled skin", "strawberry-blonde bob", "green eyes"), + ("woman", "25-year-old adult", "curvy athletic", "fair skin", "copper pixie cut", "green eyes"), + ("woman", "23-year-old adult", "hourglass", "fair skin", "fiery red waves", "emerald green eyes"), + ("woman", "21-year-old adult", "petite adult", "warm ivory skin", "short wavy ginger hair", "blue-green eyes"), + # Mediterranean / Latina / Middle-Eastern + ("woman", "22-year-old adult", "curvy", "olive Mediterranean skin", "dark blonde messy bun", "light hazel eyes"), + ("woman", "24-year-old adult", "athletic", "olive skin", "silver-highlighted brunette hair", "hazel eyes"), + ("woman", "25-year-old adult", "hourglass", "sun-kissed Latina skin", "long chocolate-brown waves", "hazel-green eyes"), + ("woman", "22-year-old adult", "curvy", "warm caramel Latina skin", "wavy auburn lob", "amber eyes"), + ("woman", "21-year-old adult", "toned", "tan skin", "high messy bun with loose strands", "light brown eyes"), + ("woman", "24-year-old adult", "busty curvy", "warm Middle-Eastern olive skin", "voluminous dark curls", "deep brown eyes"), + ("woman", "25-year-old adult", "athletic", "bronze skin", "sleek high ponytail", "green-hazel eyes"), + # East Asian + ("woman", "21-year-old adult", "slim", "fair East Asian skin", "long straight black hair", "dark almond eyes"), + ("woman", "24-year-old adult", "petite adult", "light East Asian skin", "sleek black bob", "dark almond eyes"), + ("woman", "23-year-old adult", "slim athletic", "porcelain East Asian skin", "glossy black ponytail", "dark brown eyes"), + ("woman", "25-year-old adult", "slim", "warm East Asian skin", "shoulder-length black hair with curtain bangs", "soft brown eyes"), + # Southeast Asian + ("woman", "22-year-old adult", "average", "warm Southeast Asian tan skin", "long wavy black hair", "dark brown eyes"), + ("woman", "23-year-old adult", "curvy", "golden Southeast Asian skin", "black hair in a high bun", "deep brown eyes"), + # South Asian + ("woman", "24-year-old adult", "curvy", "deep South Asian brown skin", "long glossy black hair", "dark brown eyes"), + ("woman", "22-year-old adult", "slim busty", "warm South Asian brown skin", "thick dark waves", "amber eyes"), + # Black / African + ("woman", "21-year-old adult", "athletic curvy", "deep brown African skin", "braided black hair", "hazel eyes"), + ("woman", "24-year-old adult", "plus-size", "rich dark African skin", "long locs with copper tips", "golden brown eyes"), + ("woman", "23-year-old adult", "average", "warm brown African skin", "natural afro", "dark brown eyes"), + ("woman", "25-year-old adult", "average", "deep brown African skin", "short copper-curl twists", "dark eyes"), + # Mixed / ambiguous + ("woman", "22-year-old adult", "plus-size", "warm brown skin", "short copper curls", "green eyes"), + # Extended (balanced additions) + ("woman", "23-year-old adult", "slim", "fair skin", "long chestnut waves", "blue eyes"), + ("woman", "24-year-old adult", "curvy", "warm caramel Latina skin", "long dark waves", "brown eyes"), + ("woman", "21-year-old adult", "petite adult", "light East Asian skin", "long black hair with blunt bangs", "dark brown eyes"), + ("woman", "25-year-old adult", "athletic", "warm Southeast Asian tan skin", "black hair in a ponytail", "dark brown eyes"), + ("woman", "22-year-old adult", "hourglass", "warm South Asian brown skin", "long glossy black waves", "deep brown eyes"), + ("woman", "24-year-old adult", "curvy athletic", "deep brown African skin", "voluminous natural curls", "dark brown eyes"), + ("woman", "23-year-old adult", "slim busty", "light olive skin", "soft black waves", "gray-blue eyes"), + # Extended round 2 (heritage balance preserved) + # White / European + ("woman", "22-year-old adult", "slim", "fair porcelain skin", "long jet-black waves", "icy gray eyes"), + ("woman", "24-year-old adult", "hourglass", "fair freckled skin", "long honey-blonde waves", "blue eyes"), + ("woman", "21-year-old adult", "toned", "fair skin", "chestnut high ponytail", "hazel eyes"), + ("woman", "25-year-old adult", "curvy", "pale skin", "dark red waves", "green eyes"), + ("woman", "23-year-old adult", "petite adult", "fair rosy skin", "blonde French bob", "blue-gray eyes"), + # Mediterranean / Latina / Middle-Eastern + ("woman", "22-year-old adult", "hourglass", "warm olive skin", "long dark-brown waves", "honey-brown eyes"), + ("woman", "24-year-old adult", "curvy", "golden tan Latina skin", "caramel balayage waves", "light brown eyes"), + ("woman", "23-year-old adult", "athletic", "bronzed Mediterranean skin", "dark wavy lob", "green-hazel eyes"), + ("woman", "25-year-old adult", "busty curvy", "warm Middle-Eastern skin", "long black waves", "deep brown eyes"), + # East Asian + ("woman", "22-year-old adult", "slim", "fair East Asian skin", "long black hair with soft layers", "dark almond eyes"), + ("woman", "24-year-old adult", "slim athletic", "light East Asian skin", "sleek black hair with a center part", "dark brown eyes"), + ("woman", "21-year-old adult", "petite adult", "porcelain East Asian skin", "black hair in twin buns", "dark almond eyes"), + ("woman", "25-year-old adult", "curvy", "warm East Asian skin", "wavy black lob", "soft brown eyes"), + # Southeast Asian + ("woman", "23-year-old adult", "slim busty", "warm Southeast Asian tan skin", "long wavy black hair", "deep brown eyes"), + ("woman", "22-year-old adult", "curvy", "golden Southeast Asian skin", "black hair in loose waves", "dark brown eyes"), + # South Asian + ("woman", "24-year-old adult", "hourglass", "warm South Asian brown skin", "long glossy black waves", "deep brown eyes"), + ("woman", "21-year-old adult", "slim", "deep South Asian brown skin", "thick black braid", "dark brown eyes"), + # Black / African + ("woman", "22-year-old adult", "athletic curvy", "deep brown African skin", "long box braids", "dark brown eyes"), + ("woman", "24-year-old adult", "hourglass", "warm brown African skin", "natural curls in a high puff", "golden brown eyes"), + ("woman", "25-year-old adult", "curvy", "rich dark African skin", "sleek straight black hair", "dark eyes"), + # Mixed / ambiguous + ("woman", "23-year-old adult", "slim busty", "warm golden-brown skin", "long honey-brown curls", "light hazel eyes"), + ("woman", "22-year-old adult", "athletic", "warm tan skin", "dark curly hair in a bun", "amber eyes"), + # Extended round 3: expanded demographic buckets + # White / European regional variety + ("woman", "21-year-old adult", "slim", "fair Nordic European skin", "long ash-blonde hair", "blue-gray eyes"), + ("woman", "22-year-old adult", "curvy", "fair Celtic freckled skin", "long copper curls", "green eyes"), + ("woman", "23-year-old adult", "athletic", "pale Slavic European skin", "straight dark-blonde hair", "gray-blue eyes"), + ("woman", "24-year-old adult", "hourglass", "fair Baltic European skin", "honey-blonde waves", "light green eyes"), + ("woman", "25-year-old adult", "average", "ivory Western European skin", "soft brunette lob", "hazel eyes"), + ("woman", "21-year-old adult", "petite adult", "porcelain Alpine European skin", "short black bob", "ice blue eyes"), + ("woman", "22-year-old adult", "toned", "fair Balkan European skin", "dark chestnut ponytail", "amber-brown eyes"), + ("woman", "23-year-old adult", "slim busty", "rosy Irish skin", "long auburn waves", "emerald eyes"), + ("woman", "24-year-old adult", "curvy athletic", "fair French skin", "caramel-blonde shag", "blue eyes"), + ("woman", "25-year-old adult", "plus-size", "pale freckled European skin", "long strawberry-blonde curls", "green-gray eyes"), + ("woman", "21-year-old adult", "slim", "warm tanned European skin", "sunlit brown waves", "hazel eyes"), + ("woman", "22-year-old adult", "hourglass", "fair porcelain European skin", "platinum pixie cut", "gray eyes"), + # Mediterranean / Latina / Middle-Eastern / MENA variety + ("woman", "23-year-old adult", "curvy", "olive Greek Mediterranean skin", "long espresso waves", "green-hazel eyes"), + ("woman", "24-year-old adult", "athletic", "warm Italian Mediterranean skin", "dark curly lob", "amber eyes"), + ("woman", "25-year-old adult", "hourglass", "golden Spanish Mediterranean skin", "chestnut balayage hair", "brown eyes"), + ("woman", "21-year-old adult", "petite adult", "light Portuguese Mediterranean skin", "short wavy brunette hair", "hazel eyes"), + ("woman", "22-year-old adult", "curvy", "warm Turkish olive skin", "long black waves", "dark brown eyes"), + ("woman", "23-year-old adult", "slim busty", "warm Persian olive skin", "glossy dark waves", "honey-brown eyes"), + ("woman", "24-year-old adult", "average", "warm Levantine olive skin", "dark hair in a loose bun", "deep brown eyes"), + ("woman", "25-year-old adult", "busty curvy", "golden Maghrebi olive skin", "voluminous dark curls", "amber eyes"), + ("woman", "21-year-old adult", "toned", "sun-kissed Mexican Latina skin", "long black waves", "dark brown eyes"), + ("woman", "22-year-old adult", "hourglass", "warm Colombian Latina skin", "caramel-highlighted curls", "light brown eyes"), + ("woman", "23-year-old adult", "curvy athletic", "golden Brazilian Latina skin", "long honey-brown waves", "hazel-green eyes"), + ("woman", "24-year-old adult", "plus-size", "warm Puerto Rican Latina skin", "auburn curls", "amber-brown eyes"), + ("woman", "25-year-old adult", "slim", "bronze Cuban Latina skin", "sleek black bob", "dark eyes"), + ("woman", "21-year-old adult", "petite adult", "warm Andean Latina skin", "long dark braid", "deep brown eyes"), + ("woman", "22-year-old adult", "curvy", "golden Dominican Latina skin", "dark curls with caramel tips", "honey-brown eyes"), + ("woman", "23-year-old adult", "athletic", "warm Chilean Latina skin", "dark ponytail with loose strands", "brown eyes"), + # East Asian regional variety + ("woman", "21-year-old adult", "slim", "fair Japanese East Asian skin", "long straight black hair", "dark almond eyes"), + ("woman", "22-year-old adult", "petite adult", "light Korean East Asian skin", "soft black bob with bangs", "dark brown eyes"), + ("woman", "23-year-old adult", "slim athletic", "porcelain Han Chinese East Asian skin", "long layered black hair", "dark almond eyes"), + ("woman", "24-year-old adult", "curvy", "warm Taiwanese East Asian skin", "wavy black lob", "soft brown eyes"), + ("woman", "25-year-old adult", "average", "fair Mongolian East Asian skin", "thick black braid", "dark brown eyes"), + ("woman", "21-year-old adult", "toned", "light Tibetan East Asian skin", "dark hair in a high ponytail", "deep brown eyes"), + ("woman", "22-year-old adult", "hourglass", "fair Manchu East Asian skin", "glossy black waves", "dark almond eyes"), + ("woman", "23-year-old adult", "slim busty", "warm Japanese East Asian skin", "black wolf-cut hair", "brown eyes"), + ("woman", "24-year-old adult", "athletic", "light Korean East Asian skin", "sleek center-parted hair", "dark almond eyes"), + ("woman", "25-year-old adult", "plus-size", "warm East Asian skin", "shoulder-length black curls", "deep brown eyes"), + ("woman", "21-year-old adult", "petite adult", "porcelain Taiwanese East Asian skin", "short black pixie cut", "soft brown eyes"), + ("woman", "22-year-old adult", "curvy athletic", "fair East Asian skin", "long black hair with curtain bangs", "dark brown eyes"), + # Southeast Asian regional variety + ("woman", "21-year-old adult", "slim", "warm Vietnamese Southeast Asian skin", "long glossy black hair", "dark brown eyes"), + ("woman", "22-year-old adult", "petite adult", "golden Thai Southeast Asian skin", "black hair in a high bun", "deep brown eyes"), + ("woman", "23-year-old adult", "curvy", "warm Filipina Southeast Asian skin", "wavy black hair with caramel streaks", "brown eyes"), + ("woman", "24-year-old adult", "athletic", "deep Indonesian Southeast Asian tan skin", "long black ponytail", "dark eyes"), + ("woman", "25-year-old adult", "hourglass", "golden Malay Southeast Asian skin", "soft black waves", "dark brown eyes"), + ("woman", "21-year-old adult", "average", "warm Cambodian Southeast Asian skin", "dark shoulder-length hair", "deep brown eyes"), + ("woman", "22-year-old adult", "slim busty", "warm Lao Southeast Asian skin", "long black hair with side-swept bangs", "brown eyes"), + ("woman", "23-year-old adult", "curvy athletic", "golden Burmese Southeast Asian skin", "thick black braid", "dark brown eyes"), + ("woman", "24-year-old adult", "plus-size", "warm Southeast Asian tan skin", "voluminous black curls", "dark eyes"), + ("woman", "25-year-old adult", "toned", "golden Singaporean Southeast Asian skin", "sleek black bob", "soft brown eyes"), + ("woman", "21-year-old adult", "petite adult", "warm Hmong Southeast Asian skin", "straight black hair with blunt bangs", "deep brown eyes"), + ("woman", "22-year-old adult", "hourglass", "golden Balinese Southeast Asian skin", "long wavy black hair", "dark brown eyes"), + # South Asian regional variety + ("woman", "21-year-old adult", "slim", "warm North Indian South Asian brown skin", "long dark waves", "deep brown eyes"), + ("woman", "22-year-old adult", "curvy", "golden Punjabi South Asian skin", "thick black braid", "dark brown eyes"), + ("woman", "23-year-old adult", "hourglass", "deep Tamil South Asian brown skin", "long glossy black hair", "dark eyes"), + ("woman", "24-year-old adult", "athletic", "warm Bengali South Asian skin", "black hair in a low bun", "brown eyes"), + ("woman", "25-year-old adult", "plus-size", "deep Sri Lankan South Asian brown skin", "wavy black hair", "dark brown eyes"), + ("woman", "21-year-old adult", "petite adult", "light Nepali South Asian skin", "long dark braid", "amber-brown eyes"), + ("woman", "22-year-old adult", "slim busty", "warm Pakistani South Asian skin", "soft black waves", "deep brown eyes"), + ("woman", "23-year-old adult", "curvy athletic", "golden Gujarati South Asian skin", "dark curls over one shoulder", "hazel-brown eyes"), + ("woman", "24-year-old adult", "average", "warm Bangladeshi South Asian skin", "straight black hair", "dark eyes"), + ("woman", "25-year-old adult", "toned", "deep Malayali South Asian brown skin", "long black ponytail", "dark brown eyes"), + ("woman", "21-year-old adult", "curvy", "warm Kashmiri South Asian skin", "dark chestnut waves", "green-brown eyes"), + ("woman", "22-year-old adult", "hourglass", "deep South Asian brown skin", "thick black curls", "golden brown eyes"), + # Central Asian / Indigenous / Pacific variety + ("woman", "21-year-old adult", "slim", "light Kazakh Central Asian skin", "dark blonde waves", "hazel eyes"), + ("woman", "22-year-old adult", "athletic", "warm Uzbek Central Asian skin", "long dark braid", "amber eyes"), + ("woman", "23-year-old adult", "curvy", "golden Kyrgyz Central Asian skin", "thick brown ponytail", "brown eyes"), + ("woman", "24-year-old adult", "average", "warm Uighur Central Asian skin", "long black waves", "green-hazel eyes"), + ("woman", "25-year-old adult", "hourglass", "warm Armenian skin", "voluminous dark curls", "deep brown eyes"), + ("woman", "21-year-old adult", "petite adult", "warm Indigenous Mexican skin", "long straight black hair", "dark brown eyes"), + ("woman", "22-year-old adult", "curvy athletic", "deep Indigenous Andean skin", "thick black braid", "dark eyes"), + ("woman", "23-year-old adult", "average", "warm Native American skin", "long black hair", "dark brown eyes"), + ("woman", "24-year-old adult", "curvy", "golden Pacific Islander skin", "long dark waves", "brown eyes"), + ("woman", "25-year-old adult", "plus-size", "deep Polynesian Pacific Islander skin", "thick black curls", "dark brown eyes"), + # Black / African diaspora regional variety + ("woman", "21-year-old adult", "athletic", "deep West African skin", "short natural coils", "dark brown eyes"), + ("woman", "22-year-old adult", "curvy", "rich Ghanaian African skin", "long box braids", "golden brown eyes"), + ("woman", "23-year-old adult", "hourglass", "deep Nigerian African skin", "braided updo", "dark eyes"), + ("woman", "24-year-old adult", "slim", "warm Ethiopian African skin", "soft black curls", "amber eyes"), + ("woman", "25-year-old adult", "plus-size", "rich Kenyan African skin", "long locs", "deep brown eyes"), + ("woman", "21-year-old adult", "curvy athletic", "deep Senegalese African skin", "cornrow braids with beads", "brown eyes"), + ("woman", "22-year-old adult", "average", "warm Somalian African skin", "sleek dark ponytail", "dark brown eyes"), + ("woman", "23-year-old adult", "busty curvy", "rich African-Caribbean brown skin", "voluminous natural curls", "honey-brown eyes"), + ("woman", "24-year-old adult", "athletic", "deep African-American brown skin", "short tapered curls", "dark eyes"), + ("woman", "25-year-old adult", "hourglass", "warm African-British brown skin", "long twists", "golden brown eyes"), + ("woman", "21-year-old adult", "petite adult", "deep Sudanese African skin", "black curls in a puff", "dark brown eyes"), + ("woman", "22-year-old adult", "plus-size", "rich African-diaspora brown skin", "copper-highlighted locs", "amber-brown eyes"), + # Mixed heritage combinations + ("woman", "21-year-old adult", "slim", "fair East Asian-European mixed skin", "long ash-brown waves", "gray-brown eyes"), + ("woman", "22-year-old adult", "petite adult", "light Korean European mixed skin", "soft black bob", "blue-gray eyes"), + ("woman", "23-year-old adult", "curvy", "warm Japanese Latina mixed skin", "dark wavy lob", "hazel eyes"), + ("woman", "24-year-old adult", "athletic", "golden Chinese Latina mixed skin", "long black ponytail", "light brown eyes"), + ("woman", "25-year-old adult", "hourglass", "warm East Asian Mediterranean mixed skin", "long espresso waves", "green-brown eyes"), + ("woman", "21-year-old adult", "slim busty", "light East Asian Middle-Eastern mixed skin", "glossy black waves", "amber eyes"), + ("woman", "22-year-old adult", "curvy athletic", "warm Vietnamese European mixed skin", "black hair with honey highlights", "hazel eyes"), + ("woman", "23-year-old adult", "average", "golden Filipina European mixed skin", "soft brown curls", "green eyes"), + ("woman", "24-year-old adult", "plus-size", "warm Southeast Asian Latina mixed skin", "long dark curls", "brown eyes"), + ("woman", "25-year-old adult", "toned", "golden Thai Middle-Eastern mixed skin", "sleek black high ponytail", "deep brown eyes"), + ("woman", "21-year-old adult", "petite adult", "warm South Asian European mixed skin", "dark chestnut waves", "hazel-green eyes"), + ("woman", "22-year-old adult", "hourglass", "golden South Asian Latina mixed skin", "long black curls", "honey-brown eyes"), + ("woman", "23-year-old adult", "curvy", "warm South Asian Middle-Eastern mixed skin", "thick dark waves", "amber-brown eyes"), + ("woman", "24-year-old adult", "athletic", "deep South Asian Southeast Asian mixed skin", "black hair in a braid", "dark brown eyes"), + ("woman", "25-year-old adult", "slim busty", "warm South Asian East Asian mixed skin", "long glossy black hair", "soft brown eyes"), + ("woman", "21-year-old adult", "curvy athletic", "golden Central Asian European mixed skin", "dark blonde curls", "green-hazel eyes"), + ("woman", "22-year-old adult", "average", "warm Central Asian Middle-Eastern mixed skin", "long brown waves", "amber eyes"), + ("woman", "23-year-old adult", "hourglass", "warm Indigenous Latina European mixed skin", "long dark waves", "hazel eyes"), + ("woman", "24-year-old adult", "curvy", "deep Indigenous Latina skin", "thick black curls", "dark brown eyes"), + ("woman", "25-year-old adult", "athletic", "golden Pacific Islander Asian mixed skin", "long black waves", "brown eyes"), + ("woman", "21-year-old adult", "slim", "warm Pacific Islander European mixed skin", "soft dark-brown waves", "green-brown eyes"), + ("woman", "22-year-old adult", "curvy", "warm Middle-Eastern Latina mixed skin", "voluminous dark curls", "amber eyes"), + ("woman", "23-year-old adult", "plus-size", "golden Mediterranean Latina mixed skin", "long caramel curls", "light brown eyes"), + ("woman", "24-year-old adult", "toned", "warm Turkish European mixed skin", "dark wavy ponytail", "hazel eyes"), + ("woman", "25-year-old adult", "average", "warm Persian South Asian mixed skin", "glossy black waves", "deep brown eyes"), + ("woman", "21-year-old adult", "athletic curvy", "deep African-European mixed skin", "brown curls with blonde tips", "hazel eyes"), + ("woman", "22-year-old adult", "hourglass", "warm African-Latina mixed skin", "long dark curls", "golden brown eyes"), + ("woman", "23-year-old adult", "slim busty", "deep African-East Asian mixed skin", "sleek black bob", "dark almond eyes"), + ("woman", "24-year-old adult", "curvy athletic", "warm African-South Asian mixed skin", "long braided black hair", "deep brown eyes"), + ("woman", "25-year-old adult", "plus-size", "rich African-Pacific Islander mixed skin", "voluminous black curls", "dark brown eyes"), + ("woman", "21-year-old adult", "petite adult", "warm African-Middle-Eastern mixed skin", "soft dark curls", "amber-brown eyes"), + ("woman", "22-year-old adult", "curvy", "deep African-Caribbean Latina mixed skin", "long locs with copper tips", "honey-brown eyes"), + ("woman", "23-year-old adult", "athletic", "warm African-Native American mixed skin", "thick black waves", "dark brown eyes"), + ("woman", "24-year-old adult", "hourglass", "golden African-Southeast Asian mixed skin", "black curls in a high puff", "brown eyes"), + ("woman", "25-year-old adult", "average", "warm African-Central Asian mixed skin", "dark brown waves", "hazel-brown eyes"), +] + +_YOUNG_EXPANSION_AGES = ( + "21-year-old adult", + "22-year-old adult", + "23-year-old adult", + "24-year-old adult", + "25-year-old adult", +) + +_YOUNG_EXPANSION_BODIES = ( + "slim", + "petite adult", + "toned", + "athletic", + "average", + "curvy", + "curvy athletic", + "hourglass", + "slim busty", + "busty curvy", + "soft curvy", + "plus-size", +) + +_YOUNG_VARIANTS_PER_PROFILE = 8 + + +def _expand_young_demographics(profiles: list[tuple[str, tuple[str, ...], tuple[str, ...]]]) -> list[tuple[str, str, str, str, str, str]]: + """Deterministically expand demographic profiles into balanced adult variants. + + Each profile contributes the same number of entries so larger named buckets do + not accidentally dominate the random sampler. + """ + seen = set(YOUNG_WOMEN) + expanded: list[tuple[str, str, str, str, str, str]] = [] + for profile_index, (skin, hair_options, eye_options) in enumerate(profiles): + for variant_index in range(_YOUNG_VARIANTS_PER_PROFILE): + entry = ( + "woman", + _YOUNG_EXPANSION_AGES[(profile_index + variant_index) % len(_YOUNG_EXPANSION_AGES)], + _YOUNG_EXPANSION_BODIES[(profile_index * 5 + variant_index * 7) % len(_YOUNG_EXPANSION_BODIES)], + skin, + hair_options[variant_index % len(hair_options)], + eye_options[(profile_index + variant_index * 2) % len(eye_options)], + ) + if entry not in seen: + seen.add(entry) + expanded.append(entry) + return expanded + + +YOUNG_WOMEN.extend( + _expand_young_demographics( + [ + # White / European regional profiles + ("fair Nordic European skin", ("long ash-blonde waves", "pale blonde bob", "braided flaxen hair", "soft sandy-blonde curls"), ("ice blue eyes", "blue-gray eyes", "light green eyes", "gray eyes")), + ("fair Swedish European skin", ("long honey-blonde hair", "short blonde pixie cut", "loose dark-blonde waves", "sleek blonde ponytail"), ("blue eyes", "gray-blue eyes", "green eyes", "pale hazel eyes")), + ("fair Norwegian European skin", ("platinum waves", "dark-blonde braid", "light-brown shag", "long wheat-blonde curls"), ("ice blue eyes", "blue eyes", "gray eyes", "green-gray eyes")), + ("fair Danish European skin", ("straight ash-brown hair", "soft blonde lob", "messy sandy bun", "long beige-blonde waves"), ("blue-gray eyes", "hazel eyes", "green eyes", "gray eyes")), + ("pale Celtic European skin", ("long copper curls", "auburn bob", "fiery red waves", "ginger pixie cut"), ("emerald eyes", "green-gray eyes", "blue eyes", "hazel eyes")), + ("fair Irish European skin", ("strawberry-blonde waves", "dark auburn curls", "red hair in a loose bun", "copper braid"), ("green eyes", "blue-green eyes", "gray eyes", "hazel eyes")), + ("fair Scottish European skin", ("long chestnut waves", "short copper curls", "dark-red lob", "freckled blonde ponytail"), ("green eyes", "blue eyes", "gray-green eyes", "amber eyes")), + ("pale Slavic European skin", ("straight dark-blonde hair", "long ash-brown waves", "soft brunette bob", "platinum braid"), ("gray-blue eyes", "green eyes", "blue eyes", "hazel eyes")), + ("fair Polish European skin", ("honey-brown waves", "dark-blonde lob", "long chestnut curls", "sleek brown ponytail"), ("blue-gray eyes", "hazel eyes", "green eyes", "gray eyes")), + ("fair Ukrainian European skin", ("long golden-brown waves", "dark blonde braid", "soft auburn hair", "straight brunette hair"), ("green eyes", "blue eyes", "gray eyes", "hazel-brown eyes")), + ("fair Baltic European skin", ("long honey-blonde waves", "ash-brown bob", "pale blonde braid", "soft chestnut curls"), ("light green eyes", "blue eyes", "gray-blue eyes", "hazel eyes")), + ("ivory French European skin", ("caramel-blonde shag", "brunette French bob", "long chestnut waves", "soft black pixie cut"), ("blue eyes", "hazel eyes", "green eyes", "gray eyes")), + ("warm Germanic European skin", ("dark blonde waves", "straight chestnut hair", "honey-blonde ponytail", "auburn lob"), ("blue-gray eyes", "hazel eyes", "green eyes", "brown eyes")), + ("fair Dutch European skin", ("long flaxen hair", "short blonde bob", "soft brown waves", "messy ash-blonde bun"), ("blue eyes", "green eyes", "gray eyes", "hazel eyes")), + ("warm Balkan European skin", ("dark chestnut ponytail", "long black waves", "auburn curls", "soft brunette lob"), ("amber-brown eyes", "green eyes", "hazel eyes", "brown eyes")), + ("fair Alpine European skin", ("short black bob", "long brunette waves", "blonde braid", "soft copper curls"), ("ice blue eyes", "gray eyes", "green eyes", "hazel eyes")), + # Mediterranean / Latina / Middle-Eastern / MENA profiles + ("olive Greek Mediterranean skin", ("long espresso waves", "dark curly lob", "black hair in a loose bun", "chestnut balayage hair"), ("green-hazel eyes", "amber eyes", "brown eyes", "dark eyes")), + ("warm Italian Mediterranean skin", ("dark curly lob", "long chestnut curls", "soft brunette waves", "sleek black ponytail"), ("amber eyes", "hazel eyes", "green eyes", "brown eyes")), + ("golden Spanish Mediterranean skin", ("chestnut balayage hair", "long dark waves", "soft brown curls", "black bob with side part"), ("brown eyes", "hazel eyes", "green-hazel eyes", "amber eyes")), + ("light Portuguese Mediterranean skin", ("short wavy brunette hair", "long dark-blonde waves", "caramel curls", "messy chestnut bun"), ("hazel eyes", "brown eyes", "green eyes", "gray-brown eyes")), + ("warm Turkish olive skin", ("long black waves", "dark hair in a high ponytail", "soft chestnut curls", "sleek brunette bob"), ("dark brown eyes", "amber eyes", "hazel eyes", "green-brown eyes")), + ("warm Persian olive skin", ("glossy dark waves", "long black curls", "dark-brown hair with caramel tips", "soft black lob"), ("honey-brown eyes", "deep brown eyes", "amber eyes", "green-hazel eyes")), + ("warm Levantine olive skin", ("dark hair in a loose bun", "long black waves", "chestnut curls", "sleek dark ponytail"), ("deep brown eyes", "hazel eyes", "amber eyes", "green-brown eyes")), + ("golden Maghrebi olive skin", ("voluminous dark curls", "long black waves", "dark auburn hair", "curly brunette bob"), ("amber eyes", "brown eyes", "hazel eyes", "dark eyes")), + ("warm Egyptian skin", ("long black waves", "dark curly hair", "sleek black bob", "chestnut ponytail"), ("deep brown eyes", "amber eyes", "hazel-brown eyes", "dark eyes")), + ("warm Moroccan Amazigh skin", ("dark curls with honey tips", "long black hair", "braided brunette hair", "soft chestnut waves"), ("amber eyes", "hazel eyes", "brown eyes", "green-brown eyes")), + ("warm Kurdish skin", ("long dark waves", "black hair in a braid", "soft brunette curls", "dark auburn lob"), ("hazel eyes", "green-brown eyes", "amber eyes", "deep brown eyes")), + ("sun-kissed Mexican Latina skin", ("long black waves", "dark curls", "straight brunette hair", "caramel-highlighted ponytail"), ("dark brown eyes", "hazel eyes", "amber-brown eyes", "brown eyes")), + ("warm Chicana Latina skin", ("long espresso hair", "soft black waves", "auburn curls", "brown hair with blonde streaks"), ("brown eyes", "hazel eyes", "green-brown eyes", "amber eyes")), + ("warm Colombian Latina skin", ("caramel-highlighted curls", "long dark waves", "soft brown lob", "black ponytail with loose strands"), ("light brown eyes", "hazel eyes", "green eyes", "deep brown eyes")), + ("golden Brazilian Latina skin", ("long honey-brown waves", "voluminous dark curls", "caramel balayage hair", "sleek black bob"), ("hazel-green eyes", "brown eyes", "amber eyes", "green eyes")), + ("warm Puerto Rican Latina skin", ("auburn curls", "dark curls with caramel tips", "long chestnut waves", "black hair in a high bun"), ("amber-brown eyes", "hazel eyes", "brown eyes", "green-brown eyes")), + ("bronze Cuban Latina skin", ("sleek black bob", "long dark waves", "soft brunette curls", "caramel ponytail"), ("dark eyes", "brown eyes", "hazel eyes", "amber eyes")), + ("golden Dominican Latina skin", ("dark curls with caramel tips", "long black waves", "voluminous chestnut curls", "sleek ponytail"), ("honey-brown eyes", "dark brown eyes", "hazel eyes", "amber eyes")), + ("warm Venezuelan Latina skin", ("long chocolate waves", "black curls", "caramel-highlighted lob", "soft auburn hair"), ("brown eyes", "hazel eyes", "amber eyes", "green-brown eyes")), + ("warm Peruvian Latina skin", ("long dark braid", "straight black hair", "soft brunette waves", "black hair in a bun"), ("deep brown eyes", "dark eyes", "hazel-brown eyes", "amber eyes")), + ("warm Chilean Latina skin", ("dark ponytail with loose strands", "long brunette waves", "soft black lob", "auburn curls"), ("brown eyes", "hazel eyes", "amber eyes", "green-brown eyes")), + ("light Argentine Latina skin", ("long dark-blonde waves", "soft brunette curls", "copper lob", "sleek brown ponytail"), ("hazel eyes", "green eyes", "brown eyes", "blue-gray eyes")), + ("warm Uruguayan Latina skin", ("soft chestnut waves", "dark-blonde bob", "long brunette curls", "black pixie cut"), ("brown eyes", "hazel eyes", "green eyes", "amber eyes")), + ("golden Ecuadorian Latina skin", ("long black waves", "dark braid", "soft brown curls", "sleek brunette bob"), ("dark brown eyes", "hazel-brown eyes", "amber eyes", "brown eyes")), + # East Asian profiles + ("fair Japanese East Asian skin", ("long straight black hair", "black wolf-cut hair", "short black pixie cut", "soft black bob with bangs"), ("dark almond eyes", "soft brown eyes", "dark brown eyes", "gray-brown eyes")), + ("warm Japanese East Asian skin", ("long layered black hair", "glossy black waves", "sleek black ponytail", "short black bob"), ("brown eyes", "dark almond eyes", "soft brown eyes", "deep brown eyes")), + ("light Korean East Asian skin", ("soft black bob with bangs", "sleek center-parted hair", "long black waves", "black hair in a low bun"), ("dark brown eyes", "dark almond eyes", "gray-brown eyes", "soft brown eyes")), + ("fair Korean East Asian skin", ("long black hair with curtain bangs", "short glossy bob", "black ponytail", "soft layered hair"), ("dark almond eyes", "deep brown eyes", "soft brown eyes", "gray eyes")), + ("porcelain Han Chinese East Asian skin", ("long layered black hair", "straight black hair with blunt bangs", "glossy black ponytail", "soft black lob"), ("dark almond eyes", "dark brown eyes", "soft brown eyes", "deep brown eyes")), + ("warm Han Chinese East Asian skin", ("long black waves", "sleek black bob", "black braid", "shoulder-length black curls"), ("dark brown eyes", "soft brown eyes", "dark almond eyes", "brown eyes")), + ("warm Taiwanese East Asian skin", ("wavy black lob", "long black hair with soft layers", "black hair in a high bun", "sleek bob with side part"), ("soft brown eyes", "dark brown eyes", "dark almond eyes", "hazel-brown eyes")), + ("fair Mongolian East Asian skin", ("thick black braid", "long dark waves", "straight black ponytail", "soft brown-black curls"), ("dark brown eyes", "brown eyes", "gray-brown eyes", "dark almond eyes")), + ("light Tibetan East Asian skin", ("dark hair in a high ponytail", "long black braid", "soft black waves", "straight dark hair"), ("deep brown eyes", "dark brown eyes", "amber-brown eyes", "soft brown eyes")), + ("fair Manchu East Asian skin", ("glossy black waves", "black hair in twin buns", "long straight black hair", "short black bob"), ("dark almond eyes", "dark brown eyes", "soft brown eyes", "gray-brown eyes")), + ("warm Hong Kong Chinese East Asian skin", ("sleek black lob", "long black waves", "black hair with subtle brown highlights", "messy high bun"), ("dark brown eyes", "soft brown eyes", "dark almond eyes", "brown eyes")), + ("light Okinawan East Asian skin", ("soft black waves", "long straight black hair", "black bob with bangs", "loose black ponytail"), ("dark almond eyes", "brown eyes", "soft brown eyes", "deep brown eyes")), + # Southeast Asian profiles + ("warm Vietnamese Southeast Asian skin", ("long glossy black hair", "straight black hair with curtain bangs", "dark hair in a low bun", "soft black waves"), ("dark brown eyes", "deep brown eyes", "brown eyes", "soft brown eyes")), + ("golden Thai Southeast Asian skin", ("black hair in a high bun", "long wavy black hair", "sleek black bob", "soft dark curls"), ("deep brown eyes", "dark brown eyes", "brown eyes", "amber-brown eyes")), + ("warm Filipina Southeast Asian skin", ("wavy black hair with caramel streaks", "long dark waves", "soft brown curls", "black ponytail"), ("brown eyes", "dark brown eyes", "hazel-brown eyes", "deep brown eyes")), + ("deep Indonesian Southeast Asian tan skin", ("long black ponytail", "soft black waves", "dark curls", "straight black hair"), ("dark eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("golden Malay Southeast Asian skin", ("soft black waves", "long glossy black hair", "sleek black bob", "dark hair in a loose bun"), ("dark brown eyes", "brown eyes", "soft brown eyes", "deep brown eyes")), + ("warm Cambodian Southeast Asian skin", ("dark shoulder-length hair", "long black hair", "black braid", "soft dark waves"), ("deep brown eyes", "dark brown eyes", "brown eyes", "amber-brown eyes")), + ("warm Lao Southeast Asian skin", ("long black hair with side-swept bangs", "straight black hair", "dark ponytail", "soft black curls"), ("brown eyes", "dark brown eyes", "deep brown eyes", "soft brown eyes")), + ("golden Burmese Southeast Asian skin", ("thick black braid", "long black waves", "sleek ponytail", "soft dark lob"), ("dark brown eyes", "brown eyes", "deep brown eyes", "amber-brown eyes")), + ("golden Singaporean Southeast Asian skin", ("sleek black bob", "long black waves", "black hair with brown highlights", "messy black bun"), ("soft brown eyes", "dark brown eyes", "brown eyes", "hazel-brown eyes")), + ("warm Hmong Southeast Asian skin", ("straight black hair with blunt bangs", "long black braid", "dark hair in a bun", "soft black waves"), ("deep brown eyes", "dark brown eyes", "brown eyes", "soft brown eyes")), + ("golden Balinese Southeast Asian skin", ("long wavy black hair", "black hair with sunlit brown tips", "soft dark curls", "sleek ponytail"), ("dark brown eyes", "brown eyes", "deep brown eyes", "amber-brown eyes")), + ("warm Javanese Southeast Asian skin", ("long black waves", "dark hair in a low bun", "soft black bob", "straight black ponytail"), ("deep brown eyes", "dark eyes", "brown eyes", "amber-brown eyes")), + # South Asian profiles + ("warm North Indian South Asian brown skin", ("long dark waves", "thick black braid", "soft black curls", "dark chestnut hair"), ("deep brown eyes", "dark brown eyes", "hazel-brown eyes", "amber eyes")), + ("golden Punjabi South Asian skin", ("thick black braid", "long glossy black hair", "dark waves over one shoulder", "soft brown-black curls"), ("dark brown eyes", "deep brown eyes", "amber-brown eyes", "hazel eyes")), + ("deep Tamil South Asian brown skin", ("long glossy black hair", "black hair in a low bun", "thick black curls", "straight black ponytail"), ("dark eyes", "deep brown eyes", "brown eyes", "golden brown eyes")), + ("warm Bengali South Asian skin", ("black hair in a low bun", "long black waves", "dark braid", "soft black lob"), ("brown eyes", "deep brown eyes", "dark eyes", "amber-brown eyes")), + ("deep Sri Lankan South Asian brown skin", ("wavy black hair", "long glossy black waves", "black curls", "dark hair with copper tips"), ("dark brown eyes", "deep brown eyes", "golden brown eyes", "dark eyes")), + ("light Nepali South Asian skin", ("long dark braid", "soft black waves", "straight dark-brown hair", "dark ponytail"), ("amber-brown eyes", "dark brown eyes", "hazel-brown eyes", "soft brown eyes")), + ("warm Pakistani South Asian skin", ("soft black waves", "long dark curls", "thick black braid", "dark chestnut lob"), ("deep brown eyes", "dark brown eyes", "amber eyes", "hazel-brown eyes")), + ("golden Gujarati South Asian skin", ("dark curls over one shoulder", "long black waves", "sleek dark ponytail", "soft brown-black hair"), ("hazel-brown eyes", "deep brown eyes", "amber eyes", "dark brown eyes")), + ("warm Bangladeshi South Asian skin", ("straight black hair", "long black waves", "dark braid", "soft black curls"), ("dark eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("deep Malayali South Asian brown skin", ("long black ponytail", "thick black curls", "glossy black waves", "black hair in a bun"), ("dark brown eyes", "deep brown eyes", "golden brown eyes", "dark eyes")), + ("warm Kashmiri South Asian skin", ("dark chestnut waves", "long black hair", "soft brown curls", "dark braid"), ("green-brown eyes", "hazel eyes", "amber eyes", "deep brown eyes")), + ("warm Marathi South Asian skin", ("long dark waves", "black hair with brown highlights", "thick braid", "soft black lob"), ("brown eyes", "deep brown eyes", "hazel-brown eyes", "amber eyes")), + # Central Asian / West Asian profiles + ("light Kazakh Central Asian skin", ("dark blonde waves", "long brown braid", "soft black hair", "chestnut ponytail"), ("hazel eyes", "brown eyes", "green-hazel eyes", "gray-brown eyes")), + ("warm Uzbek Central Asian skin", ("long dark braid", "soft brunette waves", "black hair in a bun", "dark chestnut curls"), ("amber eyes", "brown eyes", "hazel eyes", "green-brown eyes")), + ("golden Kyrgyz Central Asian skin", ("thick brown ponytail", "long dark waves", "black braid", "soft brown curls"), ("brown eyes", "hazel eyes", "amber-brown eyes", "green eyes")), + ("warm Uighur Central Asian skin", ("long black waves", "dark chestnut hair", "soft brown curls", "black ponytail"), ("green-hazel eyes", "amber eyes", "brown eyes", "deep brown eyes")), + ("warm Tajik Central Asian skin", ("long dark-brown waves", "black hair in a braid", "soft chestnut curls", "sleek dark ponytail"), ("hazel eyes", "amber eyes", "brown eyes", "green-brown eyes")), + ("golden Turkmen Central Asian skin", ("dark braid with loose strands", "long brown waves", "soft black curls", "chestnut bun"), ("amber-brown eyes", "hazel eyes", "brown eyes", "green eyes")), + ("warm Armenian skin", ("voluminous dark curls", "long black waves", "dark auburn hair", "soft brunette lob"), ("deep brown eyes", "hazel eyes", "amber eyes", "green-brown eyes")), + ("warm Georgian Caucasus skin", ("dark chestnut waves", "long black curls", "soft brunette hair", "black ponytail"), ("hazel eyes", "green-brown eyes", "amber eyes", "deep brown eyes")), + # Indigenous / Pacific profiles + ("warm Indigenous Mexican skin", ("long straight black hair", "thick black braid", "soft dark waves", "black hair in a low bun"), ("dark brown eyes", "deep brown eyes", "brown eyes", "dark eyes")), + ("deep Indigenous Andean skin", ("thick black braid", "long black hair", "dark hair in twin braids", "soft black waves"), ("dark eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("warm Maya Indigenous skin", ("long black waves", "straight black hair", "dark braid", "black hair in a bun"), ("deep brown eyes", "dark brown eyes", "brown eyes", "dark eyes")), + ("warm Quechua Indigenous skin", ("thick black braid", "long straight black hair", "soft dark waves", "black ponytail"), ("dark brown eyes", "deep brown eyes", "amber-brown eyes", "dark eyes")), + ("warm Aymara Indigenous skin", ("long black braid", "straight black hair", "dark waves", "black hair in a low bun"), ("dark eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("warm Native American skin", ("long black hair", "thick dark braid", "soft black waves", "straight dark hair"), ("dark brown eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("warm First Nations Indigenous skin", ("long black waves", "dark braid", "straight black hair", "soft brown-black curls"), ("dark brown eyes", "brown eyes", "amber-brown eyes", "deep brown eyes")), + ("golden Pacific Islander skin", ("long dark waves", "thick black curls", "soft black hair", "dark hair with sunlit tips"), ("brown eyes", "dark brown eyes", "amber-brown eyes", "deep brown eyes")), + ("deep Polynesian Pacific Islander skin", ("thick black curls", "long black waves", "voluminous dark hair", "braided black hair"), ("dark brown eyes", "deep brown eyes", "brown eyes", "golden brown eyes")), + ("golden Samoan Pacific Islander skin", ("long black waves", "dark curls", "sleek black ponytail", "thick braided hair"), ("brown eyes", "dark brown eyes", "deep brown eyes", "amber eyes")), + ("warm Maori Pacific Islander skin", ("long dark waves", "black curls", "thick black braid", "soft dark ponytail"), ("dark brown eyes", "brown eyes", "deep brown eyes", "amber-brown eyes")), + ("golden Hawaiian Pacific Islander skin", ("long wavy black hair", "soft dark curls", "black hair with brown tips", "thick black ponytail"), ("brown eyes", "dark brown eyes", "amber-brown eyes", "deep brown eyes")), + # Black / African diaspora profiles + ("deep West African skin", ("short natural coils", "long box braids", "braided updo", "voluminous natural curls"), ("dark brown eyes", "golden brown eyes", "dark eyes", "amber-brown eyes")), + ("rich Ghanaian African skin", ("long box braids", "natural curls in a high puff", "black twists", "sleek straight black hair"), ("golden brown eyes", "dark brown eyes", "amber eyes", "dark eyes")), + ("deep Nigerian African skin", ("braided updo", "long locs", "short tapered curls", "voluminous black curls"), ("dark eyes", "deep brown eyes", "golden brown eyes", "amber-brown eyes")), + ("warm Ethiopian African skin", ("soft black curls", "long dark waves", "braided black hair", "curly bob"), ("amber eyes", "dark brown eyes", "golden brown eyes", "deep brown eyes")), + ("warm Eritrean African skin", ("long dark curls", "soft black waves", "braided ponytail", "short natural curls"), ("amber-brown eyes", "dark brown eyes", "golden brown eyes", "deep brown eyes")), + ("warm Somalian African skin", ("sleek dark ponytail", "long black waves", "soft curls", "braided black hair"), ("dark brown eyes", "deep brown eyes", "amber eyes", "golden brown eyes")), + ("rich Kenyan African skin", ("long locs", "short natural coils", "voluminous black curls", "braided crown"), ("deep brown eyes", "dark eyes", "golden brown eyes", "amber-brown eyes")), + ("deep Senegalese African skin", ("cornrow braids with beads", "long black twists", "natural curls", "sleek braided ponytail"), ("brown eyes", "dark brown eyes", "golden brown eyes", "deep brown eyes")), + ("deep Sudanese African skin", ("black curls in a puff", "long braided hair", "soft dark waves", "short coils"), ("dark brown eyes", "deep brown eyes", "amber-brown eyes", "golden brown eyes")), + ("rich South African African skin", ("voluminous natural curls", "long twists", "short tapered curls", "sleek black bob"), ("dark brown eyes", "golden brown eyes", "amber eyes", "deep brown eyes")), + ("deep African-American brown skin", ("short tapered curls", "long box braids", "natural afro", "sleek straight black hair"), ("dark eyes", "dark brown eyes", "golden brown eyes", "amber-brown eyes")), + ("rich African-Caribbean brown skin", ("voluminous natural curls", "long locs", "black curls with copper tips", "braided updo"), ("honey-brown eyes", "dark brown eyes", "golden brown eyes", "deep brown eyes")), + ("deep Haitian African-Caribbean skin", ("long locs", "natural curls", "braided black hair", "short coils"), ("dark brown eyes", "deep brown eyes", "golden brown eyes", "amber eyes")), + ("warm Jamaican African-Caribbean skin", ("black curls with honey tips", "long braids", "voluminous natural hair", "sleek ponytail"), ("golden brown eyes", "dark brown eyes", "honey-brown eyes", "deep brown eyes")), + ("deep Afro-Brazilian African skin", ("long dark curls", "braided black hair", "voluminous natural curls", "copper-highlighted locs"), ("dark brown eyes", "golden brown eyes", "amber-brown eyes", "deep brown eyes")), + ("warm Cape Verdean African skin", ("soft dark curls", "long brown-black waves", "braided hair with caramel tips", "sleek curly bob"), ("hazel-brown eyes", "golden brown eyes", "dark brown eyes", "amber eyes")), + # Mixed heritage profiles + ("fair East Asian-European mixed skin", ("long ash-brown waves", "soft black bob", "dark-blonde ponytail", "black hair with honey highlights"), ("gray-brown eyes", "blue-gray eyes", "hazel eyes", "soft brown eyes")), + ("light Korean East Asian-European mixed skin", ("soft black bob", "long dark-blonde waves", "sleek brown ponytail", "black hair with curtain bangs"), ("blue-gray eyes", "dark almond eyes", "green eyes", "soft brown eyes")), + ("warm Japanese East Asian-Latina mixed skin", ("dark wavy lob", "long black waves", "brown hair with caramel tips", "soft black curls"), ("hazel eyes", "brown eyes", "soft brown eyes", "amber eyes")), + ("golden Chinese East Asian-Latina mixed skin", ("long black ponytail", "dark curls", "black hair with caramel streaks", "soft brunette bob"), ("light brown eyes", "dark almond eyes", "hazel-brown eyes", "brown eyes")), + ("warm East Asian-Mediterranean mixed skin", ("long espresso waves", "glossy black hair", "soft brown curls", "sleek black lob"), ("green-brown eyes", "dark brown eyes", "hazel eyes", "soft brown eyes")), + ("light East Asian-Middle-Eastern mixed skin", ("glossy black waves", "dark chestnut curls", "sleek black ponytail", "soft black bob"), ("amber eyes", "dark almond eyes", "hazel-brown eyes", "deep brown eyes")), + ("warm Vietnamese Southeast Asian-European mixed skin", ("black hair with honey highlights", "soft dark-blonde waves", "straight black hair", "brown curls"), ("hazel eyes", "dark brown eyes", "green eyes", "soft brown eyes")), + ("golden Filipina Southeast Asian-European mixed skin", ("soft brown curls", "long black waves", "caramel-highlighted ponytail", "brunette bob"), ("green eyes", "brown eyes", "hazel eyes", "dark brown eyes")), + ("warm Southeast Asian-Latina mixed skin", ("long dark curls", "black waves with caramel tips", "soft brown hair", "sleek black ponytail"), ("brown eyes", "hazel eyes", "amber eyes", "deep brown eyes")), + ("golden Thai Southeast Asian-Middle-Eastern mixed skin", ("sleek black high ponytail", "long dark waves", "soft black curls", "dark chestnut hair"), ("deep brown eyes", "amber eyes", "hazel-brown eyes", "soft brown eyes")), + ("warm South Asian-European mixed skin", ("dark chestnut waves", "long black hair", "soft brown curls", "dark-blonde lob"), ("hazel-green eyes", "deep brown eyes", "amber eyes", "green eyes")), + ("golden South Asian-Latina mixed skin", ("long black curls", "dark waves with caramel tips", "soft chestnut hair", "black ponytail"), ("honey-brown eyes", "deep brown eyes", "hazel eyes", "amber eyes")), + ("warm South Asian-Middle-Eastern mixed skin", ("thick dark waves", "long black hair", "soft brunette curls", "dark braid"), ("amber-brown eyes", "deep brown eyes", "hazel eyes", "dark brown eyes")), + ("deep South Asian-Southeast Asian mixed skin", ("black hair in a braid", "long glossy black hair", "soft dark curls", "sleek black bob"), ("dark brown eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("warm South Asian-East Asian mixed skin", ("long glossy black hair", "soft black bob", "dark waves", "straight black ponytail"), ("soft brown eyes", "deep brown eyes", "dark almond eyes", "brown eyes")), + ("golden Central Asian-European mixed skin", ("dark blonde curls", "long brown waves", "soft black hair", "chestnut ponytail"), ("green-hazel eyes", "hazel eyes", "gray-brown eyes", "amber eyes")), + ("warm Central Asian-Middle-Eastern mixed skin", ("long brown waves", "dark braid", "soft black curls", "chestnut lob"), ("amber eyes", "hazel eyes", "deep brown eyes", "green-brown eyes")), + ("warm Indigenous Latina-European mixed skin", ("long dark waves", "dark-blonde curls", "black braid", "soft chestnut hair"), ("hazel eyes", "brown eyes", "green-brown eyes", "dark brown eyes")), + ("deep Indigenous Latina skin", ("thick black curls", "long straight black hair", "dark braid", "soft black waves"), ("dark brown eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("golden Pacific Islander-Asian mixed skin", ("long black waves", "thick dark curls", "soft black hair", "straight black ponytail"), ("brown eyes", "dark brown eyes", "deep brown eyes", "soft brown eyes")), + ("warm Pacific Islander-European mixed skin", ("soft dark-brown waves", "long black curls", "dark-blonde beach waves", "black hair with brown tips"), ("green-brown eyes", "brown eyes", "hazel eyes", "deep brown eyes")), + ("warm Middle-Eastern-Latina mixed skin", ("voluminous dark curls", "long black waves", "caramel-highlighted hair", "soft chestnut bob"), ("amber eyes", "hazel eyes", "brown eyes", "green-brown eyes")), + ("golden Mediterranean-Latina mixed skin", ("long caramel curls", "dark wavy lob", "soft chestnut waves", "black ponytail"), ("light brown eyes", "hazel eyes", "green-hazel eyes", "amber eyes")), + ("warm Turkish-European mixed skin", ("dark wavy ponytail", "soft brunette curls", "long chestnut hair", "black bob"), ("hazel eyes", "green eyes", "amber-brown eyes", "deep brown eyes")), + ("warm Persian-South Asian mixed skin", ("glossy black waves", "thick dark curls", "long black braid", "soft chestnut hair"), ("deep brown eyes", "amber eyes", "hazel-brown eyes", "dark brown eyes")), + ("deep African-European mixed skin", ("brown curls with blonde tips", "long box braids", "soft chestnut curls", "voluminous natural hair"), ("hazel eyes", "golden brown eyes", "green-brown eyes", "dark brown eyes")), + ("warm African-Latina mixed skin", ("long dark curls", "black curls with caramel tips", "braided ponytail", "soft chestnut waves"), ("golden brown eyes", "hazel eyes", "dark brown eyes", "amber eyes")), + ("deep African-East Asian mixed skin", ("sleek black bob", "long black braids", "soft black curls", "glossy black ponytail"), ("dark almond eyes", "deep brown eyes", "soft brown eyes", "dark eyes")), + ("warm African-South Asian mixed skin", ("long braided black hair", "thick dark curls", "soft black waves", "black hair in a bun"), ("deep brown eyes", "golden brown eyes", "dark brown eyes", "amber-brown eyes")), + ("rich African-Pacific Islander mixed skin", ("voluminous black curls", "long locs", "thick black waves", "braided black hair"), ("dark brown eyes", "deep brown eyes", "golden brown eyes", "amber eyes")), + ("warm African-Middle-Eastern mixed skin", ("soft dark curls", "long black waves", "braided updo", "dark chestnut curls"), ("amber-brown eyes", "deep brown eyes", "hazel eyes", "golden brown eyes")), + ("deep African-Caribbean Latina mixed skin", ("long locs with copper tips", "dark curls", "voluminous natural hair", "braided black ponytail"), ("honey-brown eyes", "dark brown eyes", "golden brown eyes", "amber eyes")), + ("warm African-Native American mixed skin", ("thick black waves", "long braids", "soft dark curls", "straight black hair"), ("dark brown eyes", "deep brown eyes", "amber-brown eyes", "brown eyes")), + ("golden African-Southeast Asian mixed skin", ("black curls in a high puff", "long black waves", "sleek black bob", "braided black hair"), ("brown eyes", "deep brown eyes", "soft brown eyes", "dark brown eyes")), + ("warm African-Central Asian mixed skin", ("dark brown waves", "soft black curls", "long braided hair", "chestnut ponytail"), ("hazel-brown eyes", "golden brown eyes", "amber eyes", "dark brown eyes")), + ("warm African-Mediterranean mixed skin", ("long dark curls", "soft brunette waves", "braided black hair", "black bob with caramel tips"), ("hazel eyes", "golden brown eyes", "green-brown eyes", "deep brown eyes")), + ("deep African-Indigenous Latina mixed skin", ("thick black curls", "long dark braids", "voluminous natural hair", "soft black waves"), ("dark brown eyes", "golden brown eyes", "amber-brown eyes", "deep brown eyes")), + ("warm African-Arab mixed skin", ("soft black curls", "long dark waves", "braided updo", "sleek black ponytail"), ("amber eyes", "deep brown eyes", "hazel-brown eyes", "dark brown eyes")), + ("golden East Asian-Pacific Islander mixed skin", ("long black waves", "soft dark curls", "straight black hair", "black ponytail"), ("soft brown eyes", "dark almond eyes", "brown eyes", "deep brown eyes")), + ("warm East Asian-Indigenous Latina mixed skin", ("long black hair", "dark waves with caramel tips", "straight black bob", "soft brunette curls"), ("dark brown eyes", "soft brown eyes", "hazel-brown eyes", "deep brown eyes")), + ("warm East Asian-South Asian-Middle-Eastern mixed skin", ("glossy black waves", "long dark curls", "soft black bob", "thick black braid"), ("deep brown eyes", "amber eyes", "soft brown eyes", "hazel-brown eyes")), + ("golden Southeast Asian-South Asian mixed skin", ("long glossy black hair", "dark braid", "soft black waves", "black hair in a high bun"), ("dark brown eyes", "deep brown eyes", "brown eyes", "amber-brown eyes")), + ("warm Latina-South Asian-European mixed skin", ("dark chestnut waves", "long black curls", "caramel-highlighted hair", "soft brunette bob"), ("hazel eyes", "amber eyes", "brown eyes", "green-brown eyes")), + ("warm Latina-Middle-Eastern-European mixed skin", ("voluminous dark curls", "long chestnut hair", "soft black waves", "caramel ponytail"), ("amber eyes", "hazel eyes", "brown eyes", "green eyes")), + ("golden Indigenous-Pacific Islander mixed skin", ("long dark waves", "thick black curls", "dark braid", "soft black hair"), ("dark brown eyes", "brown eyes", "deep brown eyes", "amber eyes")), + ("warm European-Middle-Eastern mixed skin", ("long chestnut waves", "dark curls", "black ponytail", "soft auburn hair"), ("green-brown eyes", "hazel eyes", "amber eyes", "blue-gray eyes")), + ("fair European-Latina mixed skin", ("long caramel waves", "dark-blonde curls", "soft brunette bob", "copper ponytail"), ("hazel eyes", "green eyes", "brown eyes", "blue-gray eyes")), + ("warm European-South Asian-Latina mixed skin", ("long dark waves", "caramel-highlighted curls", "soft chestnut hair", "black braid"), ("hazel-green eyes", "amber eyes", "brown eyes", "deep brown eyes")), + ("deep African-East Asian-Latina mixed skin", ("long black curls", "sleek black bob", "braided dark hair", "soft waves with caramel tips"), ("dark brown eyes", "soft brown eyes", "golden brown eyes", "hazel eyes")), + ("warm African-European-East Asian mixed skin", ("soft brown curls", "black bob with bangs", "long braids", "dark-blonde waves"), ("hazel eyes", "soft brown eyes", "green-brown eyes", "dark brown eyes")), + ("golden African-South Asian-Latina mixed skin", ("long dark curls", "thick black braid", "voluminous black hair", "caramel-highlighted waves"), ("golden brown eyes", "deep brown eyes", "amber eyes", "hazel eyes")), + ("warm Indigenous-East Asian-European mixed skin", ("long black waves", "soft chestnut hair", "straight black bob", "dark-blonde ponytail"), ("hazel-brown eyes", "soft brown eyes", "green eyes", "dark brown eyes")), + ] + ) +) + +MATURE_WOMEN = [ + # White / European + ("woman", "late 20s adult", "slim", "warm tanned skin", "red hair in soft waves", "green eyes"), + ("woman", "late 20s adult", "curvy", "pale skin", "platinum-blonde lob", "gray eyes"), + ("woman", "40s adult", "slim", "fair skin", "auburn waves", "green eyes"), + ("woman", "50s adult", "average", "warm beige skin", "brunette bob", "brown eyes"), + # Mediterranean / Latina + ("woman", "30s adult", "toned", "olive Mediterranean skin", "dark ponytail", "amber eyes"), + ("woman", "30s adult", "busty", "sun-kissed Latina skin", "long chestnut waves", "dark brown eyes"), + ("woman", "late 20s adult", "hourglass", "olive skin", "long dark waves", "amber eyes"), + # East Asian + ("woman", "late 20s adult", "slim", "fair East Asian skin", "long straight black hair", "dark almond eyes"), + ("woman", "40s adult", "hourglass", "warm East Asian skin", "sleek black bob with bangs", "dark almond eyes"), + # Southeast Asian + ("woman", "30s adult", "curvy", "warm Southeast Asian tan skin", "black hair in a sleek bob", "dark brown eyes"), + # South Asian + ("woman", "30s adult", "plus-size", "deep South Asian brown skin", "long glossy black hair", "dark brown eyes"), + ("woman", "40s adult", "plus-size", "warm South Asian brown skin", "dark bun with grays", "brown eyes"), + # Black / African + ("woman", "40s adult", "curvy", "deep brown African skin", "black shoulder-length curls", "dark brown eyes"), + ("woman", "50s adult", "curvy", "rich dark African skin", "silver locs", "golden brown eyes"), + # Extended (balanced additions) + ("woman", "30s adult", "athletic", "fair skin", "dark pixie cut", "blue eyes"), + ("woman", "late 20s adult", "curvy", "light East Asian skin", "long straight black hair", "dark almond eyes"), + ("woman", "40s adult", "average", "warm caramel Latina skin", "shoulder-length brown waves", "hazel eyes"), + # Extended round 2 + ("woman", "late 20s adult", "athletic", "warm East Asian skin", "long black hair in soft waves", "dark almond eyes"), + ("woman", "30s adult", "hourglass", "fair skin", "long auburn waves", "green eyes"), + ("woman", "30s adult", "curvy", "sun-kissed Latina skin", "dark balayage waves", "brown eyes"), + ("woman", "40s adult", "slim", "light East Asian skin", "sleek black bob", "dark brown eyes"), + ("woman", "40s adult", "curvy", "olive Mediterranean skin", "shoulder-length dark waves", "hazel eyes"), + ("woman", "late 20s adult", "busty", "deep brown African skin", "natural curls", "dark brown eyes"), + ("woman", "30s adult", "athletic", "warm Southeast Asian tan skin", "long black ponytail", "dark brown eyes"), + ("woman", "50s adult", "average", "fair skin", "silver-blonde bob", "blue eyes"), + ("woman", "30s adult", "hourglass", "warm South Asian brown skin", "long glossy black waves", "deep brown eyes"), + ("woman", "40s adult", "curvy", "warm caramel Latina skin", "long dark waves", "amber eyes"), + ("woman", "late 20s adult", "slim", "pale skin", "platinum pixie cut", "gray eyes"), + ("woman", "50s adult", "curvy", "warm beige skin", "auburn shoulder-length waves", "green eyes"), + ("woman", "30s adult", "athletic", "bronze skin", "dark high ponytail", "brown eyes"), +] + +_MATURE_EXPANSION_AGES = ( + "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", +) + +_MATURE_EXPANSION_BODIES = ( + "slim", + "toned", + "athletic", + "average", + "curvy", + "soft curvy", + "curvy athletic", + "hourglass", + "slim busty", + "busty", + "busty curvy", + "plus-size", +) + + +def _mature_hair_variant(hair: str, age: str, index: int) -> str: + if "50s" in age: + templates = ( + "{hair} with graceful silver streaks", + "silver-streaked {hair}", + "{hair} with polished silver highlights", + "{hair} with elegant silver strands", + ) + elif "40s" in age: + templates = ( + "{hair}", + "{hair} with subtle silver strands", + "polished {hair}", + "{hair} with face-framing layers", + ) + else: + templates = ( + "{hair}", + "polished {hair}", + "{hair} with face-framing layers", + "{hair} styled in a sophisticated shape", + ) + return templates[index % len(templates)].format(hair=hair) + + +def _expand_mature_demographics_from_young(source_pool: list[tuple[str, str, str, str, str, str]]) -> list[tuple[str, str, str, str, str, str]]: + """Project the broad young-adult demographic coverage into mature ages. + + Mature rows reuse the same skin/heritage coverage but rotate mature age bands, + body descriptors, and age-appropriate hair styling. + """ + seen = set(MATURE_WOMEN) + expanded: list[tuple[str, str, str, str, str, str]] = [] + for index, (_, _, _, skin, hair, eyes) in enumerate(source_pool): + age = _MATURE_EXPANSION_AGES[(index * 3) % len(_MATURE_EXPANSION_AGES)] + body = _MATURE_EXPANSION_BODIES[(index * 5) % len(_MATURE_EXPANSION_BODIES)] + entry = ( + "woman", + age, + body, + skin, + _mature_hair_variant(hair, age, index), + eyes, + ) + if entry not in seen: + seen.add(entry) + expanded.append(entry) + return expanded + + +MATURE_WOMEN.extend(_expand_mature_demographics_from_young(YOUNG_WOMEN)) + +MEN = [ + ("man", "late 20s adult", "slim", "warm tan skin", "curly black hair", "brown eyes"), + ("man", "late 20s adult", "athletic", "golden skin", "tousled dark-brown hair", "green eyes"), + ("man", "late 20s adult", "lean athletic", "warm Southeast Asian tan skin", "tousled black hair", "dark brown eyes"), + ("man", "30s adult", "average", "fair skin", "short dark hair", "blue eyes"), + ("man", "30s adult", "athletic", "light East Asian skin", "short straight black hair", "dark almond eyes"), + ("man", "30s adult", "muscular", "olive skin", "dark wavy hair", "hazel eyes"), + ("man", "30s adult", "stocky", "olive skin", "dark wavy hair", "hazel eyes"), + ("man", "30s adult", "fat", "medium brown skin", "shaved head", "dark eyes"), + ("man", "30s adult", "lean athletic", "tanned skin", "slicked-back black hair", "dark brown eyes"), + ("man", "40s adult", "dad bod", "warm skin tone", "shaved head", "kind brown eyes"), + ("man", "40s adult", "broad", "deep brown skin", "trim beard", "amber eyes"), + ("man", "40s adult", "average", "fair skin", "salt-and-pepper hair", "green eyes"), + ("man", "40s adult", "fat", "warm beige skin", "curly dark hair", "brown eyes"), + ("man", "40s adult", "muscular", "bronze skin", "short cropped hair with stubble", "steel-gray eyes"), + ("man", "50s adult", "stocky", "light olive skin", "gray hair", "kind eyes"), + ("man", "50s adult", "broad", "tanned skin", "gray beard", "blue-gray eyes"), + ("man", "50s adult", "average", "deep brown skin", "short silver hair", "dark eyes"), + ("man", "60s adult", "average", "fair skin", "silver hair and moustache", "blue eyes"), + ("man", "60s adult", "fat", "warm brown skin", "bald head", "soft brown eyes"), + ("man", "60s adult", "slim", "olive skin", "long gray hair tied back", "green eyes"), + ("man", "30s adult", "broad", "deep brown African skin", "short afro", "dark brown eyes"), + ("man", "40s adult", "average", "warm South Asian brown skin", "salt-and-pepper hair", "dark eyes"), + # Extended round 2 + ("man", "late 20s adult", "athletic", "warm olive skin", "dark tousled hair", "brown eyes"), + ("man", "30s adult", "muscular", "deep brown African skin", "short fade", "dark brown eyes"), + ("man", "30s adult", "lean athletic", "light East Asian skin", "tousled black hair", "dark brown eyes"), + ("man", "40s adult", "broad", "warm tan skin", "salt-and-pepper stubble", "hazel eyes"), + ("man", "late 20s adult", "slim", "fair skin", "wavy auburn hair", "green eyes"), + ("man", "30s adult", "muscular", "bronze Latino skin", "dark slicked-back hair", "brown eyes"), + ("man", "50s adult", "fit", "olive skin", "gray-flecked beard", "blue-gray eyes"), + ("man", "40s adult", "stocky", "warm South Asian brown skin", "short dark hair", "dark eyes"), + ("man", "30s adult", "athletic", "tanned skin", "dark hair in a man bun", "hazel eyes"), + ("man", "late 20s adult", "lean", "deep brown skin", "short twists", "dark brown eyes"), +] + +_MEN_EXPANSION_AGES = ( + "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", +) + +_MEN_EXPANSION_BODIES = ( + "lean", + "slim", + "lanky", + "average", + "fit", + "athletic", + "lean athletic", + "muscular", + "broad", + "stocky", + "burly", + "dad bod", + "heavyset", + "soft-bodied", + "fat", +) + + +def _men_hair_variant(skin: str, age: str, index: int) -> str: + skin_lower = skin.lower() + if "african" in skin_lower: + younger = ("short fade", "close-cropped curls", "short afro", "neat twists", "locs tied back", "shaved head with trimmed beard") + older = ("silver-flecked short afro", "gray beard with shaved head", "salt-and-pepper locs tied back", "close-cropped gray curls", "bald head with neat gray beard") + elif any(k in skin_lower for k in ("east asian", "southeast asian", "south asian", "central asian", "japanese", "korean", "chinese", "thai", "filipina", "indian", "pakistani", "kazakh", "uighur")): + younger = ("short straight black hair", "neat black undercut", "tousled black hair", "black hair tied back", "shaved head with stubble", "dark side-parted hair") + older = ("salt-and-pepper black hair", "silver-streaked black hair", "short gray-black hair", "balding black hair with stubble", "neatly combed silver hair") + elif any(k in skin_lower for k in ("european", "fair", "pale", "ivory", "nordic", "celtic", "slavic", "baltic", "irish", "french")): + younger = ("short dark-blonde hair", "wavy brown hair", "tousled chestnut hair", "close-cropped auburn hair", "shaved head with light stubble", "slicked-back dark hair") + older = ("silver hair", "salt-and-pepper hair", "gray-flecked beard", "short white hair", "bald head with silver stubble", "silver ponytail") + else: + younger = ("short dark hair", "dark wavy hair", "slicked-back black hair", "trim beard", "shaved head with stubble", "dark hair tied back") + older = ("salt-and-pepper hair", "gray beard", "silver-streaked dark hair", "bald head with trimmed beard", "short silver hair", "long gray hair tied back") + pool = older if any(decade in age for decade in ("60s", "70s", "80s")) else younger + return pool[index % len(pool)] + + +def _expand_men_demographics_from_young(source_pool: list[tuple[str, str, str, str, str, str]]) -> list[tuple[str, str, str, str, str, str]]: + seen = set(MEN) + expanded: list[tuple[str, str, str, str, str, str]] = [] + for index, (_, _, _, skin, _, eyes) in enumerate(source_pool): + entry = ( + "man", + _MEN_EXPANSION_AGES[(index * 5) % len(_MEN_EXPANSION_AGES)], + _MEN_EXPANSION_BODIES[(index * 7) % len(_MEN_EXPANSION_BODIES)], + skin, + _men_hair_variant(skin, _MEN_EXPANSION_AGES[(index * 5) % len(_MEN_EXPANSION_AGES)], index), + eyes, + ) + if entry not in seen: + seen.add(entry) + expanded.append(entry) + return expanded + + +MEN.extend(_expand_men_demographics_from_young(YOUNG_WOMEN)) + +# Skewed toward revealing, sexy pin-up styling while staying non-explicit +# (lingerie, swimwear, crop tops, garters, cleavage, etc.). +WOMEN_CLOTHES = [ + "lacy garter belt with sheer stockings and a fitted satin bodice", + "cropped halter top with deep cleavage and high-cut denim shorts", + "plunging-neckline bodysuit with a sheer wrap skirt", + "off-shoulder corset top with a short pleated skirt", + "string bikini top with a sarong tied low on the hips", + "sheer babydoll slip over a matching lingerie set", + "deep-V wrap dress with a thigh-high slit", + "lace bralette with high-waisted garter shorts", + "cropped mesh long-sleeve over a bandeau and mini skirt", + "satin slip dress with thin straps and a low open back", + "oversized unbuttoned shirt over a lacy lingerie set", + "leather bustier with a buckled mini skirt", + "knit crop sweater slipping off one shoulder over high-cut briefs", + "halterneck monokini with cut-out sides", + "bodycon mini dress with a plunging neckline", + "tied gingham crop top with denim cut-off shorts", + "sheer mesh cover-up over a triangle bikini", + "corset-laced top with a flowing maxi skirt and a high slit", + "velvet bralette with matching tap shorts and a garter", + "fishnet stockings with a wet-look bodysuit", + "micro cardigan buttoned once over a lace bra and mini skirt", + "cropped tube top with low-rise leather pants", + "silk camisole with delicate lace trim and boy shorts", + "open-back halter dress with a daring neckline", + "athletic sports bra with cropped leggings and a bare midriff", + "feathered burlesque corset with opera gloves and stockings", + "high-cut one-piece swimsuit with a plunging front", + "draped silk wrap revealing one shoulder and a long leg", + "tied flannel over a bikini top with denim cut-offs", + "lace teddy under a sheer robe slipping open", + "metallic bandeau with high-waisted festival shorts", + "ribbed crop tank with cleavage and tiny yoga shorts", + "fitted off-shoulder satin gown with a thigh slit and elegant earrings", + "sleek backless cocktail dress with a plunging neckline", + "plunging halter jumpsuit with a backless cut", + "sheer lace blouse over a satin bralette with a pencil skirt", + "cut-out evening gown with a high slit and an open back", + "bodycon bandage dress with cut-out sides", + "wrap blouse knotted to bare the midriff with tailored shorts", + "sequined slip dress with a plunging cowl neck", + "off-shoulder knit dress clinging to the figure", + "lace-panel cocktail dress with a sheer midriff", + "satin corset top with wide-leg trousers", + "halter mini dress with an open lace-up back", + "deep-V wrap blouse with a leather mini skirt", + "fitted turtleneck bodysuit with high-cut briefs and tights", + "mesh-panel athletic set with a bare midriff", + "draped one-shoulder gown with a thigh slit", + "cropped blazer worn with little underneath and tailored shorts", + "silk pajama shirt left mostly unbuttoned with shorts", + "ribbed knit two-piece with a crop top and a mini skirt", + "bodycon midi dress with a daring back cut-out", + "satin slip with a feather-trimmed robe", + "lace bodysuit under high-waisted wide trousers", + "halterneck catsuit with a plunging zipper", + "sheer maxi skirt over bikini bottoms with a crop top", + "corset-back denim mini with a bandeau", + "off-shoulder peplum top with a thigh-high slit skirt", + "metallic mini dress with cut-out waist panels", + "wrap-front maxi with a slit to the hip and a low neckline", +] + + +def _extend_unique(target: list[str], additions: list[str]) -> None: + seen = set(target) + for item in additions: + if item not in seen: + seen.add(item) + target.append(item) + + +def _expand_women_full_clothes() -> list[str]: + additions: list[str] = [] + + lingerie_tops = ( + "embroidered lace balconette bra", + "satin longline bra", + "strappy lace bralette", + "velvet demi-cup bra", + "mesh-panel bustier", + "silk corset bustier", + "scalloped lace bodysuit", + "lace-trim teddy", + "underwire bralette", + "sheer lace blouse over a satin bra", + "ribbon-laced corset top", + "satin plunge bodysuit", + ) + lingerie_bottoms = ( + "high-waisted garter shorts", + "lace tap shorts", + "sheer stockings and a garter belt", + "high-cut briefs", + "satin tap shorts", + "lace pencil skirt", + "low-slung silk shorts", + "thigh-high stockings", + "ruffled mini skirt", + "high-waisted satin shorts", + ) + for top in lingerie_tops: + for bottom in lingerie_bottoms: + additions.append(f"{top} with {bottom}") + + swim_tops = ( + "triangle bikini top", + "bandeau bikini top", + "underwire bikini top", + "halter bikini top", + "crochet bikini top", + "metallic bikini top", + "wrap-front bikini top", + "sporty zip-front bikini top", + "plunging halterneck swimsuit", + "high-cut one-piece swimsuit", + "cut-out monokini", + "ruched balconette swim top", + ) + swim_bottoms = ( + "high-cut swim bottoms", + "tie-side bikini bottoms", + "a low-tied sarong", + "a semi-sheer beach skirt", + "high-waisted swim briefs", + "a draped resort wrap", + "tiny board shorts", + "a sheer mesh cover-up skirt", + "a knotted linen wrap", + "a beaded hip-chain detail", + ) + for top in swim_tops: + for bottom in swim_bottoms: + additions.append(f"{top} with {bottom}") + + crop_tops = ( + "ribbed crop tank", + "deep-V cropped blouse", + "tie-front gingham crop top", + "off-shoulder knit crop top", + "cropped mesh long-sleeve", + "cropped satin camisole", + "micro cardigan buttoned once", + "backless halter crop top", + "one-shoulder crop top", + "lace-up corset crop top", + "cropped blazer with a bralette underneath", + "plunging wrap blouse knotted at the waist", + ) + crop_bottoms = ( + "high-cut denim shorts", + "low-rise leather pants", + "a pleated mini skirt", + "a thigh-slit pencil skirt", + "tailored short shorts", + "a corset-back denim mini skirt", + "a satin wrap skirt", + "tiny yoga shorts", + "high-waisted festival shorts", + "a leather mini skirt", + "a flowing maxi skirt slit high on one leg", + "wide-leg trousers worn low at the waist", + ) + for top in crop_tops: + for bottom in crop_bottoms: + additions.append(f"{top} with {bottom}") + + dresses = ( + "deep-V wrap dress", + "sleek backless cocktail dress", + "bodycon mini dress", + "satin slip dress", + "halter mini dress", + "off-shoulder knit dress", + "sequined slip dress", + "draped one-shoulder gown", + "cut-out evening gown", + "metallic mini dress", + "lace-panel cocktail dress", + "open-back halter dress", + "plunging cowl-neck slip dress", + "corset-waist party dress", + "low-back velvet midi dress", + "wrap-front maxi dress", + ) + dress_details = ( + "with a thigh-high slit", + "with a low open back", + "with a plunging neckline", + "with sheer lace side panels", + "with a lace-up back", + "with cut-out waist panels", + "with thin straps and a soft drape", + "with a high slit and elegant earrings", + ) + for dress in dresses: + for detail in dress_details: + additions.append(f"{dress} {detail}") + + boudoir_bases = ( + "lace teddy", + "satin slip chemise", + "silk camisole and tap shorts", + "lace bodysuit", + "satin bustier and stockings", + "feathered burlesque corset", + "velvet bralette and tap shorts", + "silk pajama shirt left mostly unbuttoned", + "lace bra set with high-waisted briefs", + "plunging satin robe dress", + ) + boudoir_layers = ( + "under a sheer robe slipping open", + "under an oversized unbuttoned shirt", + "with opera gloves and stockings", + "with a feather-trimmed robe", + "with a draped silk wrap", + "with a semi-sheer kimono robe", + "with lace-top thigh-highs", + "with a cropped cashmere cardigan falling off one shoulder", + ) + for base in boudoir_bases: + for layer in boudoir_layers: + additions.append(f"{base} {layer}") + + editorial_tops = ( + "tailored waistcoat worn over a lace bra", + "cropped tuxedo jacket with a satin bralette", + "open silk blouse over a lace camisole", + "structured satin corset top", + "strapless bustier top", + "sheer organza blouse over a bandeau", + "leather bustier top", + "plunging halter jumpsuit", + "rhinestone bralette under a cropped jacket", + "corset-laced peplum top", + ) + editorial_bottoms = ( + "tailored trousers", + "a high-slit pencil skirt", + "wide-leg satin trousers", + "a leather mini skirt", + "a floor-length slit skirt", + "high-waisted shorts", + "a fitted midi skirt", + "low-slung tuxedo pants", + ) + for top in editorial_tops: + for bottom in editorial_bottoms: + additions.append(f"{top} with {bottom}") + + themed_outfits = ( + "retro sailor-inspired halter bodysuit with high-waisted shorts", + "pin-up polka-dot crop top with tied shorts and wedge heels", + "vintage bathing suit with a belted waist and plunging front", + "cabaret corset with fishnet stockings and satin gloves", + "resort linen shirt knotted over a bikini with a low sarong", + "glitter festival bralette with a sheer maxi skirt", + "rhinestone cowgirl crop top with denim cut-offs", + "lace-up moto bustier with glossy leggings", + "tennis-inspired pleated mini skirt with a cropped polo unbuttoned at the collar", + "ballet-wrap cardigan over a lace bralette with a chiffon skirt", + "velvet lounge set with a cropped cami and tap shorts", + "yacht-club bikini under an open linen blazer", + "spa robe loosely belted over a satin slip", + "tropical pareo dress tied low over a bikini", + "noir satin gown with a deep neckline and long gloves", + "art-deco beaded mini dress with a plunging back", + "latex-look bodysuit with sheer stockings and ankle boots", + "soft knit two-piece with a cropped cardigan and mini skirt", + "silk scarf top with a slit skirt", + "crystal-trim bralette with a high-waisted pencil skirt", + "open-back romper with a plunging front", + "lace-up halter catsuit with a deep zipper front", + "cropped racing jacket over a bandeau with leather shorts", + "beach cabana wrap dress over a triangle bikini", + "glamorous champagne-colored slip with a low cowl back", + "sheer lace maxi dress over a matching bodysuit", + "corseted denim romper with a low neckline", + "off-shoulder satin mini dress with garter-style straps", + "silk bandeau with palazzo pants slit at both sides", + "embroidered mesh bodysuit with a satin overskirt", + ) + additions.extend(themed_outfits) + return additions + + +_extend_unique(WOMEN_CLOTHES, _expand_women_full_clothes()) + +MEN_CLOTHES = [ + "open tropical shirt over a bare chest with board shorts", + "unbuttoned linen shirt revealing a toned chest with rolled trousers", + "fitted tank top with low-slung joggers", + "swim shorts with a towel over one shoulder, shirtless", + "leather jacket over a bare chest with dark jeans", + "rolled-sleeve button shirt left open over a fitted vest", + "snug henley with sleeves pushed up and tailored trousers", + "sleeveless gym shirt with shorts and a gym towel", + "fitted chef jacket unbuttoned at the collar with an apron", + "soft resort shirt open over a tank top with swim shorts", + "black shirt with rolled sleeves and relaxed trousers", + "denim jacket over a fitted tee with worn jeans", + "light blazer over an open-collar shirt with chinos", + "barber-style suspenders over an open shirt with bare forearms", + "camp flannel open over a fitted tee with rugged pants", + "wetsuit peeled to the waist on a beach", + "tailored waistcoat over a bare chest with dress trousers", + "open silk shirt with a thin chain over a toned chest", + "fitted polo with the sleeves pushed up and chinos", + "henley unbuttoned at the collar with rugged jeans", + "linen suit with the shirt open at the chest", + "leather vest over bare arms with dark trousers", + "rolled-cuff dress shirt half-unbuttoned with suspenders", + "fitted turtleneck under a tailored blazer", + "open flannel over a fitted tank with worn jeans", + "snug crew tee tucked into belted trousers", + "mechanic's coveralls peeled to the waist over a tank", + "tuxedo shirt unbuttoned with the bow tie hanging loose", +] + +SCENES = [ + ("office", "cozy office desk with warm lamp light and papers"), + ("balcony", "brick city balcony at golden sunrise"), + ("garden_rain", "lush garden during gentle summer rain"), + ("kitchen", "soft-focus kitchen with flowers and bright window light"), + ("bedroom", "minimal cozy bedroom with muted bedding and warm decor"), + ("studio", "simple studio backdrop with visible paper texture"), + ("cafe", "small cafe table by a softly lit street window"), + ("poolside", "poolside patio with sunset plants and lounge chairs"), + ("flower_market", "pastel flower market with warm stalls"), + ("garage", "clean garage doorway with soft industrial lighting"), + ("vineyard", "vineyard at dusk with a glass of wine"), + ("bookshop", "cozy bookshop shelves and warm counter light"), + ("nightclub", "soft nightclub lights and blurred dance floor"), + ("laundromat", "clean laundromat with folded towels"), + ("dance_studio", "dance studio with mirror glow and wood floor"), + ("beach_bar", "beach bar at golden sunset"), + ("sailboat", "sailboat deck with ocean sunset"), + ("library", "quiet library aisle with warm lamplight"), + ("picnic", "summer picnic blanket in a soft park setting"), + ("train_station", "vintage train station with evening lights"), + ("hotel_lobby", "hotel lobby with brass lights and polished stone"), + ("rooftop", "rooftop party with city lights and warm string lights"), + ("tailor_shop", "tailor shop with fabric swatches and warm mirrors"), + ("market", "outdoor produce market with pastel stalls"), + ("artist_studio", "art studio with canvases and soft afternoon light"), + ("greenhouse", "greenhouse with leaves and diffused glass light"), + ("record_shop", "record shop aisle with warm neon accents"), + ("bakery", "bakery counter with pastries and morning light"), + ("museum", "quiet gallery wall with soft spotlights"), + ("car_interior", "parked vintage car interior with sunset through windows"), + ("boudoir", "candlelit boudoir with draped silks and a vanity mirror"), + ("lingerie_boutique", "upscale lingerie boutique with soft pink lighting"), + ("rooftop_pool", "rooftop infinity pool at night with city glow"), + ("neon_bar", "neon-lit cocktail bar with moody shadows"), + ("penthouse_window", "penthouse window at night overlooking city lights"), + ("velvet_lounge", "velvet speakeasy lounge with warm amber light"), + ("beach_sunset", "tropical beach at golden sunset with gentle surf"), + ("spa", "steamy spa with soft towels and warm tile"), + ("vintage_motel", "retro motel room with a glowing neon sign outside"), + ("art_deco_suite", "art deco hotel suite with gold accents and soft lamps"), + ("jacuzzi", "steamy outdoor jacuzzi at dusk"), + ("yacht_deck", "luxury yacht deck under the sun"), + ("tropical_villa", "open tropical villa with ocean breeze"), + ("loft_window", "sunlit industrial loft with tall windows"), + ("vanity_mirror", "vintage vanity with a bulb-lit mirror"), + ("silk_bed", "rumpled silk-sheet bed in soft morning light"), + ("pool_float", "swimming pool with a flamingo float"), + ("sauna", "warm wooden sauna with soft steam"), + ("beach_cabana", "white beach cabana with billowing curtains"), + ("dressing_room", "theater dressing room with vanity bulbs"), + ("desert_pool", "desert resort pool at golden hour"), + ("garden_pool", "private garden pool surrounded by greenery"), + ("claw_tub_bath", "marble bathroom with a claw-foot tub and soft steam"), + ("walk_in_closet", "mirrored walk-in closet with warm boutique lighting"), + ("four_poster", "draped four-poster bed in a sunlit room"), + ("fireside_rug", "fireside lounge with a soft rug and warm glow"), + ("sunroom", "glass sunroom full of plants and golden afternoon light"), + ("attic_skylight", "cozy attic loft under a starlit skylight"), + ("home_library", "warm home library nook with stacked books"), + ("photo_studio", "photo studio with seamless paper backdrops and softboxes"), + ("hot_spring", "steaming open-air hot spring among rocks"), + ("lavender_field", "rolling lavender field at golden hour"), + ("cherry_blossom", "cherry-blossom park in soft spring light"), + ("autumn_forest", "autumn forest path with falling leaves"), + ("lakeside_dock", "wooden lakeside dock at calm sunrise"), + ("mountain_cabin", "mountain cabin porch with a valley view"), + ("snowy_chalet", "snowy chalet window with warm firelight inside"), + ("waterfall_pool", "secluded jungle waterfall pool"), + ("desert_dunes", "desert dunes at dusk with long shadows"), + ("marina", "marina boardwalk lined with white yachts at sunset"), + ("jazz_club", "smoky jazz club with warm stage light"), + ("casino_floor", "glamorous casino floor with gold accents"), + ("ferris_wheel", "ferris wheel cabin over city lights at night"), + ("retro_diner", "retro diner booth with chrome and neon"), + ("grand_staircase", "grand marble staircase beneath a chandelier"), + ("tiki_bar", "tropical tiki bar with torchlight"), + ("outdoor_shower", "rustic outdoor beach shower in dappled light"), + ("infinity_spa", "infinity-edge spa pool overlooking the sea"), + ("moonlit_pool", "moonlit pool with underwater lights glowing"), + ("rain_window", "rainy night by a window streaked with droplets"), + ("silk_tent", "draped silk tent with warm lantern light"), + ("rose_garden", "rose garden in full bloom at golden hour"), + ("champagne_bar", "marble champagne bar with soft gold light"), + ("ballet_studio", "ballet studio with barres and morning light"), + ("clifftop_pool", "cliffside infinity pool overlooking the ocean"), + ("artist_loft", "paint-splattered artist loft with skylights"), + ("vintage_boudoir", "vintage boudoir with a velvet chaise and lace curtains"), + ("hammam", "ornate tiled hammam with warm steam"), + ("onsen_night", "open-air onsen under a starry night"), + ("cabana_night", "lantern-lit poolside cabana at night"), + ("rooftop_garden", "lush rooftop garden with string lights"), + ("greenhouse_dusk", "glass greenhouse glowing at dusk"), + ("snow_sauna", "log sauna with a frosted window and snow outside"), + ("desert_tent", "luxury desert tent with rugs and lanterns"), + ("yacht_cabin", "cozy yacht cabin with portholes and warm light"), + ("private_jet", "plush private jet cabin with soft cabin light"), + ("vineyard_terrace", "vineyard terrace at sunset with a wine glass"), + ("clifftop_villa", "whitewashed clifftop villa over the sea"), + ("bamboo_spa", "bamboo spa pavilion with diffused light"), + ("waterfall_grotto", "hidden grotto behind a waterfall"), + ("autumn_cabin", "warm cabin interior with autumn light through the windows"), + ("ski_lodge", "ski lodge lounge by a roaring fire"), + ("beach_bonfire", "beach bonfire under a dusky sky"), + ("starlit_rooftop", "rooftop deck under a blanket of stars"), + ("opera_box", "gilded opera box with velvet seats"), + ("speakeasy_booth", "leather speakeasy booth with low amber light"), + ("gold_marble_bath", "opulent marble bath with gold fixtures"), + ("silk_canopy_bed", "silk-canopied bed with sheer drapes"), + ("garden_swing", "wicker hanging swing in a flowering garden"), + ("poolhouse", "modern glass poolhouse with loungers"), + ("rooftop_jacuzzi", "rooftop jacuzzi with the skyline behind"), + ("tropical_waterfall", "tropical waterfall with mossy rocks"), + ("lakeside_sauna", "lakeside sauna deck at sunrise"), + ("powder_room", "vintage powder room with a tufted stool"), + ("gallery_after_hours", "after-hours art gallery with track lighting"), + ("sunset_daybed", "rooftop lounge daybed at sunset"), + ("forest_spring", "misty forest hot spring among ferns"), + ("candle_bath", "candlelit soaking tub with rose petals"), +] + + +def _scene_slug(text: str) -> str: + return re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")[:52] + + +def _extend_scene_unique(target: list[tuple[str, str]], additions: list[tuple[str, str]]) -> None: + seen_slugs = {slug for slug, _ in target} + seen_descriptions = {description for _, description in target} + for slug, description in additions: + if description in seen_descriptions: + continue + base_slug = slug + suffix = 2 + while slug in seen_slugs: + slug = f"{base_slug}_{suffix}" + suffix += 1 + seen_slugs.add(slug) + seen_descriptions.add(description) + target.append((slug, description)) + + +def _expand_scenes() -> list[tuple[str, str]]: + bases = ( + # Interiors / boudoir / glam rooms + ("art_deco_elevator_lobby", "art deco elevator lobby with brass doors and mirrored walls"), + ("boutique_hotel_corridor", "boutique hotel corridor with patterned carpet and wall sconces"), + ("marble_penthouse_foyer", "marble penthouse foyer with a sculptural staircase"), + ("velvet_theater_box", "private velvet theater box with gilded trim"), + ("opera_backstage", "opera backstage dressing area with costume racks"), + ("old_hollywood_suite", "old Hollywood hotel suite with a velvet chaise"), + ("designer_dressing_room", "designer dressing room with garment rails and vanity bulbs"), + ("mirrored_powder_room", "mirrored powder room with polished brass fixtures"), + ("paris_apartment", "Paris apartment with tall windows and herringbone floors"), + ("midcentury_living_room", "midcentury living room with low furniture and wood paneling"), + ("sunken_lounge", "sunken lounge with curved sofas and a conversation pit"), + ("vinyl_listening_room", "vinyl listening room with speakers and stacked records"), + ("private_screening_room", "private screening room with leather seats and dim aisle lights"), + ("fashion_showroom", "fashion showroom with mannequins and fabric bolts"), + ("jewelry_boutique", "jewelry boutique with glass cases and velvet displays"), + ("perfume_counter", "perfume counter with crystal bottles and polished mirrors"), + ("vintage_beauty_salon", "vintage beauty salon with chrome chairs and hair dryers"), + ("makeup_trailer", "film-set makeup trailer with mirror lights and costume notes"), + ("portrait_studio", "portrait studio with painted canvas backdrops"), + ("ceramic_studio", "ceramic studio with clay shelves and a pottery wheel"), + ("glass_artist_studio", "glass artist studio with colored panes and worktables"), + ("florist_workroom", "florist workroom with buckets of flowers and ribbon"), + ("candlelit_conservatory", "glass conservatory filled with plants and candles"), + ("winter_garden_room", "indoor winter garden with palms and tiled floors"), + ("collector_library", "collector library with ladders and leather-bound books"), + ("private_gallery", "private art gallery with sculpture plinths"), + ("loft_stairwell", "industrial loft stairwell with iron railings"), + ("warehouse_photo_set", "converted warehouse photo set with canvas drops"), + ("atelier_balcony", "artist atelier balcony overlooking rooftops"), + ("tailors_fitting_room", "tailor's fitting room with mirrors and pin cushions"), + ("grand_hotel_bar", "grand hotel bar with marble counters and amber glass shelves"), + ("jazz_green_room", "jazz-club green room with velvet curtains and instrument cases"), + ("speakeasy_hallway", "speakeasy hallway with patterned wallpaper and low sconces"), + ("casino_private_room", "private casino room with card tables and gold accents"), + ("champagne_cellar", "champagne cellar with arched brick and stacked bottles"), + ("wine_library", "wine library with dark wood shelves and tasting glasses"), + ("rooftop_greenhouse_room", "rooftop greenhouse room with city windows and leafy plants"), + ("skylit_attic_studio", "skylit attic studio with trunks and soft fabric"), + ("romantic_train_compartment", "vintage train compartment with polished wood and curtains"), + ("luxury_rail_dining_car", "luxury rail dining car with linen tables and brass lamps"), + ("private_jet_lounge", "private jet lounge cabin with cream leather seats"), + ("yacht_saloon", "yacht saloon with varnished wood and porthole light"), + # Urban / nightlife / public spaces + ("neon_arcade", "retro neon arcade with glowing cabinets"), + ("rainy_taxi_stand", "rainy taxi stand with streetlight reflections"), + ("rooftop_cinema", "rooftop cinema with deck chairs and skyline lights"), + ("city_fire_escape", "city fire escape overlooking brick alleys"), + ("brownstone_stoop", "brownstone stoop with wrought-iron railings"), + ("subway_tile_platform", "quiet tiled subway platform with vintage signage"), + ("closed_department_store", "after-hours department store with display windows"), + ("luxury_mall_atrium", "luxury mall atrium with skylights and polished stone"), + ("hotel_elevator_bank", "hotel elevator bank with brass doors and patterned carpet"), + ("parking_garage_rooftop", "parking garage rooftop with city lights"), + ("rainy_neon_crosswalk", "rainy neon crosswalk with glossy pavement"), + ("old_movie_marquee", "old movie theater marquee with warm bulbs"), + ("drive_in_theater", "drive-in theater lot with a glowing screen"), + ("retro_gas_station", "retro gas station with chrome pumps"), + ("midnight_diner_counter", "midnight diner counter with chrome stools"), + ("tiki_lounge_booth", "tiki lounge booth with carved wood and torchlight"), + ("cocktail_lab", "cocktail lab bar with glassware and citrus bowls"), + ("recording_booth", "recording booth with microphones and acoustic panels"), + ("radio_station", "late-night radio station with glowing controls"), + ("dancehall_balcony", "dancehall balcony overlooking a polished floor"), + ("gallery_rooftop_party", "gallery rooftop party with art lights and city views"), + ("fashion_week_backstage", "fashion-week backstage with clothing racks and makeup lights"), + ("photobooth_corner", "vintage photobooth corner with velvet curtains"), + ("bookstore_window_nook", "bookstore window nook with stacked novels"), + ("flower_shop_storefront", "flower shop storefront with buckets of roses"), + ("bakery_back_room", "bakery back room with flour-dusted counters"), + ("tailor_shop_window", "tailor shop window with mannequins and fabric rolls"), + ("hotel_rooftop_bar", "hotel rooftop bar with glass railings and skyline views"), + # Resort / water / spa / luxury travel + ("overwater_bungalow_deck", "overwater bungalow deck above turquoise water"), + ("lagoon_boardwalk", "lagoon boardwalk with resort lights over the water"), + ("white_sand_cabana", "white sand beach cabana with gauzy curtains"), + ("cliffside_shower_alcove", "cliffside outdoor shower alcove with stone walls"), + ("mosaic_spa_pool", "mosaic spa pool with steam and blue tile"), + ("resort_changing_cabana", "resort changing cabana with striped curtains"), + ("infinity_pool_steps", "infinity pool steps overlooking the horizon"), + ("sunset_yacht_bow", "sunset yacht bow with polished railings"), + ("marina_catwalk", "marina catwalk between white yachts"), + ("sail_loft", "sail loft with canvas sails and rope coils"), + ("boathouse_dock", "wooden boathouse dock with quiet water"), + ("lakeside_daybed", "lakeside daybed beneath linen curtains"), + ("hot_spring_grotto", "hot-spring grotto with mossy stone"), + ("onsen_courtyard", "onsen courtyard with wooden screens and steam"), + ("hammam_antechamber", "hammam antechamber with patterned tiles"), + ("saltwater_plunge_pool", "saltwater plunge pool in a private courtyard"), + ("rooftop_spa_terrace", "rooftop spa terrace with loungers and skyline haze"), + ("desert_resort_daybed", "desert resort daybed under a canvas canopy"), + ("moroccan_riad_pool", "Moroccan riad pool courtyard with zellige tile"), + ("balinese_villa_pool", "Balinese villa pool with carved screens and palms"), + ("santorini_terrace", "whitewashed Santorini terrace above the sea"), + ("amalfi_balcony", "Amalfi balcony with lemon trees and ocean view"), + ("capri_boat_deck", "Capri boat deck with striped cushions"), + ("seaside_cliff_path", "seaside cliff path with wild grass and waves below"), + ("beach_club_lounger", "beach club lounger row with umbrellas"), + ("poolside_bar_stools", "poolside bar stools with reflected water light"), + ("floating_breakfast_pool", "private pool with a floating breakfast tray"), + ("waterfall_spa_pavilion", "waterfall spa pavilion with bamboo screens"), + ("jungle_pool_lanai", "jungle pool lanai with tropical leaves"), + ("cave_pool", "cave pool with shafts of light on blue water"), + ("thermal_bath_hall", "thermal bath hall with columns and rising steam"), + # Nature / seasonal / travel + ("wildflower_hillside", "wildflower hillside with a winding path"), + ("orchard_ladder", "fruit orchard with a wooden ladder and baskets"), + ("olive_grove", "olive grove with silver leaves and stone walls"), + ("sunflower_field", "sunflower field with a dirt path"), + ("wheat_field", "wheat field with tall golden stalks"), + ("red_rock_overlook", "red rock desert overlook with layered cliffs"), + ("desert_oasis", "desert oasis with palms and still water"), + ("cactus_garden", "cactus garden with terracotta pots"), + ("rose_covered_archway", "rose-covered archway in a garden path"), + ("wisteria_pergola", "wisteria pergola with hanging purple blooms"), + ("bamboo_forest_path", "bamboo forest path with filtered light"), + ("fern_grotto", "fern grotto with mist and mossy rocks"), + ("rainforest_suspension_bridge", "rainforest suspension bridge among leaves"), + ("coastal_cave", "coastal cave opening onto the sea"), + ("black_sand_beach", "black sand beach with volcanic rocks"), + ("tidal_pool_rocks", "tidal pool rocks with reflective water"), + ("lighthouse_walkway", "lighthouse walkway above crashing waves"), + ("misty_lake_boat", "misty lake with a small wooden boat"), + ("covered_bridge", "covered bridge with autumn trees"), + ("maple_forest_path", "maple forest path with red leaves"), + ("snowy_balcony", "snowy balcony with pine trees and warm window light"), + ("ice_hotel_suite", "ice hotel suite with carved translucent walls"), + ("alpine_hot_tub_deck", "alpine hot tub deck with mountain views"), + ("ski_chalet_balcony", "ski chalet balcony with wool blankets"), + ("mountain_meadow", "mountain meadow with distant peaks"), + ("vineyard_lane", "vineyard lane between grape rows"), + ("lavender_courtyard", "lavender courtyard with stone planters"), + ("tulip_greenhouse", "tulip greenhouse with rows of color"), + ("moon_garden", "moon garden with pale flowers and stone paths"), + ("botanical_palm_house", "botanical palm house with iron-and-glass arches"), + # Editorial / stylized sets + ("seamless_red_backdrop", "seamless red studio backdrop with softbox glow"), + ("pastel_paper_set", "pastel paper studio set with simple geometric blocks"), + ("checkerboard_photo_set", "checkerboard photo set with glossy floor"), + ("chrome_curtain_set", "chrome curtain photo set with reflective strands"), + ("velvet_drape_set", "velvet drape studio set with pooled fabric"), + ("tropical_print_set", "tropical-print studio set with painted palm leaves"), + ("noir_shadow_set", "noir shadow studio set with venetian-blind light"), + ("pop_art_set", "pop-art studio set with oversized dots and color blocks"), + ("magazine_cover_set", "magazine cover photo set with clean graphic lighting"), + ("lingerie_editorial_set", "lingerie editorial studio set with soft curtains and floor cushions"), + ("swimwear_catalog_set", "swimwear catalog set with sand-colored floor and blue backdrop"), + ("boudoir_editorial_set", "boudoir editorial set with silk sheets and a velvet bench"), + ) + moods = ( + ("golden_hour", "in golden-hour light with warm shadows"), + ("lamplight", "with warm lamplight and soft shadows"), + ("blue_hour", "at blue hour with glowing accent lights"), + ("morning", "in airy morning light"), + ("night_glow", "at night with cinematic practical lights"), + ("overcast", "under soft overcast light"), + ("neon_reflections", "with subtle neon reflections"), + ("candlelit", "with candlelit accents"), + ) + additions: list[tuple[str, str]] = [] + for base_slug, base_description in bases: + for mood_slug, mood_description in moods: + additions.append(( + _scene_slug(f"{base_slug}_{mood_slug}"), + f"{base_description} {mood_description}", + )) + additions.extend( + [ + ("rainy_paris_balcony", "rainy Paris balcony with wrought-iron rails and glowing apartment windows"), + ("monsoon_hotel_veranda", "monsoon hotel veranda with wet tile and lush plants"), + ("desert_motel_pool_night", "desert motel pool at night with a buzzing neon sign"), + ("carnival_after_hours", "after-hours carnival midway with string lights and quiet rides"), + ("circus_dressing_tent", "circus dressing tent with striped canvas and costume trunks"), + ("masquerade_ballroom", "masquerade ballroom with marble columns and scattered masks"), + ("cruise_ship_promenade", "cruise ship promenade deck with warm rail lights"), + ("glass_skybridge", "glass skybridge between towers with city lights below"), + ("botanical_hotel_lobby", "botanical hotel lobby with palms and brass elevators"), + ("desert_observatory", "desert observatory deck under a deep starry sky"), + ("moonlit_orchid_house", "moonlit orchid house with humid glass and pale blooms"), + ("private_pool_cinema", "private pool cinema with floating lights and a projection screen"), + ("rooftop_tent_lounge", "rooftop tent lounge with rugs and lanterns"), + ("velvet_train_station_lounge", "velvet train-station lounge with arched windows"), + ("snow_globe_storefront", "holiday storefront with frosted glass and warm displays"), + ("valentine_chocolate_shop", "chocolate shop with ribbon boxes and rose petals"), + ("summer_boardwalk_arcade", "summer boardwalk arcade with ocean air and neon signs"), + ("autumn_book_cafe", "autumn book cafe with window rain and amber lamps"), + ("spring_flower_atrium", "spring flower atrium with glass roof and pastel blossoms"), + ("new_years_rooftop", "New Year's rooftop terrace with city fireworks in the distance"), + ("studio_water_tank", "studio water tank set with rippling reflections"), + ("mirror_maze_set", "mirror maze set with soft theatrical lights"), + ("giant_fan_photo_set", "photo set with a giant fan and wind-swept fabric"), + ("silk_ribbon_set", "studio set filled with long silk ribbons and soft pastel light"), + ("paper_moon_set", "vintage paper moon photo set with painted stars"), + ("oversized_cocktail_glass_set", "playful editorial set with oversized cocktail props"), + ("retro_pool_float_set", "retro pool-float set with glossy blue floor"), + ("dreamy_cloud_backdrop", "dreamy cloud backdrop with cottony set pieces"), + ("neon_heart_wall", "neon heart wall with glossy black floor"), + ("gold_confetti_set", "gold confetti studio set with warm spotlights"), + ] + ) + return additions + + +_extend_scene_unique(SCENES, _expand_scenes()) + +# Body language only -- facial expression is a separate axis (see EXPRESSIONS). +POSES = [ + "confident hip-angled pose", + "relaxed lean with one hand at the waist", + "playful over-the-shoulder glance", + "standing tall with one hand holding a prop", + "seated with crossed ankles", + "leaning on a counter", + "walking pose with wind-touched hair", + "one hand lightly adjusting hair", + "editorial fashion stance with shoulders angled toward camera", + "cozy seated pose", + "arched-back pin-up pose", + "lying on one side propped on an elbow", + "kneeling pose with a slight back arch", + "leaning back against a wall with one knee bent", + "stretching pose with arms raised overhead", + "sitting on the floor with knees drawn up", + "glancing back with hips turned away", + "perched on the edge of a table with crossed legs", + "reclining gracefully across a chaise", + "hands framing the waist in a classic cheesecake pose", + "twisting at the waist to glance back", + "seated on the floor leaning back on both hands", + "kneeling upright with hands resting on thighs", + "lying on the back with knees raised", + "leaning forward with hands on the knees", + "one foot propped up on a stool", + "arched spine with arms stretched overhead", + "sitting sideways draped over the back of a chair", + "standing with arms crossed and a hip pop", + "lounging with one arm tucked behind the head", + "peeking playfully around a doorframe", + "balanced on tiptoes with a gentle twist", + "hands clasped behind the back with the shoulders open", + "one hand on the hip, the other lifting the hair", + "seated with legs crossed and leaning forward slightly", + "leaning against a doorframe with ankles crossed", + "looking back with a hand resting on the hip", + "perched on a stool with one heel hooked on the rung", + "standing in profile with the back gently arched", + "sitting on the floor with legs swept to one side", + "reclining on an elbow with the legs angled", + "tilting the head while adjusting an earring", + "leaning forward with forearms on a railing", + "one knee up on a chair, leaning on the thigh", + "stretching the arms overhead with a soft side bend", + "crossing the ankles while leaning on a wall", + "resting both hands on a tabletop and leaning in", + "twirling slightly so the skirt flares", + "sitting sideways with one arm over the chair back", + "gazing over the shoulder with a relaxed stance", +] + +# "evocative" pose mode: described with neutral movement / fashion / yoga / +# dance vocabulary (arch, stretch, recline, lean, glance, lift the hair) that +# reads as alluring through body line and geometry -- no sexual or flaggable +# wording, so it stays safe through the image model's filters. Selected with +# --poses evocative. +EVOCATIVE_POSES = [ + "arching the back gently with both arms reaching overhead", + "reclining across the bed with one knee raised", + "kneeling on soft cushions, leaning back on both hands", + "stretching languidly with both arms above the head", + "lying on one side with the head propped on a hand", + "leaning forward over a railing, weight on the forearms", + "glancing back over one bare shoulder", + "rising onto tiptoes in a long arched line", + "draped sideways across an armchair, one leg over the armrest", + "twisting at the waist with one hip pushed out", + "seated on the floor with knees drawn up, leaning back on the hands", + "reclining against a pile of pillows in a relaxed open posture", + "lying on the stomach with ankles crossed and lifted", + "reaching up to tie the hair, elbows raised and back arched", + "leaning against a wall with one knee bent and hip cocked", + "stretching low to the floor with shoulders down and hips raised", + "perched on the edge of a table, leaning back on straight arms", + "curled on one side with a soft inward curve of the body", + "standing with weight on one hip and shoulders rolled back", + "kneeling tall and lifting the chin with hands sliding down the thighs", + "lounging back on the elbows with legs extended and crossed at the ankle", + "rolling the shoulders back while lifting the hair off the neck", + "bending at the waist to adjust a shoe strap", + "stepping out of the water with the back arched and hair swept back", + "leaning back against the headboard with both arms raised to the pillows", + "lying back with one arm draped lazily above the head", + "rolling onto the back with the knees softly bent and relaxed apart", + "reaching the arms forward along the floor with the hips lifted", + "seated on a windowsill with one knee drawn up to the chest", + "leaning into a doorway with one arm stretched up the frame", + "tilting the head back while running both hands through the hair", + "stepping forward mid-stride with the hips leading the motion", + "pausing mid-turn with the body caught in a soft spiral", + "crouching low and reaching one hand toward the floor", + "sitting back onto the heels with a long lifted spine", + "draping back over the arm of a sofa with the head tipped back", + "lying across the foot of the bed on the stomach with ankles raised", + "propped on one elbow with the other hand resting on the hip", + "leaning on a counter with the back gently arched", + "kneeling on a chair and leaning over the backrest", + "reaching overhead to a high shelf, the body drawn into a long line", + "sitting sideways on the floor with one hip dipped and legs folded", + "tipping the face up into the light with the shoulders drawn open", + "leaning a shoulder against a column with the weight on one side", + "wrapping a towel loosely while looking back over the shoulder", + "settling into a low lunge with the hips lowering and the chest open", + "lying along a chaise on one side with the top leg sliding forward", + "sitting on the edge of a tub with one foot dipped in the water", + "leaning forward onto a vanity to meet the mirror's gaze", + "perched on a stool with crossed legs and the torso leaned back", + "curling forward to fasten a sandal with one shoulder dipped", + "reclining on a towel poolside with one knee bent up", + "leaning out over a balcony rail into the breeze", + "sliding down a wall into a relaxed low crouch", + "stretching across the bed to reach the nightstand", + "pressing the back to the wall with the hips eased forward", + "lying back in tall grass with the arms loose overhead", + "leaning on folded arms across a table with the shoulders forward", + "swaying mid-dance as the weight shifts onto one hip", + "rising onto the knees with a long lifted line through the body", + "tilting back in a chair with both arms folded behind the head", + "stretching like waking, arms overhead and the back in a deep curve", + "balancing on the balls of the feet with the ribs lifted high", + "trailing one hand down the side of the body in a slow stretch", + "kneeling forward over a cushion with the spine in a soft dip", + "looking back across the room with the chin over one shoulder", + "easing onto the side with the top knee drawn up the leg", + "leaning across a piano with the weight on the forearms", + "rolling the hips into a slow contrapposto stance", + "sitting on the floor and leaning back on locked arms, chin up", + "kneeling upright with the back deeply arched and the chin lifted", + "kneeling and sinking the hips back toward the heels with a long lower-back curve", + "on all fours in a long cat-stretch with the shoulders low, glancing back over the shoulder", + "kneeling tall with the hips eased forward and both hands resting on the thighs", + "lying on the back drawing both knees up with the feet flat and a hand on the stomach", + "kneeling and reaching both hands back to gather the hair with the chest lifted", + "kneeling and leaning back onto straight arms with the hips eased up", + "arching back over a low ottoman with the arms reaching overhead", + "lying on the back with one knee raised, gazing up toward the viewer", + "kneeling back on the heels and tipping the head back with the eyes closed", + "rising onto the knees on the bed with a long arch through the spine", + "sitting back on the heels and arching to reach into the hair with the chest open", + "lying back across pillows lifting the hips into a gentle bridge", + "kneeling tall and clasping the hands behind the head with the elbows wide", + "on the knees with the weight sunk into one hip, leaning onto one hand", + "lying on the stomach and pushing up onto the forearms with an arched back, chin up", + "kneeling and tipping the torso back with the hands resting high on the thighs", + "lying on the side and drawing the top knee up toward the chest", + "crouched low on the toes with the forearms resting on the knees, looking up", + "kneeling and rolling the shoulders back with the hands gliding down the front of the body", + "arching deeply backward with the hands sliding down the thighs", + "lying back with both arms stretched overhead and the spine in a long curve", + "kneeling and bowing forward with the arms reaching far along the floor", + "sitting with the knees together and the ankles apart, leaning back on the hands", + "lifting one leg onto a chair and leaning over the raised knee", + "reclining with one arm behind the head and the other along the hip", + "twisting to look back with both hands gathering the hair up high", + "lying on the side with the top leg drawn forward and the hip lifted", + "rising from the water with the head tipped back and the back arched", + "leaning over a vanity with the back arched and the chin lifted to the mirror", + "sitting on the heels and arching back to rest the hands on the floor behind", + "kneeling upright and arching to drape the head and arms backward", + "stretching up onto the toes with the ribs lifted and the hips tilted", + "lying along the edge of a pool with one knee bent and the toes pointed", + "curling onto the side and reaching one arm long overhead", + "leaning back on a sofa with one leg lifted along the cushions", + "kneeling tall and slowly peeling a long glove down one arm", + "easing a strap off one shoulder while glancing down", + "sitting on a counter and leaning back on straight arms with the chin up", + "arching off the bed with the shoulders down and the hips lifting", + "reclining across pillows with one knee raised and the other leg long", + "lying on the stomach with the chin on the hands and the ankles crossed high", + "rolling onto the back with both knees drawn up and tilted to one side", + "sitting on the floor and leaning far back with the chest open to the ceiling", + "kneeling and trailing both hands slowly down the sides of the body", + "tipping the head back under a stream of water with the back arched", + "stretching across a chaise on the side with the top leg sliding down", + "leaning a hip to a wall and arching away with the arms overhead", + "sitting with the legs folded to one side and twisting to look back", + "reaching back to unclasp the hair with the chest lifted and the back arched", + "lying back with one hand resting low on the stomach and the other overhead", + "balancing on one hip on the floor with the legs stacked and folded", + "leaning forward over crossed arms with the shoulders drawn toward the viewer", + "drawing one knee to the chest while seated and resting the chin on it", + "stretching long on the back with the toes pointed and the arms overhead", + "sinking into a deep side bend with one arm sweeping overhead", + "perching on the edge of a tub and leaning back with the spine arched", + "lying on the side propped on a forearm with the top leg bent up", + "kneeling and clasping the hands high overhead in a long stretch", + "twisting at the waist while seated, one hand on the floor behind", + "rolling the shoulders open while reclining on a heap of cushions", + "lifting the hair off the neck with both hands and tilting the head back", + "stepping out of a bath with one foot raised to the rim and the back curved", + "lying back across a bench with the head tipped and the arms loose", + "kneeling and arching while smoothing both hands up over the ribs", + "sitting on the floor leaning on one hand and stretching the other leg out long", + "curving the back over the arm of a chaise with the legs draped down", + "reaching overhead to grip a canopy post with the body drawn long", + "lying on the front and arching up onto the forearms with the chin lifted", + "kneeling back on the heels and arching to graze the floor behind with the fingertips", + "lifting one leg to a rail and folding forward along the extended thigh", + "reclining with the hips rolled to one side and one arm overhead", + # Prop-interactive poses — phallic-shaped items used in classic suggestive pin-up compositions + "kneeling upright with a cucumber held to each side of the head like antennae, looking up with wide eyes", + "lying back with a banana resting lightly across the lips, eyes half-closed", + "kneeling with a corn cob pressed to each cheek, gazing straight at the viewer", + "seated with a long zucchini balanced across both wrists, arms extended forward", + "biting the tip of an asparagus spear with a coy over-the-shoulder glance", + "reclining on one elbow with a peeled banana held above the face, studying it", + "pressing a popsicle to the chin and gazing up through the lashes", + "kneeling and cradling a large eggplant in both hands with a soft amused smile", + "holding a banana at chin level with both hands, lips slightly parted", + "sitting cross-legged with a corn cob raised to the lips, looking directly at the viewer", + "bending forward at the waist with a cucumber held to each side of the head", + "lying on the front propped on the elbows with a banana balanced on the lower lip", + "kneeling tall and pressing a long carrot to the lips with both hands", + "seated at a vanity pressing two cucumbers to the cheeks like earrings, eyes wide", + "gripping a pool cue at the base while leaning forward over it with the arms long", + "lying on one side with a banana balanced along the curve of the hip", + "standing with a baseball bat resting on one shoulder, one hand on the hip, looking back", + "holding two long carrots hip-width apart at waist height, hip cocked to one side", + "wrapping both hands around a wine bottle held at lap height, gazing up at the viewer", + "seated with a hot dog held at lip level in both hands, head tilted", + "kneeling with both arms raised holding a corn cob in each hand beside the face", + "lying back on the elbows with a zucchini resting across the lap, one hand on it", + "pressing the flat of a cucumber against one cheek and looking sidelong at the viewer", + "holding a churro to the lips with one hand while the other rests on the knee", + "seated cross-legged with a large eggplant balanced upright between the palms", + "sliding fingers slowly down a banana held upright, gazing at the viewer", + "pressing the curved tip of a banana to the lower lip with both hands", + "cradling a banana in both outstretched palms offered toward the viewer", + "running a fingertip from base to tip along a banana held in the other hand", + "seated with a banana balanced upright between pressed-together knees", + "reclining with a peeled banana held loosely above the face, tracing the curve with one finger", + "kneeling and presenting a peeled banana in both hands extended toward the viewer", + "pressing a cucumber lengthwise along the cheek with a sleepy expression", + "holding a cucumber upright in one fist beside the face, direct gaze", + "lying on the side with a cucumber pressed lightly to the lips", + "kneeling with a cucumber pressed vertically between both palms in prayer position", + "seated with two cucumbers held upright at either side of the head", + "on all fours with a cucumber balanced along the spine, glancing back", + "leaning forward with a cucumber resting in the palm extended toward the viewer", + "biting into a corn cob while seated cross-legged, eyes on the viewer", + "lying on the stomach propped on the elbows holding a corn cob up beside the face", + "kneeling tall with a corn cob held upright in each raised fist", + "licking along the side of a popsicle with the eyes closed", + "pulling a popsicle slowly from between lightly closed lips", + "pressing a melting popsicle to the cheek and glancing sidelong at the viewer", + "pressing an ice lolly flat against the tongue with the eyes on the viewer", + "letting ice cream drip over the fingers and licking one finger clean", + "holding an ice cream cone at lip level with the tongue extended toward it", + "biting the end off a carrot, eyes wide", + "holding a long carrot horizontally between the teeth, hands on hips", + "pressed against a wall with a carrot held up beside the head", + "stroking a finger slowly down the length of a long carrot, head tilted", + "pressing two carrots to the temples like horns, looking wide-eyed at the viewer", + "cradling a large eggplant in both arms against the chest", + "lying on the side with a large eggplant propped against the hip", + "holding an eggplant at waist height in both hands, head tilted, soft smile", + "resting a long zucchini on one shoulder like a rifle, one hand on the hip", + "seated with a zucchini laid across the thighs, hands resting on each end", + "pressing a zucchini lengthwise to the lips, eyes half-closed", + "holding a zucchini at hip height in one hand with the hip popped out", + "pressing an unlit cigar to the lips with two fingers, one brow raised", + "trailing a cigar along the lower lip with half-closed eyes", + "holding a long cigarette holder between two fingers beside the face, chin lifted", + "holding a pool cue vertical beside the body, one hand high and one hand low", + "leaning over a table with the pool cue extended forward, hips angled back", + "seated on a surface gripping a pool cue between the knees, leaning forward on it", + "wrapping the lips loosely around the neck of a wine bottle, eyes gazing up", + "pressing the base of a wine bottle to the chin, looking forward", + "tilting a wine bottle above the open mouth with one hand", + "pressing a microphone to the lips in a singing pose, eyes closed", + "holding a microphone upright at chin level, head tipped back slightly", + "seated on a stool leaning toward a microphone at lap height, lips parted", + "seated with a baseball bat standing upright between the knees, both hands wrapped around it", + "resting both hands on top of a bat planted between the feet, leaning on it", + "holding a breadstick between the teeth while looking at the viewer over one shoulder", + "pressing a hot dog to the lips with a playful expression", + "holding a long breadstick in each raised fist beside the head, kneeling", + "lying on the back with a cucumber resting lengthwise across the stomach, hands at the sides", + "sitting with the back gently arched and a zucchini balanced across the collarbone", + "leaning forward with a corn cob suspended between two fingers at lip height", + "seated holding a banana in the crook of the elbow, head resting on it", + "kneeling with two bananas held up beside the temples, gazing forward", + "seated with a wine bottle standing upright between closed knees, both hands on the neck", + "lying on the front with a long carrot balanced across the back of both hands", + "pressing an eggplant between both palms at chest height, chin down, eyes up", + "holding a churro with the tip resting on the lower lip, soft smile", + "biting through the middle of a corn cob with both hands raised, eyes on the viewer", + "kneeling with a cucumber pressed under the chin, both palms cupping it", + # Extended prop-interactive set — banana + "holding a banana with both hands at arm's length and studying it with a raised brow", + "tucking a banana under the chin with both hands clasped beneath it", + "balancing a banana on the nose tip with arms wide for balance", + "pressing a banana lengthwise to the cheek and closing the eyes", + "tracing the tip of a banana slowly with one index finger while holding it in the other hand", + "reclining on the back with a banana balanced vertically on the sternum", + "sitting with a banana balanced upright between pressed knees, hands on thighs", + "kneeling and offering a banana in both cupped hands with the chin dipped", + "lying on the side with a banana tucked between the chin and collarbone", + "standing with a banana in each hand raised shoulder-high like dumbbells, grinning", + "pressing the flat of a banana to the lips with both hands and closing the eyes", + "seated with a banana balanced across the thighs end to end, hands on hips", + "pressing a banana to both lips as if about to eat it, eyes on the viewer", + "nuzzling the tip of a banana against the nose with both hands, eyes closed", + "dragging a banana slowly across the lower lip while gazing at the viewer", + "holding a banana in one fist and tapping it thoughtfully against the chin", + "seated with a banana held vertically in each fist raised on either side of the head", + "lying on the stomach propped on the elbows, a banana held horizontally beneath the chin", + "resting a banana diagonally across the collarbone with one hand steadying each end", + "kneeling with a banana pressed flat to the lips, eyes closed, lashes down", + # Extended prop-interactive set — cucumber + "lying on the back with a cucumber balanced across the closed eyes, hands folded on the stomach", + "seated with a cucumber balanced upright on one knee, one hand steadying it", + "holding a cucumber horizontally between the teeth with arms crossed", + "pressing a cucumber flat to the stomach with both hands, back gently arched", + "kneeling and tapping a cucumber lightly against the lips, gazing to one side", + "lying on the side with a cucumber propped against the hip bone, hand resting on it", + "dangling a cucumber from two fingers at hip height while looking at the viewer", + "pressing two cucumbers end-to-end across the lips, eyes half-closed", + "seated with a cucumber held vertically in front of the face, one eye peeking around it", + "lying on the front with a cucumber pressed lengthwise between the forearms on the floor", + "kneeling and balancing a cucumber upright on an open palm held overhead", + "pressing a cucumber under the chin with one finger, head tilted back", + "holding a cucumber in both hands at waist height and looking down at it", + "seated cross-legged with a long cucumber resting across both ankles", + "standing with a cucumber balanced on one shoulder, steadying it with one finger", + "lying on the back holding a cucumber above the face, rotating it slowly", + "pressing the flat end of a cucumber to the lower lip, gazing sidelong", + "kneeling with a cucumber cradled in both arms against the chest like a bouquet", + "seated at a table resting the chin on one end of a cucumber, eyes up", + "holding a cucumber with both hands at chest height, squeezing it gently", + # Extended prop-interactive set — corn cob + "holding a corn cob in each hand and pressing both to the cheeks simultaneously", + "seated with a corn cob pressed between the palms at chest height, elbows out", + "reclining and gnawing the side of a corn cob with a glance at the viewer", + "raising a corn cob with both hands above the head, tilting back to bite it", + "kneeling and offering a corn cob forward with both hands, chin down and eyes up", + "holding a corn cob up like a microphone and leaning toward it with lips parted", + "biting through the far end of a corn cob with both arms raised holding it high", + "pressing two corn cobs to the temples like spiral horns, wide eyes", + "seated with a corn cob balanced on each knee, palms resting on each end", + "lying on the front with a corn cob propped under the chin for the head to rest on", + "holding a corn cob at chin level between two fingers like a cigar, one brow raised", + "kneeling with a corn cob held to each shoulder like epaulettes, chin lifted", + # Extended prop-interactive set — popsicle / ice cream / ice lolly + "letting a popsicle drip down the chin and catching the drop on a fingertip", + "tracing the outline of the lips slowly with a popsicle, eyes on the viewer", + "licking a dripping ice cream cone from the base to the top", + "pressing an ice lolly flat to the tongue, making direct eye contact", + "pressing a popsicle to one cheek and smiling at the viewer", + "holding an ice cream cone at lip height with the tongue tip extended toward it", + "pressing a melting popsicle under the lower lip with eyes heavy-lidded", + "squeezing a popsicle upward out of its wrapper, watching it rise", + "running the tip of the tongue slowly along the length of a popsicle from base to top", + "holding a half-eaten popsicle to the lips and looking sideways at the viewer", + "pressing an ice lolly flat to the lower lip with two fingers, lashes down", + "tilting an ice cream cone toward the open mouth, a drop ready to fall", + # Extended prop-interactive set — carrot + "seated with a carrot balanced across the nose, eyes wide", + "pressing a carrot flat to the cheek and squinting at the viewer", + "seated with a long carrot held straight up in one fist at shoulder height, smiling", + "nibbling along the side of a carrot slowly, eyes locked on the viewer", + "biting off the leafy end of a carrot with a grin", + "holding a carrot horizontally below the nose like a moustache, one brow up", + "pressing a carrot tip to the lips and kissing it lightly", + "reclining with a long carrot balanced diagonally across the collarbone", + "kneeling with a carrot pressed gently to each cheek like blush sticks", + "lying on the stomach holding a carrot upright like a candle in front of the face", + "seated and holding a peeled carrot at waist height in both hands, head tilted", + "pressing a carrot flat to the tongue, eyes closed", + # Extended prop-interactive set — eggplant + "seated with a large eggplant cradled against one cheek, eyes soft", + "holding an eggplant at face height with both hands and gazing over the top of it", + "pressing the rounded end of an eggplant to the chin, head tilted, smiling", + "balancing an eggplant on end on an open palm held at shoulder height", + "kneeling and hugging a large eggplant to the chest", + "seated with an eggplant gripped at either end, resting it across the knees", + "pressing an eggplant slowly to the lips with closed eyes", + "lying on the side with an eggplant held vertically in both hands in front of the face", + "holding an eggplant up to the cheek and closing the eye on that side", + "seated cross-legged with an eggplant balanced upright on one palm, gazing at it", + "pressing the stem end of an eggplant lightly to the lower lip", + "kneeling with a large eggplant resting on each upturned thigh, hands on top", + # Extended prop-interactive set — zucchini + "seated with a long zucchini across both knees, one hand on each end", + "holding a zucchini at chest height like a scepter, chin lifted", + "pressing a zucchini end-on to the lips with eyes half-closed", + "lying on the front with a zucchini balanced between both forearms", + "standing with a zucchini held vertically alongside the face, one eye peeking past it", + "holding a zucchini horizontally below the nose, one brow raised", + "seated and tapping a zucchini slowly against the chin", + "kneeling with a zucchini gripped in both hands at waist height, arms extended", + "pressing a zucchini flat to the sternum with both palms, back slightly arched", + "lying on the back with a zucchini balanced standing upright on the stomach", + "seated on the floor with a long zucchini bridging the gap between both raised knees", + "holding two zucchinis upright beside the head like pillars, wide-eyed", + # Extended prop-interactive set — pool cue + "standing with a pool cue gripped in both fists at hip height, hips angled", + "seated with the tip of a pool cue resting on the lower lip, eyes thoughtful", + "kneeling with a pool cue held vertically at the side, both hands low on the shaft", + "resting both wrists over the top of a pool cue balanced horizontally behind the neck", + "leaning the chin on the butt end of a pool cue held vertical between the feet", + "seated with a pool cue gripped at hip height with both hands, leaning forward", + # Extended prop-interactive set — wine bottle + "cradling a wine bottle lengthwise against the body, both arms around it", + "pressing a wine bottle vertically to the stomach with both hands, back slightly arched", + "seated with a wine bottle between the knees, both hands on the neck, leaning in", + "trailing the neck of a wine bottle slowly down the collarbone", + "holding a wine bottle at arm's length and gazing at it", + "reclining with a wine bottle balanced across the hip, one hand loosely on the neck", + "pressing the neck of a wine bottle lightly to the lips, eyes heavy", + "sitting cross-legged with a wine bottle held upright in both palms offered forward", + # Extended prop-interactive set — microphone + "building up to sing with a microphone in one fist at lip height, eyes closed", + "pressing the microphone head to the chin and gazing slowly down its length", + "wrapping one hand around a microphone at waist height, hip cocked, head turned", + "holding a microphone with both hands close to the lips, mid-lyric", + "seated with a microphone pressed to the cheek, eyes soft", + "trailing a microphone slowly down from the lips to the chin", + # Extended prop-interactive set — baseball bat + "resting a bat across both shoulders behind the head with arms draped over it", + "seated with a bat held vertically in both hands between the knees", + "holding a bat in one hand with the end resting on the floor, leaning on it", + "pressing the barrel end of a bat slowly to the lips", + "lying on the back with a bat balanced across the raised knees, hands folded on it", + # Extended prop-interactive set — hot dog / breadstick / churro + "holding a hot dog up beside the face with a mock-serious comparison expression", + "pressing a hot dog slowly to the lips, one eyebrow arched", + "seated with a hot dog held at lap height in both hands, leaning toward it", + "pressing a breadstick to one corner of the mouth", + "holding a breadstick vertically in front of the face like a wand, one eye closed", + "nibbling slowly along the length of a breadstick, eyes on the viewer", + "pressing a churro to both lips as if about to bite it, eyes up", + "holding a churro at chin level and gazing past it at the viewer", + "dragging a churro slowly across the lower lip, lashes down", + # Extended prop-interactive set — cigar + "pressing an unlit cigar vertically to the closed lips, chin lifted", + "rolling a cigar slowly between two fingers at chin height, gazing at the viewer", + "holding a cigar between the teeth at the corner of the mouth, one brow raised", + "pressing an unlit cigar to one cheek and closing the eye on that side", + # Extended multi-prop combinations + "kneeling with a banana in one hand and a cucumber in the other, one held to each cheek", + "seated with a corn cob in each hand pressed to the temples, staring at the viewer", + "lying back with a cucumber across the lips and a banana resting on the collarbone", + "kneeling with a zucchini balanced across the shoulders and a carrot held to the lips", + "holding a hot dog in one hand and a banana in the other, one at each side of the face", + # Final top-up to reach 2-in-3 + "seated with a banana pressed end-on to the lips, eyes drifting closed", + "holding a cucumber in one hand and tapping the tip against the chin thoughtfully", + "kneeling with a zucchini held out in both hands like an offering, eyes down", + "pressing a corn cob to the cheek and holding it there, eyes on the viewer", + "lying back with a long carrot balanced diagonally across the chest, hands at the sides", + "seated and pressing an eggplant vertically to the sternum with both palms", + "holding a popsicle at arm's length and studying it before bringing it to the lips", + "pressing the tip of a churro gently to the tip of the tongue", + "seated with a wine bottle tilted toward the lips, eyes half-closed", + "reclining with a banana held loosely in the fingers at hip height", + "kneeling with a corn cob pressed flat to the lips with both hands, eyes up", + "pressing a cucumber end-on against the closed lips, lashes lowered", + "seated with a breadstick balanced across the upper lip like a moustache, one brow raised", + "standing and tracing a slow circle on the chest with the tip of a carrot", + "lying on the side with a banana balanced on the upper hip, one hand resting on it", + "pressing an unlit cigar slowly from one corner of the mouth to the other", + "seated with a hot dog held vertically between two fingers at eye level, staring past it", + "kneeling and pressing the length of a banana slowly to one cheek", + "lying on the back with two cucumbers balanced upright side by side on the stomach", + "seated with a long zucchini pressed to the collarbone, chin resting on the top end", + "holding a corn cob upright in each fist at chin height, looking at the viewer over them", + "dragging the tip of a popsicle slowly along the lower lip, chin tilted up", + "seated with a carrot pressed lengthwise to the lips, both hands cupping it", + "pressing the microphone to the lower lip and breathing slowly, eyes soft", + "kneeling with a baseball bat leaned against one shoulder, arms folded across the top", + "seated with an eggplant held at hip height and one finger resting on its tip", + "lying on the stomach with a banana pressed lengthwise to the lips, chin raised", + "holding a wine bottle vertically in both hands at lap height and looking up at the viewer", + "pressed against a wall with a cucumber held vertically at chest height in one fist", + "seated with two breadsticks held parallel at lip height, peering at the viewer between them", + "kneeling upright with a long carrot balanced horizontally across both open palms at chest height", + "reclining on one elbow, trailing the tip of a zucchini slowly down the forearm", + "seated cross-legged pressing a banana gently to each cheek with both hands", + "holding a pool cue behind the neck across the shoulders, arms draped over it, hips swaying", + "pressing a churro tip-first gently between the teeth, looking at the viewer", + "lying on the back with a cucumber balanced upright and held steady by one fingertip on the tip", + "seated and resting both wrists on top of a large eggplant stood upright in the lap", + "kneeling with a corn cob pressed to one ear like a telephone, eyes wide", + "pressing a hot dog lengthwise to the lips with both hands, one eye on the viewer", + "seated with a long zucchini pressed along the forearm from wrist to elbow, comparing lengths", + "holding a banana upright between pressed-together palms at chest height in a prayer pose", + "lying on the stomach with a large eggplant balanced on the lower back, hands folded under the chin", + "kneeling with a popsicle pressed to the lower lip and ice cream beginning to melt over the fingers", + "seated with a wine bottle cradled in the crook of the elbow, free hand on the hip", + "standing with a carrot pressed to the chin like a conductor's baton, one hand raised", + "pressing a corn cob gently to the mouth, tilting the head as if listening to it", + "seated with a cucumber balanced across the back of both hands resting in the lap", + "lying on the side with two bananas held together end-to-end at lips height", + "kneeling and holding a breadstick horizontally between the lips, hands raised", + "seated with the end of a pool cue resting on the lower lip, eyes looking up its length", + "pressing a zucchini to both lips and closing the eyes, chin slightly tilted up", + "lying back with an eggplant held above the face, slowly lowering it toward the lips", + "seated with a hot dog balanced on the nose tip, arms outstretched for balance", + "kneeling with a carrot pressed between both palms, fingers interlaced around it", + "reclining on the elbows with a banana balanced horizontally across the mouth", + "pressing a cucumber end-first to the chin, looking steadily at the viewer", + "seated with a corn cob held at chest height, tracing the rows with one fingertip", + "lying on the front with a long zucchini pressed under the chin, head raised", + "kneeling tall with an unlit cigar pressed to the lips, chin up, gaze level", +] + +# back-to-viewer / rear-emphasis pin-up angles (glance-over-shoulder cheesecake). +# Kept separate so --backside-bias can weight how often they appear. +BACKSIDE_POSES = [ + "standing with the back to the viewer, weight on one hip, glancing over one shoulder", + "walking away and looking back over the shoulder", + "lying on the front across the bed with the ankles crossed and lifted, propped on the elbows", + "seated on the floor facing away, leaning back on the hands with the head turned", + "kneeling upright facing away with an arched back, glancing over the shoulder", + "facing a wall with a forearm resting on it, looking back over the shoulder", + "standing at a window from behind with the hip cocked, glancing back", + "running both hands up into the hair, seen from behind", + "arching the back while facing away with the head turned to the side", + "stretching tall facing away with the arms overhead and the back lengthened", + "leaning in to a mirror with the back toward the viewer, glancing at the reflection", + "leaning over a balcony rail facing out with the weight on the forearms", + "kneeling on the bed facing away and sinking toward the heels, looking over the shoulder", + "rising onto tiptoe facing away, reaching upward with an arched back", + "wrapped loosely in a sheet from behind, glancing over the shoulder", + "sitting sideways with the back half-turned, looking over the shoulder", + "wading away into the surf and looking back over the shoulder", + "leaning a shoulder to the wall facing away, hips angled, head turned back", + "seated on a stool facing away with a long curve through the spine, glancing back", + "stretching across a lounge chair on the front with the ankles raised", + "standing with the back to the viewer and looking over the shoulder while lifting the hair", + "seated on the floor facing away with the spine arched, head turned back", + "kneeling tall facing away and reaching both arms overhead, glancing back", + "leaning forearms on a balcony rail facing out, hips angled, looking back", + "walking toward a doorway and pausing to glance back over the shoulder", + "standing rear-on with one hip pushed out, head turned to the viewer", + "facing a full-length mirror from behind, meeting the reflection's eyes", + "lying on the front along the bed propped on the forearms, ankles crossed up, looking back", + "rising onto tiptoe facing away with the arms stretched high", + "seated sideways on a stool, back half-turned, glancing over the shoulder", + "leaning a shoulder to a column facing away, head tipped back toward the viewer", + "wrapping a towel low and looking back over the shoulder", + "standing at a rain-streaked window from behind, a hand on the glass, glancing back", + "kneeling on a cushion facing away and arching the back, chin over the shoulder", + "stepping into the surf facing away and turning the head back", + "facing away and gathering the hair up off the neck, head dipped", + "standing with the hands flat on a wall facing away, hips angled, looking back", + "perched on the edge of a bed facing away with a long curve through the spine", + "leaning over a vanity facing away, glancing back at the viewer", + "standing with the back arched and weight on one hip, hands at the hips, looking over the shoulder", + "facing away on the knees and lengthening the spine tall, glancing back", + "draped face-down across a chaise with the ankles lifted and crossed", + "standing at a railing facing out over a view, looking back with a soft smile", + "turning away mid-step with the hair swinging, glancing back", + "seated facing away on a windowsill, one knee up, looking back over the shoulder", +] + +# Full pool used when no backside skew is requested. +EVOCATIVE_ALL = EVOCATIVE_POSES + BACKSIDE_POSES + +# Figure / curve emphasis -- an independent axis (like pose and expression) that +# describes a woman's silhouette with tasteful figure-drawing / fashion +# vocabulary (full bust, cinched/wasp waist, full rounded hips, peach-shaped +# rear) so the image model renders pronounced pin-up curves WITHOUT the crude +# anatomy words that trip its safety filters -- the same trick as the evocative +# poses. Applied to single women only; selected/weighted via --figure. Composes +# with any base body type (slim -> slim-thick, plus-size -> voluptuous, etc.). +FIGURE_CURVY = [ + "a full bust, a narrow waist, and full, rounded hips", + "a small, cinched waist and full hips", + "a full bust and a tiny waist", + "full, rounded hips and a peach-shaped lower silhouette", + "soft, heavy curves and a defined waist", + "a full bust and full, curvy hips", + "an ample bust and a snatched waist", + "a generous neckline and full, rounded hips", + "soft, natural fullness up top and a small waist", + "a full, soft bust and a narrow waist", + "wide, curvy hips and a slim waist", + "a rounded, peachy rear and a cinched waist", + "full hips, a small waist, and a soft, full bust", + "an opulent, voluptuous figure with a defined waist", + "a buxom upper figure and full, rounded hips", + "soft, full curves and a wasp waist", + "a heart-shaped lower silhouette and a slim waist", + "a full bust and a generously curved rear", + "a tiny waist framed by full hips and a full bust", + "lush, soft curves and a nipped-in waist", + "a full, rounded rear and a small waist", + "a soft, heavy bust and curvy hips", + "shapely, full hips and a defined waistline", + "a curvy silhouette with a full bust and peachy hips", + "an ample, soft bust and a narrow waistline", + "full, sculpted hips and a small waist", + "voluptuous curves with a cinched middle", + "a generous bust and a rounded, peach-shaped rear", + "soft fullness up top and full, rounded hips", + "a plush, curvy figure with a defined waist", + "full curves through the bust and hips with a slim waist", + "a soft, full chest and a snatched waist", + "a soft, rounded bust and full hips", + "a curvy waist-to-hip balance with a full bust", + "full, heavy curves and a slender waist", + "a deep neckline and a rounded, full rear", + "soft, generous curves with a trim waist", + "a full bust and softly rounded hips", + "a pinched waist between a full bust and full hips", + "lush hips and a soft, full bust", + "a curvy lower half with a tiny waist", + "a full, soft figure with a nipped waist", + "rounded, full hips and a soft bust", + "an ample neckline and a wasp waist", + "soft, full hips and a snatched waistline", + "a voluptuous bust and a slim, defined waist", + "full curves up top and a peach-shaped rear", + "a soft, heavy figure with a cinched waist", + "a full, rounded silhouette with a small waist", + "generous, soft curves and a defined middle", + "a full bust over a narrow ribcage and waist", + "wide, soft hips and a small, defined waist", + "a rounded rear and a soft, full neckline", + "a curvy hourglass-leaning silhouette with a slim waist", + "plush hips and a soft, ample bust", + "a soft, full bustline and curvy hips", + "a defined waist beneath full, soft curves", + "a peachy, rounded lower silhouette and a slim waist", + "a soft, curvy figure with a pinched waist and full hips", + "an ample, rounded bust and a tiny waist", + "full, soft hips and thighs with a slim waist", + "a curvy, soft silhouette with a generous neckline", + "soft, rounded curves with an hourglass-cinched waist", +] + +# Lithe / athletic / slender notes -- mixed in with --figure balanced so the +# dataset keeps some non-voluptuous variety. +FIGURE_ATHLETIC = [ + "a lithe, balanced figure with a defined waist", + "an athletic, toned figure with subtle curves", + "a slim, elegant silhouette", + "a toned midsection and gently curved hips", + "a lean figure with a small waist", + "a sleek, willowy silhouette", + "an athletic build with soft curves", + "a trim waist and toned, shapely legs", + "a graceful, slender figure", + "a fit, sculpted figure with a defined waist", + "a petite, balanced figure with gentle curves", + "a slim-thick figure with a small waist and soft hips", + "a toned, hourglass-leaning figure with a slim waist", + "an athletic, curvy-fit silhouette", + "a lean, dancer's figure with subtle curves", + "a sporty, toned figure with a defined waist", + "a slender frame with gently rounded hips", + "a lithe figure with a small waist and a soft bust", + "a fit, lightly hourglass silhouette", + "a willowy frame with a defined waistline", +] + +# Maximum-exaggerated subset (selected with --figure bombshell). +FIGURE_BOMBSHELL = [ + "a dramatic hourglass with a very full bust, a tiny waist, and full, rounded hips", + "an exaggerated bombshell figure with an ample bust, a wasp waist, and wide, curvy hips", + "a very full bust and a dramatically cinched waist", + "lush, voluptuous curves with a tiny waist and a full, peach-shaped rear", + "an opulent hourglass with a generous bust and full, rounded hips", + "a heavy, soft bust and a dramatically full, rounded rear", + "extreme hourglass proportions with a snatched waist and full curves", + "a very full neckline and wide, curvy hips around a tiny waist", + "a bombshell silhouette with a full bust, a nipped-in waist, and peachy hips", + "dramatic full curves through the bust and hips with a wasp waist", + "a generously full bust and an exaggerated, rounded rear", + "voluptuous, larger-than-life curves with a tightly cinched waist", + "an extreme hourglass with a heavy bust, a wasp waist, and very full hips", + "a dramatically voluptuous figure with a tiny cinched waist", + "a very full, soft bust and dramatically wide, curvy hips", + "an exaggerated peach-shaped rear and a snatched waist", + "lush, oversized curves with a tightly nipped waist", + "a heavy, full neckline and a dramatically rounded rear", + "extreme curves through the bust and hips around a tiny waist", + "a bombshell hourglass with a very full bust and peachy, full hips", + "dramatically full hips and a heavy bust over a wasp waist", + "an opulent, larger-than-life bust and a cinched waist", + "a voluptuous, exaggerated silhouette with a tiny waist and a full rear", + "a very full, rounded rear and a dramatically pinched waist", + "a heavy, soft bust and exaggeratedly wide hips with a slim waist", +] + +EXPRESSIONS = [ + "warm genuine smile", + "bright open-mouthed laugh", + "soft closed-lip smile", + "playful wink", + "flirtatious side glance", + "sultry half-lidded gaze", + "coy lowered-eyes look", + "confident raised eyebrow", + "mischievous smirk", + "surprised wide-eyed look with parted lips", + "dreamy faraway gaze", + "tender affectionate expression", + "teasing bitten-lip smile", + "serene calm expression with soft eyes", + "joyful beaming grin", + "thoughtful pensive look", + "sly knowing smile", + "inviting warm gaze toward the viewer", + "shy gentle smile with a faint blush", + "bold direct stare", + "amused giggle caught mid-laugh", + "relaxed contented expression", + "cheeky playful grin", + "soft pout", + "delighted smile with raised eyebrows", + "confident chin-up expression", + "wistful gaze", + "starry-eyed enchanted look", + "narrow-eyed teasing smirk", + "peaceful closed-eyes smile", + "curious tilted-head look", + "radiant happy expression", + "smoldering intense gaze", + "sweet dimpled smile", + "candid mid-conversation expression", + "nose-scrunching laugh", + "alluring glossy-lipped smile", + "daydreaming soft expression", + "spirited excited grin", + "composed elegant expression", + "blowing a playful kiss", + "biting the corner of the lip", + "winking with a grin", + "smize with softly parted lips", + "raised chin with a confident smirk", + "soft surprised gasp", + "content closed-eyes sigh", + "doe-eyed innocent look", + "knowing side-eye", + "warm crinkle-eyed smile", + "dreamy lip-parted gaze", + "flirty eyebrow flash", + "serene meditative gaze", + "delighted gasp with a hand near the cheek", + "mischievous stifled giggle", + "sultry over-the-glasses look", + "playful tongue-out grin", + "soft parted-lips look", + "confident closed-mouth smile with arched brows", + "languid heavy-lidded gaze", + "bright surprised smile", + "tender half-smile with soft eyes", + "coquettish glance through the lashes", + "serene faint smile with closed eyes", + "bold lifted-brow challenge", + "warm laughing eyes", + "dreamy upward gaze", + "sultry parted-lip stare", + "shy bitten-lip glance away", + "amused crooked half-smile", + "playful scrunched-nose smile", + "soft sigh with lowered lashes", + "radiant ear-to-ear grin", + "knowing arched-brow smirk", + "gentle adoring gaze", + "mischievous sidelong glance", + "calm confident gaze into the lens", + "delighted open-mouthed grin", + "wistful soft-eyed look", + "teasing pursed-lip smile", + "content dreamy half-smile", + "fierce narrowed-eye look", + "sweet hopeful gaze", + "relaxed sleepy smile", + "flirtatious raised-shoulder glance", + "quiet enigmatic smile", +] + +COMPOSITIONS = [ + "tight upper body portrait", + "waist-up portrait", + "mid-thigh portrait", + "three-quarter portrait", + "full body portrait", + "seated portrait", + "environmental portrait", + "low-angle full body shot", + "close-up beauty portrait", + "over-the-shoulder back view", + "dynamic dutch-angle portrait", + "wide environmental pin-up", + "extreme close-up on the face", + "full-length mirror reflection shot", + "high-angle looking-down portrait", + "profile portrait", + "cropped torso pin-up frame", + "knee-up three-quarter portrait", + "from-below hero angle full body", + "intimate close crop on the shoulders and face", + "reclining full-length composition", + "diagonal across-the-frame pin-up", + "mirror-and-subject double framing", + "over-the-hip rear three-quarter view", + "wide negative-space environmental shot", + "soft-focus foreground framing the subject", + "centered symmetrical full body portrait", + "candid off-center snapshot framing", + "tight crop from hips to shoulders", + "low water-level poolside angle", +] + +# Keywords that indicate a pose already includes a prop — skip PROPS sampling when matched +_POSE_PROP_KEYWORDS = { + "banana", "cucumber", "corn cob", "popsicle", "eggplant", "zucchini", + "carrot", "cigar", "hot dog", "breadstick", "churro", "candy cane", + "pool cue", "wine bottle", "microphone", "baseball bat", "bratwurst", + "lollipop", "asparagus", "ice lolly", "ice cream", "drumstick", "straw", + "cigarette", +} + + +def _pose_has_prop(pose_text: str) -> bool: + low = pose_text.lower() + return any(kw in low for kw in _POSE_PROP_KEYWORDS) + + +PROPS = [ + "holding a coffee mug", + "holding a bouquet", + "holding a wine glass", + "holding a banana", + "holding a milkshake", + "holding a paintbrush", + "holding a towel", + "holding a small clutch purse", + "holding a book", + "biting into a ripe banana", + "holding a cucumber", + "pressing a popsicle to the lips", + "holding a cold drink", + "holding a corn cob", + "holding a camera", + "holding a vinyl record", + "holding a zucchini", + "holding a carrot", + "draped in a sheer scarf", + "holding a feather boa", + "holding a cocktail glass", + "holding a hand mirror", + "adjusting a stocking", + "holding a lace fan", + "wrapped in a silk sheet", + "holding a champagne flute", + "holding a martini glass", + "twirling a strand of hair", + "holding a polaroid camera", + "holding a champagne bottle", + "draped in string fairy lights", + "licking an ice lolly", + "holding a lollipop", + "resting sunglasses on the head", + "holding a beach ball", + "carrying a surfboard", + "holding a tropical cocktail with a paper umbrella", + "hugging a fluffy pillow", + "holding a glass of champagne to the lips", + "holding a single rose", + "trailing a feather along the collarbone", + "holding a silk ribbon", + "wrapped loosely in a towel", + "holding a string of pearls", + "holding a large eggplant", + "holding a cocktail with a cherry", + "balancing a sun hat on the hip", + "holding a teacup and saucer", + "draped in a fur stole", + "holding a lit candle", + "holding a glass of red wine", + "holding a vintage telephone receiver", + "holding a cigar", + "holding a lollipop to the lips", + "twirling a parasol", + "nibbling the end of a churro", + "peeling a banana", + "holding an ice cream cone", + "holding a perfume bottle", + "holding a long unlit cigarette holder", + "holding a wide-brim sun hat", + "holding a chilled bottle against the skin", + "holding a breadstick to the lips", + "sliding a finger along a banana", + "holding a glass of iced lemonade", + "holding an asparagus spear", + "biting the tip of a carrot", + "holding a hot dog", + "licking whipped cream off a finger", + "holding a candy cane to the lips", + "dragging a finger along a corn cob", + "sucking on a straw", + "holding a bratwurst", + "biting into a popsicle", + "pressing a microphone to the lips", + "holding a baseball bat over one shoulder", + "gripping a pool cue", + "holding a wine bottle by the neck", + "pressing a lipstick to the lips", + "holding a drumstick between the fingers", +] + +COUPLE_TYPES = [ + ("two women", "two women", "close affectionate couple pose"), + ("two men", "two men", "relaxed romantic couple pose"), + ("woman and man", "a woman and a man", "playful date-night pose"), +] + +COUPLE_AGES = [ + "21-year-old adults", + "22-year-old adults", + "23-year-old adults", + "24-year-old adults", + "25-year-old adults", + "early 20s adults", + "early and mid-20s adults", + "mid-20s adults", + "late 20s and 30s adults", + "30s and 40s", + "26-year-old adults", + "late 20s adults", + "early 30s adults", + "mid-20s and early 30s adults", + "27 and 29-year-old adults", + "early-to-mid 20s adults", +] + +COUPLE_OUTFITS = [ + "stylish evening outfits with fitted silhouettes and soft fabric folds", + "cozy off-shoulder knitwear and an open-collar shirt", + "date-night cocktail wear with warm satin highlights", + "casual resort outfits with relaxed adult glamour", + "tailored party clothes with playful accessories", + "matching lingerie-inspired loungewear with sheer robes", + "beachwear with a bikini and an open shirt", + "a silk slip and an unbuttoned shirt for an intimate evening", + "coordinated satin robes loosely tied", + "a backless gown and a tailored open-collar shirt", + "matching evening wear with plunging necklines", + "a slip dress and a half-buttoned dress shirt", + "festival outfits with a bralette and an open shirt", + "loungewear with a camisole and low-slung joggers", + "a corset top and a fitted waistcoat", + "resort chic with a sarong and linen trousers", +] + +# --------------------------------------------------------------------------- +# "minimal" clothing mode: every pool is restricted to the little-coverage, +# lingerie/swimwear/bralette register that the user confirmed renders well, +# while still staying tasteful and non-explicit. Selected via --clothing minimal. +# --------------------------------------------------------------------------- +WOMEN_CLOTHES_MINIMAL = [ + # Confirmed pass-throughs — kept exactly as the image model accepted them + "fishnet stockings with a wet-look bodysuit", + "wet-look bikini with metallic O-ring details", + "satin robe falling open over a matching bra and high-leg briefs", + "crochet bikini with wooden beads", + "knit crop sweater slipping off one shoulder over high-cut briefs", + "micro cardigan buttoned once over a lace bra and briefs", + "tied flannel over a bikini top with denim cut-offs", + "sheer maxi skirt over bikini bottoms with a crop top", + "corset-laced top with a flowing maxi skirt and a high slit", + "satin slip dress with thin straps and a low open back", + "lace-panel cocktail dress with a sheer midriff", + "deep-V wrap dress with a thigh-high slit", + "cropped halter top with deep cleavage and high-cut denim shorts", + "draped one-shoulder gown with a thigh slit", + "micro cardigan buttoned once over a lace bra and mini skirt", + # Vocabulary-fixed entries (high-risk terms substituted per image-model research) + "triangle bikini top with adjustable ties and a sarong tied low on the hips", + "oversized unbuttoned shirt over a lace bra set", + "lace suspender belt with lace-top thigh-highs and a fitted satin bodice", + "matching lace bra and briefs under a semi-sheer robe", + "semi-sheer babydoll slip over a matching lace bra set", + "lace bralette with high-waisted suspender shorts", + "semi-sheer mesh cover-up over a triangle bikini", + "velvet bralette with matching tap shorts and suspender-inspired straps", + "lace bodysuit under a semi-sheer robe slipping open", + "satin slip chemise with thin straps", + "balconette bra with matching suspender straps and stockings", + "corset bustier with a suspender belt and stockings", + "mesh-panel bodysuit layered over a bikini", + "deep-V bodysuit with a mesh-panel overlay", + "lace-trim bodysuit with a deep-V neckline", + "scalloped-lace bra and suspender set with lace-top thigh-highs", + "off-shoulder corset crop with suspender straps and stockings", + "semi-sheer lace bodystocking", + "satin bustier with detachable suspender straps", + "lace bralette under a cropped semi-sheer blouse", + "silk bodysuit with a deep-V lace front", + "sheer patterned bodystocking under an open kimono", + "satin bodysuit with a deep cowl back", + "semi-sheer lace catsuit over a bra and briefs", + "halterneck deep-V swimsuit with a keyhole front", + "high-leg cheeky bodysuit in mesh-panel fabric", + "lace-trimmed silk robe falling open over a bra and high-cut briefs", + "tie-side bikini with a long semi-sheer beach skirt", + "cut-out monokini with a deep-V keyhole and high-cut legs", + "semi-sheer mesh slip dress over a bikini", + "lace bodysuit with an open back and high-cut legs", + "satin bra with suspender straps and lace-top stay-up stockings", + "halter monokini with a deep-V to the waist", + "lace bodysuit with snap closures", + "minimal triangle bikini with adjustable sliding ties", + "deep-V halter swimsuit cinched at the waist", + "lace bralette with a semi-sheer wrap mini skirt", + "semi-sheer babydoll with a deep-V lace neckline", + "satin slip robe loosely belted over a lace bra set", + "underbust corset over a semi-sheer bra and briefs", + "bandeau bikini top with a semi-sheer sarong knotted at the hip", + "metallic tie-side bikini with a semi-sheer cover-up", + "deep-V bra and suspender set under a cropped semi-sheer cardigan", + "mesh-panel bralette with matching cheeky briefs", + "tie-side monokini with deep side cut-outs", + "lace bodysuit with a halter neckline and high-cut legs", + "semi-sheer lace robe over a bandeau and briefs", + "bodysuit with a deep-V zip front and high-cut legs", + "satin-and-lace bustier with attached suspender straps", + "semi-sheer kimono robe open over a matching lace bra set", + "boudoir corset with suspender straps and stockings", + "lace bra and high-cut briefs under a draped silk wrap", + "tie-side bikini with a draped hip-chain detail", + "unbuttoned denim shirt knotted above a triangle bikini", + "babydoll dress with semi-sheer panels and a lace hem", + "sequined bikini with a semi-sheer sarong", + "tie-front crop top over tie-side bikini bottoms", + "off-shoulder lace bodysuit with semi-sheer long sleeves", + # Safe unchanged entries + "halterneck monokini with cut-out sides", + "high-cut one-piece swimsuit with a deep-V front", + "athletic sports bra with a bare midriff and tiny shorts", + "feathered burlesque corset with opera gloves and stockings", + "metallic bandeau with high-waisted festival shorts", + "triangle bikini top with high-cut bottoms", + "silk camisole with delicate lace trim and boy shorts", + "off-shoulder bodysuit with high-cut legs", + "halter bikini with a semi-sheer wrap sarong", + "ribbed crop tank with a bare midriff and tiny shorts", + "strappy caged bralette with matching high-waisted briefs", + "sporty zip-front bikini", + "wrap-front bikini top with a tied sarong skirt", + "satin slip nightie with lace cups", + "cropped mesh long-sleeve over a bandeau and briefs", + "leather harness bralette with high-cut shorts", + "sporty crop bralette with cheeky shorts", + "off-shoulder mesh top over a bikini", + "neon triangle bikini", + "cropped tank tied up high with shorts", + "bandeau top with a low-slung wrap skirt", + "satin cami and tap-short set with lace edging", + "cropped tube top with matching low-rise booty shorts", + "halter-neck bodysuit with a bare back and high legs", + "tie-side bikini bottoms with a cropped mesh tee", + "lace high-cut bodysuit under an open silk blouse", + "halter crop top over high-cut swim bottoms", + "satin bralette with a wrap-tie front and tap shorts", + "delicate slip dress with a thigh-high slit and thin straps", + "cropped knit halter over bikini bottoms", + "tie-front bikini top with ruched high-cut bottoms", + "tiny ribbed bralette with matching micro shorts", + "mesh cover-up dress over a matching bra and briefs", + "underwire bikini top with a beaded hip chain and bottoms", + "crochet halter top with matching tie-side bottoms", + "delicate chemise slipping off both shoulders", + "ruffled halter bikini with side ties", + "wet white tank top clinging over a bikini", + "bandeau bikini with side-tie bottoms", + "halter monokini with a deep side cut-out", + "lace mini slip that ends high on the thigh", + "thin-strap silk slip clinging to the figure", + "boudoir robe open over a lace bra and tap shorts", + "lace-up leather bra top with matching briefs", +] + + +def _expand_women_minimal_clothes() -> list[str]: + additions: list[str] = [] + minimal_bases = ( + "triangle bikini", + "bandeau bikini", + "underwire bikini", + "halter bikini", + "crochet bikini", + "metallic tie-side bikini", + "lace bodysuit", + "satin bodysuit", + "mesh-panel bodysuit", + "lace teddy", + "satin slip chemise", + "corset bustier", + "balconette bra set", + "lace bralette set", + "halter monokini", + "high-leg swimsuit", + "plunging one-piece swimsuit", + "strappy caged bralette set", + ) + minimal_details = ( + "with lace-top thigh-highs", + "with a low-tied semi-sheer sarong", + "under an open silk robe", + "under a semi-sheer kimono robe", + "with a sheer mesh cover-up", + "with detachable suspender straps", + "with a draped silk wrap", + "with a cropped mesh tee", + "with a loose unbuttoned linen shirt", + "with opera gloves and stockings", + "with a feather-trimmed robe", + "with a beaded hip-chain detail", + ) + for base in minimal_bases: + for detail in minimal_details: + additions.append(f"{base} {detail}") + + bralette_tops = ( + "lace bralette", + "satin bralette", + "mesh-panel bralette", + "rhinestone bralette", + "velvet bralette", + "crochet halter bralette", + "strappy underwire bralette", + "bandeau top", + ) + minimal_bottoms = ( + "high-cut briefs", + "lace tap shorts", + "cheeky shorts", + "high-waisted briefs", + "tie-side bikini bottoms", + "tiny swim shorts", + "low-rise satin shorts", + "a semi-sheer wrap mini skirt", + ) + for top in bralette_tops: + for bottom in minimal_bottoms: + additions.append(f"{top} with {bottom}") + + additions.extend( + [ + "lace bra and high-cut briefs under an oversized shirt falling off one shoulder", + "deep-V mesh bodysuit with lace-top stockings", + "satin plunge bodysuit with a low open back", + "scalloped lace teddy with a sheer robe", + "cut-out monokini with a low-tied beach wrap", + "minimal resort bikini with a transparent-effect mesh skirt", + "semi-sheer lace catsuit layered over a bandeau and briefs", + "silk slip chemise with a feather-trimmed hem", + "open satin robe over a balconette bra and matching briefs", + "lace-up corset bustier with high-cut briefs and stockings", + "halter swimsuit with high-cut legs and a deep keyhole front", + "cropped wet-look bustier with matching briefs", + "crochet bikini with a draped hip scarf", + "tie-front bikini top with tiny ruched shorts", + "sheer black cover-up over a metallic bikini", + "lace bodysuit under an open cropped denim shirt", + "satin bra set with a garter belt and stockings", + "mesh wrap dress over a high-leg swimsuit", + "plunging bodysuit with a sheer maxi cover-up", + "strappy bikini with a long open beach shirt", + "soft ribbed bralette with matching high-cut shorts", + "minimal bandeau swim set with a side-tied sarong", + "lace chemise with semi-sheer panels and thin straps", + "deep-V one-piece swimsuit with a loose robe", + "corset-laced swimsuit with high-cut legs", + "silk cami and tap shorts under a translucent robe", + "underwire bikini with lace-up side details", + "velvet bra set with suspender straps and stockings", + "semi-sheer babydoll over a high-leg bra set", + "open kimono over a lace teddy and thigh-highs", + ] + ) + return additions + + +_extend_unique(WOMEN_CLOTHES_MINIMAL, _expand_women_minimal_clothes()) + +MEN_CLOTHES_MINIMAL = [ + "swim shorts with a towel over one shoulder, shirtless", + "open tropical shirt over a bare chest with board shorts", + "unbuttoned linen shirt revealing a toned chest with rolled trousers", + "fitted swim briefs by the pool, shirtless", + "leather jacket over a bare chest with dark jeans", + "sleeveless gym shirt pushed up with athletic shorts", + "wetsuit peeled to the waist on a beach", + "low-slung joggers with a bare chest and a towel", + "soft resort shirt open over a bare chest with swim shorts", + "fitted tank top with low-slung shorts", + "board shorts with a surfboard, shirtless", + "loose robe open over swim trunks", + "swim briefs poolside, shirtless", + "unbuttoned silk shirt over a bare chest", + "gym shorts with a sweat towel, shirtless", + "open bathrobe over swim trunks", + "linen trousers rolled up, shirtless on a beach", + "tank top damp after a workout", + "fitted swim briefs dripping wet after a swim", + "open robe revealing a bare chest, loosely belted", + "low-slung towel wrapped at the waist, shirtless", + "athletic shorts rolled low with a bare torso", + "unbuttoned denim shirt over a bare chest with swim shorts", + "snug boxer briefs with an open shirt", + "wet board shorts on the beach, shirtless", + "open linen shirt blowing in the breeze over swim trunks", + "compression shorts after a workout, shirtless", + "loose drawstring trousers worn low, bare chest", + "swim shorts with a towel around the neck, shirtless", + "half-zipped wetsuit pushed off the shoulders", +] + +COUPLE_OUTFITS_MINIMAL = [ + "matching beachwear with a bikini and swim shorts", + "lingerie-inspired loungewear with sheer robes", + "a lace bra set and an open shirt for an intimate evening", + "poolside swimwear with playful accessories", + "a silk slip with a bare-chested partner in low-slung trousers", + "matching satin sleepwear with delicate lace trim", + "a bikini and board shorts at the beach", + "matching robes loosely tied", + "his-and-hers swimwear by the pool", + "a lace lingerie set and boxer briefs", + "a bikini and an open beach shirt", + "a sheer chemise and boxer briefs", + "matching swimwear at the poolside", + "a string bikini and swim briefs", + "a lace teddy and a loosely tied robe", + "satin lingerie and an open shirt", + "a triangle bikini and low-slung board shorts", + "matching sheer robes over lingerie", + "a bralette set and a bare-chested partner with a towel", + "a monokini and swim trunks by the water", +] + +GROUP_SCENES = [ + ("rooftop", "rooftop party with city lights and warm string lights"), + ("nightclub", "soft nightclub lights and blurred dance floor"), + ("hotel_lobby", "hotel lobby with brass lights and polished stone"), + ("beach_bar", "beach bar at golden sunset"), + ("gallery", "quiet gallery opening with soft spotlights"), + ("garden_party", "garden party with lanterns and flowers"), + ("pool_party", "poolside party with sun loungers and cocktails"), + ("rooftop_pool", "rooftop infinity pool at night with city glow"), + ("yacht_party", "luxury yacht party at golden hour"), + ("beach_volleyball", "sunny beach volleyball court"), +] + +GROUP_COMPOSITIONS = ["group portrait", "party scene", "environmental group portrait"] + +GROUP_AGES = ["21-25 adult-heavy", "early to late 20s adults", "mid-20s to 30s adults"] + +LAYOUTS_FULL = [ + ("polaroid_selfie", "Polaroid selfie layout with two adult friends posing close together"), + ("real_estate_leaflet", "fictional real estate leaflet with a glamorous adult realtor holding keys"), + ("dance_guide", "three-pose dance guide sheet with a fully clothed adult dancer"), + ("cocktail_menu", "fictional cocktail menu cover with a glamorous adult host holding a glass"), + ("travel_postcard", "vintage travel postcard layout with a stylish adult traveler"), + ("comic_panel", "single comic panel with an adult couple sharing a playful glance"), + ("pinup_calendar", "vintage pin-up calendar page with a glamorous adult model"), + ("lingerie_catalog", "stylish lingerie catalog page with a confident adult model"), + ("fashion_editorial", "glossy fashion-editorial spread with a glamorous adult model"), + ("perfume_ad", "elegant perfume advertisement layout with a glamorous adult model"), + ("vintage_pinup_poster", "retro pin-up poster layout with a glamorous adult model"), + ("magazine_cover", "stylish magazine cover with a confident adult cover model"), + ("noir_comic_page", "noir comic page with a glamorous adult femme-fatale character"), + ("boudoir_album", "boudoir photo album page with an elegant adult model"), +] + +LAYOUTS_MINIMAL = [ + ("lingerie_catalog", "stylish lingerie catalog page with a confident adult model"), + ("pinup_calendar", "vintage pin-up calendar page with a glamorous adult model in lingerie"), + ("swimwear_lookbook", "swimwear lookbook page with a confident adult model in a bikini"), + ("boudoir_postcard", "boudoir-style postcard layout with a glamorous adult model in lingerie"), + ("comic_panel", "single comic panel with an adult pin-up in lingerie sharing a playful glance"), + ("beach_poster", "retro beach poster layout with a glamorous adult model in a bikini"), + ("swimwear_cover", "swimwear magazine cover with a confident adult model in a bikini"), + ("boudoir_series", "boudoir photography series with an adult model in lingerie"), + ("pinup_card", "pin-up playing card with a glamorous adult model in lingerie"), + ("bikini_calendar", "bikini calendar page with a glamorous adult model"), + ("lingerie_lookbook", "lingerie lookbook spread with a confident adult model"), + ("boudoir_magazine_cover", "boudoir magazine cover with a glamorous adult model in lingerie"), + ("swimwear_poster", "retro swimwear poster with a confident adult model in a bikini"), + ("pinup_poster_lingerie", "vintage pin-up poster with a glamorous adult model in lingerie"), + ("beach_lookbook", "beachwear lookbook page with an adult model in a bikini"), + ("boudoir_postcard_set", "boudoir postcard set with an elegant adult model in lingerie"), +] + + +def slugify(text: str) -> str: + text = text.lower() + text = re.sub(r"[^a-z0-9]+", "_", text) + return text.strip("_")[:48] + + +def choose(rng: random.Random, items: list): + return items[rng.randrange(len(items))] + + +def batch_clothing_modes(rng: random.Random, default: str, minimal_ratio: float | None) -> list[str]: + if minimal_ratio is None: + return [default] * BATCH_SIZE + minimal_count = round(BATCH_SIZE * minimal_ratio) + modes = ["minimal"] * minimal_count + ["full"] * (BATCH_SIZE - minimal_count) + rng.shuffle(modes) + return modes + + +def batch_single_pose_modes(rng: random.Random, default: str, standard_ratio: float | None, count: int) -> list[str]: + if standard_ratio is None: + return [default] * count + standard_count = round(count * standard_ratio) + modes = ["standard"] * standard_count + ["evocative"] * (count - standard_count) + rng.shuffle(modes) + return modes + + +def batch_category_quotas() -> list[tuple[str, int]]: + woman_count = round(BATCH_SIZE * 0.72) + man_count = round(BATCH_SIZE * 0.12) + couple_count = round(BATCH_SIZE * 0.12) + group_count = BATCH_SIZE - woman_count - man_count - couple_count + return [ + ("woman", woman_count), + ("man", man_count), + ("couple", couple_count), + ("group_or_layout", group_count), + ] + + +def article(phrase: str) -> str: + return "an" if phrase[:1].lower() in "aeiou" else "a" + + +# Heritage keyword sets for the --ethnicity filter (matched against the skin +# field). Include both broad labels and regional names so mixed-heritage entries +# like "Japanese Latina" or "Filipina European" are included in Asian modes even +# when they do not repeat the broad East/Southeast/South Asian label. +ASIAN_KEYWORDS = ( + "east asian", + "southeast asian", + "south asian", + "central asian", + "japanese", + "korean", + "chinese", + "taiwanese", + "mongolian", + "tibetan", + "manchu", + "vietnamese", + "thai", + "filipina", + "filipino", + "indonesian", + "malay", + "cambodian", + "lao", + "burmese", + "singaporean", + "hmong", + "balinese", + "indian", + "punjabi", + "tamil", + "bengali", + "sri lankan", + "nepali", + "pakistani", + "gujarati", + "bangladeshi", + "malayali", + "kashmiri", + "kazakh", + "uzbek", + "kyrgyz", + "uighur", + "asian mixed", +) +WHITE_KEYWORDS = ( + "fair", + "ivory", + "porcelain", + "pale", + "scandinavian", + "rosy", + "freckled", + "beige", + "tanned", + "european", + "nordic", + "celtic", + "slavic", + "baltic", + "alpine", + "balkan", + "irish", + "french", + "mediterranean", +) + + +def by_ethnicity(pool: list, ethnicity: str) -> list: + """Filter an appearance pool by heritage keywords found in the skin field. + 'asian' = East/Southeast/South/Central Asian; 'white_asian' = white/European + Asian; + 'any' returns the full pool.""" + if ethnicity == "any": + return pool + if ethnicity == "asian": + kws = ASIAN_KEYWORDS + elif ethnicity == "white_asian": + kws = WHITE_KEYWORDS + ASIAN_KEYWORDS + else: + kws = (ethnicity,) + filtered = [e for e in pool if any(k in e[3].lower() for k in kws)] + return filtered or pool + + +def without_plus(pool: list) -> list: + """Drop plus-size appearance entries (for the --no-plus-women filter).""" + filtered = [e for e in pool if "plus" not in e[2].lower()] + return filtered or pool + + +def without_black(pool: list) -> list: + """Drop Black/African appearance entries (for the women-focused --no-black filter). Keyed on + the 'African' tag in the skin field; ambiguous 'warm/deep brown skin' (mixed) + entries are intentionally left in since they are not Black-coded.""" + filtered = [e for e in pool if "african" not in e[3].lower()] + return filtered or pool + + +def figure_pool(mode: str) -> list: + """Curve-emphasis vocabulary for single women, selected by --figure mode.""" + if mode == "bombshell": + return FIGURE_BOMBSHELL + if mode == "balanced": + return FIGURE_CURVY + FIGURE_ATHLETIC + return FIGURE_CURVY # 'curvy' default: voluptuous-leaning mix + + +def choose_woman(rng: random.Random, ethnicity: str = "any", no_plus: bool = False, no_black: bool = False): + young = by_ethnicity(YOUNG_WOMEN, ethnicity) + mature = by_ethnicity(MATURE_WOMEN, ethnicity) + if no_plus: + young = without_plus(young) + mature = without_plus(mature) + if no_black: + young = without_black(young) + mature = without_black(mature) + if rng.random() < 0.84: + return choose(rng, young) + return choose(rng, mature) + + +class ExpressionDeck: + """Draws facial expressions without replacement until the deck empties, then + reshuffles -- guarantees an even spread of expressions across the dataset.""" + + def __init__(self, items: list, rng: random.Random) -> None: + self.items = list(items) + self.rng = rng + self.pool: list = [] + + def draw(self) -> str: + if not self.pool: + self.pool = list(self.items) + self.rng.shuffle(self.pool) + return self.pool.pop() + + def draw_two(self) -> tuple[str, str]: + first = self.draw() + second = self.draw() + if second == first: + second = self.draw() + return first, second + + +def row_base(index: int, batch: int, subject: str, age: str, body: str, scene_slug: str, composition: str) -> dict: + short = slugify(f"{subject}_{age}_{body}_{scene_slug}_{composition}") + return { + "id": f"sxcp_{index:04d}", + "file_name": f"images/sxcp_{index:04d}_{short}.png", + "batch": f"batch_{batch:03d}", + "primary_subject": subject, + "age_band": age, + "body_type": body, + "scene": scene_slug, + "composition": composition, + "negative_prompt": NEGATIVE_PROMPT, + } + + +def make_single(index: int, batch: int, rng: random.Random, gender: str, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False) -> dict: + minimal = clothing == "minimal" + if gender == "woman": + subject, age, body, skin, hair, eyes = choose_woman(rng, ethnicity, no_plus, no_black) + clothes = choose(rng, WOMEN_CLOTHES_MINIMAL if minimal else WOMEN_CLOTHES) + figure_note = choose(rng, figure_pool(figure)) + else: + # The ethnicity bias targets women; men stay any-heritage unless the + # batch is a fully-themed 'asian' batch. + men_eth = ethnicity if ethnicity == "asian" else "any" + men_pool = by_ethnicity(MEN, men_eth) + subject, age, body, skin, hair, eyes = choose(rng, men_pool) + clothes = choose(rng, MEN_CLOTHES_MINIMAL if minimal else MEN_CLOTHES) + figure_note = "" + body_phrase = f"{body} figure with {figure_note}" if figure_note else f"{body} figure" + + scene_slug, scene = choose(rng, SCENES) + if poses == "evocative": + if backside_bias > 0: + if rng.random() < backside_bias: + pool = BACKSIDE_POSES + pose_mode = "evocative_backside" + else: + pool = EVOCATIVE_POSES + pose_mode = "evocative" + else: + pool = EVOCATIVE_ALL + pose_mode = "evocative" + pose = choose(rng, pool) + if backside_bias <= 0 and pose in BACKSIDE_POSES: + pose_mode = "evocative_backside" + else: + pose = choose(rng, POSES) + pose_mode = "standard" + composition = choose(rng, COMPOSITIONS) + prop = "" if _pose_has_prop(pose) else ("" if rng.random() < 0.30 else choose(rng, PROPS)) + expression = expr_deck.draw() + + row = row_base(index, batch, subject, age, body, scene_slug, composition) + row["expression"] = expression + row["clothing_mode"] = clothing + row["pose_mode"] = pose_mode + if figure_note: + row["figure"] = figure_note + prop_caption = f", {prop}" if prop else "" + row["caption"] = ( + f"{TRIGGER}, {subject}, {age}, {body_phrase}, {skin}, {hair}, {eyes}, " + f"{pose}, {expression}, {clothes}{prop_caption}, {scene}, {composition}, coloured pencil comic illustration, " + "crisp linework, hatching, soft pastel palette, warm sensual lighting, textured paper" + ) + prop_prompt = f"Prop/detail: {prop}. " if prop else "" + row["prompt"] = ( + f"A {subject}: sexy but tasteful adult pin-up " + f"coloured-pencil comic illustration, {age}, {body_phrase}, {skin}, {hair}, {eyes}. " + f"Scene: {scene}. Pose: {pose}. Facial expression: {expression}. Clothing: {clothes}, fashion editorial styling. {prop_prompt}" + f"Composition: vertical {composition}. Use crisp clean comic linework, detailed hatching, " + "soft blended shading, pastel skin tones, muted blues and pinks, warm sensual lighting, " + "and tactile textured paper. " + f"Avoid: {NEGATIVE_PROMPT}." + ) + return row + + +def make_couple(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False) -> dict: + primary_subject, subject_phrase, pose = choose(rng, COUPLE_TYPES) + if ethnicity == "asian": + subject_phrase = { + "two women": "two Asian women", + "two men": "two Asian men", + "a woman and a man": "an Asian woman and an Asian man", + }.get(subject_phrase, subject_phrase) + elif ethnicity == "white_asian": + subject_phrase = { + "two women": "two women, both white or East Asian", + "two men": "two men", + "a woman and a man": "a white or East Asian woman and a man", + }.get(subject_phrase, subject_phrase) + scene_slug, scene = choose(rng, SCENES) + composition = choose(rng, ["two-person portrait", "waist-up couple portrait", "full body couple pose", "seated couple portrait"]) + ages = choose(rng, COUPLE_AGES) + body_options = ["slim and average", "curvy and broad", "plus-size and average", "fat and slim", "stocky and curvy", "average and dad bod"] + if no_plus: + body_options = [b for b in body_options if "plus" not in b and "fat" not in b] + body = choose(rng, body_options) + outfits = choose(rng, COUPLE_OUTFITS_MINIMAL if clothing == "minimal" else COUPLE_OUTFITS) + expr_a, expr_b = expr_deck.draw_two() + + row = row_base(index, batch, primary_subject, ages, body, scene_slug, composition) + row["expression"] = f"{expr_a}; {expr_b}" + row["clothing_mode"] = clothing + row["caption"] = ( + f"{TRIGGER}, {subject_phrase}, ages {ages}, {body} body types, {pose}, " + f"one with {article(expr_a)} {expr_a} and the other with {article(expr_b)} {expr_b}, {outfits}, " + f"{scene}, {composition}, coloured pencil comic illustration, crisp linework, hatching, " + "soft pastel palette, warm sensual lighting, textured paper" + ) + row["prompt"] = ( + f"{subject_phrase.capitalize()}: sexy but tasteful adult pin-up " + f"coloured-pencil comic illustration. Ages: {ages}. Body types: {body}. Scene: {scene}. " + f"Pose: {pose}, affectionate and flirtatious but non-explicit. " + f"Facial expressions: one with {article(expr_a)} {expr_a}, the other with {article(expr_b)} {expr_b}. Clothing: {outfits}, resort styling. " + f"Composition: vertical {composition}. Use crisp comic linework, detailed hatching, " + "soft pastel colours, warm sensual lighting, and tactile textured paper. " + f"Avoid: {NEGATIVE_PROMPT}." + ) + return row + + +def make_group_or_layout(index: int, batch: int, rng: random.Random, expr_deck: ExpressionDeck, clothing: str = "full", ethnicity: str = "any", no_plus: bool = False) -> dict: + minimal = clothing == "minimal" + group_outfits = "minimal beachwear and lingerie-inspired outfits" if minimal else "stylish revealing party outfits" + if ethnicity == "asian": + eth, women_note = "Asian ", "" + elif ethnicity == "white_asian": + eth, women_note = "", " with the women white or East Asian" + else: + eth, women_note = "", "" + body_types_text = ("slim, average, curvy, athletic, and toned" if no_plus + else "slim, average, curvy, broad, fat, and plus-size") + is_group = rng.random() < 0.65 + if is_group: + scene_slug, scene = choose(rng, GROUP_SCENES) + composition = choose(rng, GROUP_COMPOSITIONS) + subject = f"mixed {eth}adult group" + age = choose(rng, GROUP_AGES) + body = "diverse" + scene_text = scene + expr_a, expr_b = expr_deck.draw_two() + row = row_base(index, batch, subject, age, body, str(scene_slug), composition) + row["expression"] = f"{expr_a}; {expr_b}" + row["clothing_mode"] = clothing + row["caption"] = ( + f"{TRIGGER}, mixed {eth}adult group of women and men{women_note}, ages {age}, diverse body types, " + f"{group_outfits}, a lively mix of expressions from {article(expr_a)} {expr_a} to {article(expr_b)} {expr_b}, " + f"{scene_text}, {composition}, coloured pencil comic illustration, crisp linework, hatching, " + "soft pastel palette, warm sensual lighting, textured paper" + ) + row["prompt"] = ( + f"A mixed {eth}adult group of women and men{women_note}: sexy but tasteful " + f"adult pin-up coloured-pencil comic illustration, ages {age}, diverse body types including " + f"{body_types_text}. Scene: {scene_text}. Clothing: {group_outfits}, resort styling. " + f"Facial expressions: a lively mix including {article(expr_a)} {expr_a} and {article(expr_b)} {expr_b}. " + f"Composition: vertical {composition}. Use crisp linework, hatching, soft pastel palette, " + f"warm sensual lighting, and textured paper. Avoid: {NEGATIVE_PROMPT}." + ) + return row + + layout_slug, layout_desc = choose(rng, LAYOUTS_MINIMAL if minimal else LAYOUTS_FULL) + if ethnicity == "asian": + layout_desc = layout_desc.replace("adult", "Asian adult") + elif ethnicity == "white_asian": + layout_desc = layout_desc.replace("adult", "white or East Asian adult") + expression = expr_deck.draw() + subject = "layout scene" + age = "adult" + body = "varied" + composition = "designed illustration layout" + row = row_base(index, batch, subject, age, body, layout_slug, composition) + row["expression"] = expression + row["clothing_mode"] = clothing + row["caption"] = ( + f"{TRIGGER}, {layout_desc} with {article(expression)} {expression}, sexy tasteful adult pin-up styling, clean designed layout, " + "coloured pencil comic illustration, crisp linework, hatching, soft pastel palette, warm sensual lighting, " + "textured parchment paper" + ) + row["prompt"] = ( + f"{layout_desc.capitalize()}: sexy but tasteful adult pin-up " + "coloured-pencil comic illustration, adults only, clean graphic composition, " + "pastel palette, crisp comic linework, detailed hatching, warm sensual lighting, tactile parchment texture. " + f"Facial expression: {expression}. " + f"Avoid: {NEGATIVE_PROMPT}. Use no readable text unless the layout naturally needs small decorative placeholder marks." + ) + return row + + +def build_rows(total: int, start_index: int, clothing: str = "full", ethnicity: str = "any", poses: str = "standard", backside_bias: float = 0.0, figure: str = "curvy", no_plus: bool = False, no_black: bool = False, minimal_clothing_ratio: float | None = None, standard_pose_ratio: float | None = None, seed: int = DEFAULT_RNG_SEED, expression_seed: int = EXPRESSION_SEED) -> list[dict]: + rng = random.Random(seed) + expr_deck = ExpressionDeck(EXPRESSIONS, random.Random(expression_seed)) + rows: list[dict] = [] + batch_quotas = batch_category_quotas() + if sum(count for _, count in batch_quotas) != BATCH_SIZE: + raise ValueError("batch quotas must match batch size") + single_subject_count = sum(count for category, count in batch_quotas if category in ("woman", "man")) + + batch_count = total // BATCH_SIZE + index = start_index + for batch in range(1, batch_count + 1): + batch_rows: list[dict] = [] + clothing_modes = batch_clothing_modes(rng, clothing, minimal_clothing_ratio) + single_pose_modes = batch_single_pose_modes(rng, poses, standard_pose_ratio, single_subject_count) + for category, count in batch_quotas: + for _ in range(count): + row_clothing = clothing_modes.pop() + if category == "woman": + row_pose = single_pose_modes.pop() + row = make_single(index, batch, rng, "woman", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black) + elif category == "man": + row_pose = single_pose_modes.pop() + row = make_single(index, batch, rng, "man", expr_deck, row_clothing, ethnicity, row_pose, backside_bias, figure, no_plus, no_black) + elif category == "couple": + row = make_couple(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus) + else: + row = make_group_or_layout(index, batch, rng, expr_deck, row_clothing, ethnicity, no_plus) + batch_rows.append(row) + index += 1 + rng.shuffle(batch_rows) + rows.extend(batch_rows) + return rows + + +def write_jsonl(path: Path, rows: list[dict]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", encoding="utf-8") as handle: + for row in rows: + handle.write(json.dumps(row, ensure_ascii=True, sort_keys=True) + "\n") + + +def write_batch_helpers(batch_dir: Path, rows: list[dict], command_text: str) -> None: + prompt_blocks = [] + captions_dir = batch_dir / "captions" + captions_dir.mkdir(parents=True, exist_ok=True) + # Clear stale sidecars so renamed rows do not leave orphans behind. + for old in captions_dir.glob("*.txt"): + old.unlink() + + for row in rows: + image_name = Path(row["file_name"]).name + prompt_blocks.append( + "\n".join( + [ + row["id"], + f"file: {row['file_name']}", + f"age: {row['age_band']}", + f"subject: {row['primary_subject']}", + f"prompt: {row['prompt']}", + f"caption: {row['caption']}", + ] + ) + ) + caption_path = captions_dir / f"{Path(image_name).stem}.txt" + caption_path.write_text(row["caption"] + "\n", encoding="utf-8") + + (batch_dir / "prompts.txt").write_text("\n\n---\n\n".join(prompt_blocks) + "\n", encoding="utf-8") + (batch_dir / "command.txt").write_text( + "\n".join( + [ + f"batch: {batch_dir.name}", + f"rows: {len(rows)}", + f"batch_size: {BATCH_SIZE}", + "command:", + command_text, + "", + ] + ), + encoding="utf-8", + ) + + +def rebuild_all_prompts(out_dir: Path) -> None: + """Rebuild all_prompts.jsonl from whatever batch metadata is on disk, so the + aggregate always matches reality even when only some batches were written.""" + lines: list[str] = [] + for batch_dir in sorted(out_dir.glob("batch_*")): + meta = batch_dir / "metadata.jsonl" + if meta.exists(): + for line in meta.read_text(encoding="utf-8").splitlines(): + if line.strip(): + lines.append(line) + (out_dir / "all_prompts.jsonl").write_text("\n".join(lines) + "\n", encoding="utf-8") + + +def write_readme(out_dir: Path, total: int, batch_size: int, clothing: str = "full", minimal_clothing_ratio: float | None = None) -> None: + if minimal_clothing_ratio is not None: + clothing_note = ( + f"Clothing mode: **mixed**. Each batch uses approximately " + f"{minimal_clothing_ratio:.0%} minimal clothing rows and " + f"{1.0 - minimal_clothing_ratio:.0%} full clothing rows." + ) + elif clothing == "minimal": + clothing_note = ( + "Clothing mode: **minimal**. Every clothing pool is restricted to the little-coverage " + "lingerie/swimwear/bralette/bodysuit register (still tasteful, non-explicit, adults only). " + "Use this set for the most revealing pin-up outputs." + ) + else: + clothing_note = ( + "Clothing mode: **full**. Women's clothing is skewed toward revealing pin-up styling " + "(lingerie, swimwear, crop tops, garters, cleavage) alongside some glam options." + ) + text = f"""# Prompt Batches + +Generated prompt manifests for the `sxcpinup_coloredpencil` adult-only dataset. + +- Total rows: {total} +- Batch size: {batch_size} +- Batch directories: `batch_001` onward +- Each row includes: `id`, `file_name`, `batch`, metadata fields (incl. `expression`, `clothing_mode`, and single-subject `pose_mode`), `prompt`, `caption`, and `negative_prompt` + +Use `prompt` to generate each image. Save the output to the row's `file_name` path inside that batch directory, then use `caption` for training metadata or sidecar captions. For quick review, each batch also includes `prompts.txt`. + +Safety/style boundary: sexy, revealing, tasteful adult pin-up that stays non-explicit. Adults only. + +{clothing_note} + +Variety: appearance, scene, pose, composition, prop, clothing, and facial-expression axes are all sampled independently. Facial expression is decoupled from body pose and drawn from a 40-entry deck for an even spread. Age mix stays young-adult heavy: most single-woman prompts are 21-25 adults, with a smaller late-20s to 50s diversity tail plus men, couples, groups, and layout scenes. +""" + readme = out_dir / "README.md" + if readme.exists(): + # Never clobber a manually-edited README (it doubles as a working + # instructions/state file). Write a sidecar instead. + (out_dir / "README.generated.md").write_text(text, encoding="utf-8") + print(f"README.md exists -- left untouched; wrote {out_dir/'README.generated.md'} instead") + return + readme.write_text(text, encoding="utf-8") + + +def parse_write_batches(spec: str, batch_count: int) -> set[int]: + spec = spec.strip().lower() + if spec in ("all", "*", ""): + return set(range(1, batch_count + 1)) + selected: set[int] = set() + for part in spec.split(","): + part = part.strip() + if not part: + continue + if "-" in part: + lo, hi = part.split("-", 1) + selected.update(range(int(lo), int(hi) + 1)) + else: + selected.add(int(part)) + return {n for n in selected if 1 <= n <= batch_count} + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--out-dir", default="dataset/prompt_batches") + parser.add_argument("--total", type=int, default=DEFAULT_TOTAL) + parser.add_argument("--start-index", type=int, default=DEFAULT_START_INDEX) + parser.add_argument( + "--write-batches", + default="all", + help="Which batches to write, e.g. 'all' or '2-12' or '2,4,6'. " + "Batches are always computed for the full range so ids stay consistent.", + ) + parser.add_argument( + "--clothing", + choices=["full", "minimal"], + default="full", + help="'full' = the broad revealing+glam wardrobe; " + "'minimal' = restrict every pool to little-coverage lingerie/swimwear styling.", + ) + parser.add_argument( + "--minimal-clothing-ratio", + type=float, + default=None, + help="Optional 0.0-1.0 per-batch ratio of minimal clothing rows. " + "When set, this mixes minimal and full clothing and overrides --clothing.", + ) + parser.add_argument( + "--ethnicity", + choices=["any", "asian", "white_asian"], + default="any", + help="'any' = balanced heritage mix (default); " + "'asian' = restrict all subjects to Asian (East/Southeast/South/Central Asian); " + "'white_asian' = bias women to white/European + Asian (men stay any heritage).", + ) + parser.add_argument( + "--poses", + choices=["standard", "evocative"], + default="standard", + help="'standard' = the default body-pose pool; 'evocative' = poses worded " + "with neutral movement/fashion vocabulary that read alluring (single subjects).", + ) + parser.add_argument( + "--standard-pose-ratio", + type=float, + default=None, + help="Optional 0.0-1.0 per-batch ratio of standard poses among single-subject rows. " + "When set, this mixes standard and evocative poses and overrides --poses for single subjects.", + ) + parser.add_argument( + "--backside-bias", + type=float, + default=0.0, + help="0.0-1.0 probability that a single-subject evocative pose is a " + "rear/back-to-viewer angle (applies to evocative single-subject rows).", + ) + parser.add_argument( + "--figure", + choices=["curvy", "balanced", "bombshell"], + default="curvy", + help="Curve emphasis for single women, described with filter-safe " + "figure-drawing vocabulary (full bust, cinched waist, full hips, " + "peach-shaped rear): 'curvy' (default, voluptuous-leaning mix), " + "'balanced' (adds athletic/slim variety), 'bombshell' (maximum exaggerated hourglass).", + ) + parser.add_argument( + "--no-plus-women", + action="store_true", + help="Exclude plus-size women (and fat/plus couple combos); men are unaffected.", + ) + parser.add_argument( + "--no-black", + action="store_true", + help="Exclude Black/African-coded women. Men, couples, groups, and layouts " + "are unaffected by this filter, so " + "the image model may still render Black people there.", + ) + args = parser.parse_args() + + if args.total % BATCH_SIZE != 0: + raise ValueError(f"total must be divisible by {BATCH_SIZE}") + if args.minimal_clothing_ratio is not None and not 0.0 <= args.minimal_clothing_ratio <= 1.0: + raise ValueError("--minimal-clothing-ratio must be between 0.0 and 1.0") + if args.standard_pose_ratio is not None and not 0.0 <= args.standard_pose_ratio <= 1.0: + raise ValueError("--standard-pose-ratio must be between 0.0 and 1.0") + + out_dir = Path(args.out_dir) + rows = build_rows( + args.total, + args.start_index, + args.clothing, + args.ethnicity, + args.poses, + args.backside_bias, + args.figure, + args.no_plus_women, + args.no_black, + args.minimal_clothing_ratio, + args.standard_pose_ratio, + ) + batch_count = args.total // BATCH_SIZE + write_set = parse_write_batches(args.write_batches, batch_count) + command_text = "python3 " + shlex.join(sys.argv) + + written: list[str] = [] + skipped: list[str] = [] + for offset in range(0, len(rows), BATCH_SIZE): + batch_rows = rows[offset : offset + BATCH_SIZE] + batch_name = batch_rows[0]["batch"] + batch_number = int(batch_name.split("_")[1]) + batch_dir = out_dir / batch_name + + if batch_number not in write_set: + continue + + images_dir = batch_dir / "images" + images_dir.mkdir(parents=True, exist_ok=True) + if any(images_dir.iterdir()): + skipped.append(f"{batch_name} (images present)") + continue + + write_jsonl(batch_dir / "metadata.jsonl", batch_rows) + write_batch_helpers(batch_dir, batch_rows, command_text) + written.append(batch_name) + + rebuild_all_prompts(out_dir) + write_readme(out_dir, args.total, BATCH_SIZE, args.clothing, args.minimal_clothing_ratio) + clothing_summary = ( + f"mixed minimal_ratio={args.minimal_clothing_ratio}" + if args.minimal_clothing_ratio is not None + else args.clothing + ) + pose_summary = ( + f"mixed standard_ratio={args.standard_pose_ratio}" + if args.standard_pose_ratio is not None + else args.poses + ) + print(f"Wrote batches: {', '.join(written) if written else '(none)'} (clothing={clothing_summary}, ethnicity={args.ethnicity}, poses={pose_summary}, figure={args.figure}, no_plus_women={args.no_plus_women}, no_black={args.no_black})") + if skipped: + print(f"Skipped (already rendered): {', '.join(skipped)}") + print(f"Output dir: {out_dir}") + + +if __name__ == "__main__": + main() diff --git a/prompt_builder.py b/prompt_builder.py new file mode 100644 index 0000000..3dc8f17 --- /dev/null +++ b/prompt_builder.py @@ -0,0 +1,1381 @@ +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: vertical {composition}. {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: vertical {composition}. {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: vertical {composition}. " + "{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." +) + + +_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 _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 < 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", + "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 _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 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 _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 _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]]: + 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: + if not _compatible_entry(subcategory, women_count, men_count): + raise ValueError( + f"Subcategory '{subcategory['name']}' is not compatible with " + f"women_count={women_count}, men_count={men_count}" + ) + return category, subcategory + 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 + + +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 _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": f"{body} figure with {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 _lettered(prefix: str, count: int) -> list[str]: + letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + return [f"{prefix} {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 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: + 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 = g.choose(rng, g.COUPLE_TYPES) + 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, + } + + 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 + return _list_from(_merged_field(category, subcategory, item, "scenes", fallback)) + + +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 = _merged_field(category, subcategory, item, "compositions") + if configured: + return _list_from(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], +) -> 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) + + category, subcategory = _find_subcategory( + categories, + category_choice, + subcategory_choice, + category_rng, + subcategory_rng, + women_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 = _choose_text( + expression_rng, + _compatible_entries(_list_from(_merged_field(category, subcategory, item, "expressions", g.EXPRESSIONS)), women_count, men_count), + ) + if subject_type in ("couple", "group") and ";" not in expression: + expression = f"{expression}; {_choose_text(expression_rng, g.EXPRESSIONS)}" + 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, + "composition": 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, + "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", ""), + "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, +) -> 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) + 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, + ) + + if extra_positive.strip(): + row["prompt"] = f"{row['prompt'].rstrip()} {extra_positive.strip()}" + 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 + return row