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, "positive_suffix": positive_suffix, "custom_item": item_name, "item_axis_values": item_axis_values, "scene_text": scene, "pose": pose, "seed_config": seed_config, "content_seed_axis": content_axis, "role_graph": role_graph, "cast_summary": context.get("cast_summary", ""), "scene_kind": context.get("scene_kind", ""), "women_count": context.get("women_count", ""), "men_count": context.get("men_count", ""), "person_count": context.get("person_count", ""), "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 INSTA_OF_SOFT_LEVELS = { "social_tease": "Instagram-style thirst-trap post, suggestive but non-explicit, polished social feed energy", "lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate but without explicit sex", "implied_nude": "implied nude creator set, strategically covered body, sensual but no visible sex act", "explicit_tease": "explicit adult tease, nudity can be visible, but no penetration or partnered sex act", } INSTA_OF_HARDCORE_LEVELS = { "explicit": "explicit adult creator content with clear sexual contact and adult-only framing", "hardcore": "hardcore adult creator content with anatomically clear sexual contact and intense body language", } INSTA_OF_PLATFORM_STYLES = { "hybrid": "hybrid Instagram-to-OF creator shoot, polished social-media framing with intimate subscriber-content energy", "instagram": "Instagram-inspired creator shoot, polished mirror-selfie and feed-post aesthetics", "onlyfans": "OnlyFans-inspired creator shoot, intimate subscriber-view camera and candid premium-content framing", } INSTA_OF_NEGATIVE = ( "minors, childlike appearance, teen, underage, schoolgirl, non-consensual, coercion, rape, " "violence, injury, blood, gore, incest, bestiality, watermark, logo, readable username, social media UI" ) INSTA_OF_SOFT_NEGATIVE = ( INSTA_OF_NEGATIVE + ", explicit intercourse, penetration, oral sex, cumshot, genital contact, group sex" ) def build_insta_of_options_json( softcore_cast: str = "solo", hardcore_cast: str = "use_counts", hardcore_women_count: int = 1, hardcore_men_count: int = 1, softcore_level: str = "lingerie_tease", hardcore_level: str = "hardcore", platform_style: str = "hybrid", continuity: str = "same_creator_same_room", ) -> str: return json.dumps( { "softcore_cast": softcore_cast, "hardcore_cast": hardcore_cast, "hardcore_women_count": int(hardcore_women_count), "hardcore_men_count": int(hardcore_men_count), "softcore_level": softcore_level, "hardcore_level": hardcore_level, "platform_style": platform_style, "continuity": continuity, }, ensure_ascii=True, sort_keys=True, ) def _parse_insta_of_options(options_json: str | dict[str, Any] | None) -> dict[str, Any]: defaults = { "softcore_cast": "solo", "hardcore_cast": "use_counts", "hardcore_women_count": 1, "hardcore_men_count": 1, "softcore_level": "lingerie_tease", "hardcore_level": "hardcore", "platform_style": "hybrid", "continuity": "same_creator_same_room", } if not options_json: return defaults if isinstance(options_json, dict): raw = options_json else: try: raw = json.loads(str(options_json)) except json.JSONDecodeError as exc: raise ValueError(f"Invalid Insta/OF options JSON: {exc}") from exc if not isinstance(raw, dict): raise ValueError("Insta/OF options must be a JSON object") parsed = {**defaults, **raw} parsed["softcore_cast"] = parsed["softcore_cast"] if parsed["softcore_cast"] in ("solo", "same_as_hardcore") else defaults["softcore_cast"] parsed["hardcore_cast"] = parsed["hardcore_cast"] if parsed["hardcore_cast"] in ("use_counts", "couple", "threesome", "group") else defaults["hardcore_cast"] parsed["softcore_level"] = parsed["softcore_level"] if parsed["softcore_level"] in INSTA_OF_SOFT_LEVELS else defaults["softcore_level"] parsed["hardcore_level"] = parsed["hardcore_level"] if parsed["hardcore_level"] in INSTA_OF_HARDCORE_LEVELS else defaults["hardcore_level"] parsed["platform_style"] = parsed["platform_style"] if parsed["platform_style"] in INSTA_OF_PLATFORM_STYLES else defaults["platform_style"] parsed["continuity"] = parsed["continuity"] if parsed["continuity"] in ("same_creator_same_room", "same_creator_new_scene") else defaults["continuity"] for key in ("hardcore_women_count", "hardcore_men_count"): try: parsed[key] = max(0, min(12, int(parsed[key]))) except (TypeError, ValueError): parsed[key] = defaults[key] return parsed def _insta_of_hardcore_counts(options: dict[str, Any]) -> tuple[int, int]: policy = str(options.get("hardcore_cast", "use_counts")) if policy == "couple": women_count, men_count = 1, 1 elif policy == "threesome": women_count, men_count = 2, 1 elif policy == "group": women_count, men_count = 3, 2 else: women_count = int(options.get("hardcore_women_count") or 0) men_count = int(options.get("hardcore_men_count") or 0) women_count = max(1, min(12, women_count)) men_count = max(0, min(12, men_count)) if women_count + men_count < 2: men_count = 1 return women_count, men_count def _insta_of_descriptor(row: dict[str, Any]) -> str: age = str(row.get("age_band") or row.get("age") or "").strip() age = " ".join(age.split()) age = age.removesuffix(" adults").removesuffix(" adult").strip() pieces = [ f"{age} adult woman" if age else "adult woman", row.get("body_phrase"), row.get("skin"), row.get("hair"), row.get("eyes"), ] return ", ".join(str(piece).strip() for piece in pieces if piece and str(piece).strip()) def _insta_of_cast_phrase(women_count: int, men_count: int) -> str: context = _configured_cast_context(women_count, men_count) return context["cast_summary"] def _insta_of_active_trigger(prompt: str, trigger: str, enabled: bool) -> str: return _prepend_trigger(prompt, trigger, enabled) def build_insta_of_pair( row_number: int, start_index: int, seed: int, ethnicity: str, figure: str, no_plus_women: bool, no_black: bool, trigger: str, prepend_trigger_to_prompt: bool, seed_config: str | dict[str, Any] | None = None, options_json: str | dict[str, Any] | None = None, extra_positive: str = "", extra_negative: str = "", ) -> dict[str, Any]: options = _parse_insta_of_options(options_json) hard_women_count, hard_men_count = _insta_of_hardcore_counts(options) active_trigger = trigger.strip() or g.TRIGGER parsed_seed_config = _parse_seed_config(seed_config) soft_row = build_prompt( category="Provocative erotic clothes", subcategory=RANDOM_SUBCATEGORY, row_number=row_number, start_index=start_index, seed=seed, clothing="minimal", ethnicity=ethnicity, poses="evocative", backside_bias=0.0, figure=figure, no_plus_women=no_plus_women, no_black=no_black, minimal_clothing_ratio=-1, standard_pose_ratio=-1, trigger=active_trigger, prepend_trigger_to_prompt=False, extra_positive="", extra_negative="", seed_config=parsed_seed_config, women_count=1, men_count=0, ) hard_row = build_prompt( category="Hardcore sexual poses", subcategory=RANDOM_SUBCATEGORY, row_number=row_number, start_index=start_index, seed=seed, clothing="minimal", ethnicity=ethnicity, poses="evocative", backside_bias=0.0, figure=figure, no_plus_women=no_plus_women, no_black=no_black, minimal_clothing_ratio=-1, standard_pose_ratio=-1, trigger=active_trigger, prepend_trigger_to_prompt=False, extra_positive="", extra_negative="", seed_config=parsed_seed_config, women_count=hard_women_count, men_count=hard_men_count, ) descriptor = _insta_of_descriptor(soft_row) platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]] soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]] hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]] hard_scene = soft_row["scene_text"] if options["continuity"] == "same_creator_same_room" else hard_row["scene_text"] hard_composition = soft_row["composition"] if options["continuity"] == "same_creator_same_room" else hard_row["composition"] soft_cast = ( "solo creator setup; the primary creator is alone in the softcore version" if options["softcore_cast"] == "solo" else f"non-explicit teaser setup with the same adult cast as the hardcore version: {_insta_of_cast_phrase(hard_women_count, hard_men_count)}" ) hard_cast = _insta_of_cast_phrase(hard_women_count, hard_men_count) soft_prompt = ( f"Insta/OF softcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. " f"Softcore setup: {soft_level}. Cast continuity: {soft_cast}. " f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. " f"Facial expression: {soft_row['expression']}. Composition: {soft_row['composition']}. " "Keep the softcore version adult-only, consensual, seductive, creator-shot, and non-explicit. " f"{soft_row['positive_suffix']} Avoid: {INSTA_OF_SOFT_NEGATIVE}." ) hard_prompt = ( f"Insta/OF hardcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. " f"Hardcore setup: {hard_level}. Cast: {hard_cast}. " "Apply the shared descriptor to the most visually central woman, keeping her continuous with the softcore version. " f"Role graph: {hard_row['role_graph']} Sexual scene: {hard_row['item']}. " f"Setting: {hard_scene}. Facial expressions: {hard_row['expression']}. Composition: {hard_composition}. " "All participants are consenting adults 21+. " f"{hard_row['positive_suffix']} Avoid: {INSTA_OF_NEGATIVE}." ) if extra_positive.strip(): soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}" hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}" soft_prompt = _insta_of_active_trigger(soft_prompt, active_trigger, bool(prepend_trigger_to_prompt)) hard_prompt = _insta_of_active_trigger(hard_prompt, active_trigger, bool(prepend_trigger_to_prompt)) soft_negative = _combined_negative(INSTA_OF_SOFT_NEGATIVE, extra_negative) hard_negative = _combined_negative(INSTA_OF_NEGATIVE, extra_negative) soft_caption = ( f"{active_trigger}, Insta/OF softcore mode, {descriptor}, {soft_level}, " f"{soft_row['item']}, {soft_row['pose']}, {soft_row['scene_text']}, {soft_row['composition']}" ) hard_caption = ( f"{active_trigger}, Insta/OF hardcore mode, same primary creator descriptor, {descriptor}, " f"{hard_cast}, {hard_row['role_graph']}, {hard_row['item']}, {hard_scene}, {hard_composition}" ) metadata = { "mode": "Insta/OF", "options": options, "shared_descriptor": descriptor, "softcore_prompt": soft_prompt, "hardcore_prompt": hard_prompt, "softcore_negative_prompt": soft_negative, "hardcore_negative_prompt": hard_negative, "softcore_caption": soft_caption, "hardcore_caption": hard_caption, "softcore_row": soft_row, "hardcore_row": hard_row, "hardcore_women_count": hard_women_count, "hardcore_men_count": hard_men_count, } return metadata