435 lines
21 KiB
Python
435 lines
21 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
|
|
try:
|
|
from . import category_library as category_policy
|
|
except ImportError: # Allows local smoke tests from the repository root.
|
|
import category_library as category_policy
|
|
|
|
|
|
INSTA_OF_SOFT_LEVELS = {
|
|
"social_tease": "Instagram-style thirst-trap post, suggestive polished social feed energy",
|
|
"lingerie_tease": "premium OF teaser set, lingerie-focused, sensual and intimate",
|
|
"implied_nude": "implied nude creator set, strategically covered body and intimate teaser framing",
|
|
"explicit_tease": "stronger adult teaser set with bolder nude-adjacent styling and solo-tease framing",
|
|
"explicit_nude": "explicit nude creator set with fully nude solo-tease framing",
|
|
}
|
|
|
|
INSTA_OF_HARDCORE_LEVELS = {
|
|
"explicit": "explicit adult creator content with clear sexual contact and adult-only framing",
|
|
"hardcore": "hardcore adult creator content with anatomically clear sexual contact and intense body language",
|
|
}
|
|
|
|
INSTA_OF_PLATFORM_STYLES = {
|
|
"hybrid": "hybrid Instagram-to-OF creator shoot, polished social-media framing with intimate subscriber-content energy",
|
|
"instagram": "Instagram-inspired creator shoot, polished mirror-selfie and feed-post aesthetics",
|
|
"onlyfans": "OnlyFans-inspired creator shoot, intimate subscriber-view camera and candid premium-content framing",
|
|
}
|
|
|
|
INSTA_OF_HARDCORE_CLOTHING_CONTINUITY = {
|
|
"none": "",
|
|
"same_outfit": "Woman A keeps her teaser outfit on with the body contact readable",
|
|
"partially_removed": "Woman A's teaser outfit is pushed aside and partly removed where needed, leaving body contact unobstructed",
|
|
"implied_nude": "Woman A's body is partly exposed, with fabric slipping off or covering only part of the body",
|
|
"explicit_nude": "Woman A's body is fully exposed, bare skin unobstructed",
|
|
}
|
|
|
|
HARDCORE_DETAIL_DENSITY_CHOICES = ["compact", "balanced", "dense"]
|
|
|
|
HARDCORE_DETAIL_DIRECTIVES = {
|
|
"compact": "Use one compact position-first sexual action sentence; avoid repeated aftermath wording. ",
|
|
"balanced": "",
|
|
"dense": "Use dense but coherent motion, contact, and aftermath detail while keeping one readable body position. ",
|
|
}
|
|
|
|
INSTA_OF_NEGATIVE = (
|
|
"minors, childlike appearance, teen, underage, schoolgirl, non-consensual, coercion, rape, "
|
|
"violence, injury, blood, gore, incest, bestiality, watermark, logo, readable username, social media UI"
|
|
)
|
|
|
|
INSTA_OF_SOFT_NEGATIVE = (
|
|
INSTA_OF_NEGATIVE
|
|
+ ", explicit intercourse, penetration, oral sex, cumshot, genital contact, group sex, "
|
|
"shirtless partner, bare-chested partner, partner nudity"
|
|
)
|
|
|
|
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL = {
|
|
"social_tease": "Casual clothes / Smart casual",
|
|
"lingerie_tease": "Provocative erotic clothes / Provocative lingerie",
|
|
"implied_nude": "Provocative erotic clothes / Provocative lingerie",
|
|
"explicit_tease": "Provocative erotic clothes / Sheer exposed",
|
|
"explicit_nude": "Provocative erotic clothes / Nude accessories",
|
|
}
|
|
|
|
INSTA_OF_SOFTCORE_OUTFITS = {
|
|
"social_tease": [
|
|
"cropped fitted tee, low-rise jeans, delicate jewelry, and polished feed-post styling",
|
|
"oversized off-shoulder sweater with fitted shorts and soft lounge socks",
|
|
"ribbed tank top, mini skirt, hoop earrings, and casual creator styling",
|
|
"silky camisole tucked into relaxed trousers with a subtle waist chain",
|
|
"sporty crop top, bike shorts, clean sneakers, and glossy social-feed styling",
|
|
"button-down shirt tied at the waist over a fitted bralette and denim shorts",
|
|
"body-hugging knit dress with bare shoulders and simple heels",
|
|
"relaxed hoodie half-zipped over a crop top with high-cut shorts",
|
|
],
|
|
"lingerie_tease": [
|
|
"black lace lingerie set with opaque cups, high-waisted briefs, garter straps, and sheer robe",
|
|
"satin bralette and matching high-waisted panties under an oversized shirt",
|
|
"lace bodysuit with opaque cups, soft stockings, and delicate garter details",
|
|
"silk slip dress with thin straps, thigh slit, and subtle lace trim",
|
|
"matching balconette bra and brief set under a loosely draped satin robe",
|
|
"velvet lingerie set with covered cups, garter belt, sheer stockings, and small gold accents",
|
|
"mesh robe over a covered lace teddy, styled as a premium creator teaser",
|
|
"structured corset top with opaque panels, matching briefs, and sheer stockings",
|
|
],
|
|
"implied_nude": [
|
|
"oversized white shirt slipping off one shoulder, body mostly covered, bare legs, and soft creator-shot styling",
|
|
"towel wrap held across the chest and hips, implied nude but fully covered",
|
|
"satin sheet wrapped around the body with shoulders and legs visible but intimate areas covered",
|
|
"open robe held closed by hand, implied nude beneath without explicit exposure",
|
|
"bath towel and damp hair after a shower, covered chest and hips, intimate creator styling",
|
|
"soft blanket wrapped around the body, bare shoulders visible, sensual but covered",
|
|
],
|
|
"explicit_tease": [
|
|
"sheer robe over matching lingerie with intimate areas obscured by lace pattern and pose",
|
|
"wet-look bodysuit with opaque panels, high-cut legs, and glossy club-light styling",
|
|
"transparent mesh dress over covered lingerie, posed as an adult creator teaser",
|
|
"lace teddy with strategic opaque embroidery, garter straps, and sheer stockings",
|
|
"bare-shoulder robe opened around covered lingerie, bold solo adult tease",
|
|
"strappy lingerie set with covered cups and high-waisted bottoms, styled as a stronger solo teaser",
|
|
],
|
|
"explicit_nude": [
|
|
"body fully exposed with jewelry accents and direct adult selfie confidence",
|
|
"mirror-selfie body exposure with jewelry accents and bold creator-shot framing",
|
|
"body fully exposed with direct eye contact and soft creator-shot styling",
|
|
"vanity-mirror body exposure with necklace detail and premium creator-shot styling",
|
|
"shower-afterglow body exposure with wet hair, skin highlights, and phone-shot framing",
|
|
"indoor body exposure with one hand holding the phone and direct camera awareness",
|
|
],
|
|
}
|
|
|
|
INSTA_OF_SOFTCORE_POSES = {
|
|
"social_tease": [
|
|
"taking a mirror selfie with one hip angled and relaxed social-feed confidence",
|
|
"leaning against a doorway with one hand holding the phone and a casual teasing smile",
|
|
"sitting casually for a polished outfit-check selfie",
|
|
"standing by the window with shoulders relaxed and body angled toward the phone",
|
|
"posing in a clean feed-post stance with one hand at the waist",
|
|
"stretching one arm above the head in a casual morning selfie pose",
|
|
],
|
|
"lingerie_tease": [
|
|
"taking a mirror lingerie selfie with one hip angled and the outfit clearly visible",
|
|
"kneeling in a covered lingerie teaser pose with hands resting on fabric",
|
|
"leaning with the robe draped around covered lingerie",
|
|
"standing in a three-quarter lingerie outfit-check pose with legs softly crossed",
|
|
"sitting with stockings and garter details visible in a controlled teaser pose",
|
|
"turning slightly over one shoulder to show the lingerie silhouette",
|
|
],
|
|
"implied_nude": [
|
|
"holding the towel or sheet securely in place while posing for an implied nude selfie",
|
|
"sitting with soft fabric wrapped securely around the body and shoulders visible",
|
|
"standing by a mirror with a towel wrapped around the body",
|
|
"reclining under satin fabric with intimate areas fully obscured",
|
|
"holding an open robe closed in a covered implied nude teaser pose",
|
|
"looking into the phone camera while wrapped in a blanket with bare shoulders visible",
|
|
],
|
|
"explicit_tease": [
|
|
"posing in a stronger adult teaser stance with covered lingerie and direct camera awareness",
|
|
"kneeling with a sheer robe arranged around covered lingerie",
|
|
"standing close to the mirror with the outfit framed boldly",
|
|
"leaning forward slightly with hands on the robe and intimate areas obscured",
|
|
"sitting in a bolder covered lingerie pose with direct eye contact",
|
|
"arching subtly in a solo adult tease while the styling keeps explicit anatomy obscured",
|
|
],
|
|
"explicit_nude": [
|
|
"taking a bold mirror selfie with direct eye contact and the body clearly framed",
|
|
"posing with body fully exposed and jewelry accents as styling",
|
|
"standing with body fully exposed in a premium creator-shot pose",
|
|
"reclining with body fully exposed and the phone held close",
|
|
"turning slightly in a mirror pose with the body framed head-to-thigh",
|
|
"kneeling in a controlled adult teaser pose with body fully exposed and direct phone-camera awareness",
|
|
],
|
|
}
|
|
|
|
INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS = [
|
|
"satin slip dress under an oversized shirt",
|
|
"soft cardigan over a camisole with relaxed trousers",
|
|
"fitted crop top with high-waisted jeans",
|
|
"silky robe over a covered bralette and lounge shorts",
|
|
"bodycon mini dress with simple heels",
|
|
"ribbed tank top with joggers and delicate jewelry",
|
|
"oversized tee with fitted shorts and lounge socks",
|
|
"button-down shirt with a fitted skirt",
|
|
]
|
|
|
|
INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS = [
|
|
"fitted black tee with dark jeans",
|
|
"buttoned linen shirt with chinos",
|
|
"hoodie and joggers",
|
|
"open overshirt over a fitted tank with relaxed trousers",
|
|
"gym tee with track pants and a towel over one shoulder",
|
|
"casual knit shirt with tailored trousers",
|
|
"dark crewneck sweater with jeans",
|
|
"short-sleeve button-up shirt with relaxed shorts",
|
|
]
|
|
|
|
SOFTCORE_CAST_POSES = [
|
|
"standing together for a mirror selfie with relaxed close body language",
|
|
"posing shoulder-to-shoulder in a creator-shot group teaser",
|
|
"leaning together in a polished subscriber preview",
|
|
"sitting close together with relaxed hands and styled outfit visibility",
|
|
"arranged around Woman A in a flirtatious creator-teaser pose",
|
|
"posing together as a coordinated adult creator set",
|
|
"standing near the phone tripod with relaxed teasing body language",
|
|
"framed together in a softcore cast reveal",
|
|
]
|
|
|
|
|
|
def _is_false(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return not value
|
|
text = str(value).strip().lower()
|
|
return text in {"false", "0", "no", "off", "disabled"}
|
|
|
|
|
|
def _clamped_float(value: Any, default: float = 0.5, min_value: float = 0.0, max_value: float = 1.0) -> float:
|
|
try:
|
|
number = float(value)
|
|
except (TypeError, ValueError):
|
|
number = default
|
|
return max(min_value, min(max_value, number))
|
|
|
|
|
|
def _normalize_free_text_values(values: Any) -> list[str]:
|
|
if isinstance(values, str):
|
|
raw_values = [part.strip() for part in re.split(r"[\n;]+", values) if part.strip()]
|
|
elif isinstance(values, (list, tuple, set)):
|
|
raw_values = list(values)
|
|
else:
|
|
raw_values = []
|
|
normalized: list[str] = []
|
|
for raw_value in raw_values:
|
|
value = str(raw_value or "").strip()
|
|
if value and value not in normalized:
|
|
normalized.append(value)
|
|
return normalized
|
|
|
|
|
|
def character_softcore_outfit_values(source: str, custom_outfits: str = "") -> list[str]:
|
|
source = str(source or "no_change").strip()
|
|
if source in INSTA_OF_SOFTCORE_OUTFITS:
|
|
return list(INSTA_OF_SOFTCORE_OUTFITS[source])
|
|
if source == "partner_woman":
|
|
return list(INSTA_OF_SOFTCORE_PARTNER_WOMEN_OUTFITS)
|
|
if source == "partner_man":
|
|
return list(INSTA_OF_SOFTCORE_PARTNER_MEN_OUTFITS)
|
|
if source == "custom":
|
|
return _normalize_free_text_values(custom_outfits)
|
|
return []
|
|
|
|
|
|
def hardcore_detail_density_choices() -> list[str]:
|
|
return list(HARDCORE_DETAIL_DENSITY_CHOICES)
|
|
|
|
|
|
def hardcore_detail_directive(density: Any) -> str:
|
|
return HARDCORE_DETAIL_DIRECTIVES.get(str(density or "balanced"), "")
|
|
|
|
|
|
def character_hardcore_clothing_values(state: str, custom_clothing: str = "") -> list[str]:
|
|
state = str(state or "no_change").strip()
|
|
if state == "fully_nude":
|
|
return ["fully nude"]
|
|
if state == "partly_exposed":
|
|
return ["partly nude, body exposed"]
|
|
if state == "same_outfit":
|
|
return ["keeps the teaser outfit on with the body contact readable"]
|
|
if state == "partially_removed":
|
|
return ["teaser outfit is pushed aside and partly removed where needed, leaving body contact unobstructed"]
|
|
if state == "custom":
|
|
return _normalize_free_text_values(custom_clothing)
|
|
return []
|
|
|
|
|
|
def build_insta_of_options_json(
|
|
softcore_cast: str = "solo",
|
|
hardcore_cast: str = "use_counts",
|
|
hardcore_women_count: int = 1,
|
|
hardcore_men_count: int = 1,
|
|
softcore_level: str = "lingerie_tease",
|
|
hardcore_level: str = "hardcore",
|
|
platform_style: str = "hybrid",
|
|
continuity: str = "same_creator_same_room",
|
|
hardcore_clothing_continuity: str = "partially_removed",
|
|
softcore_camera_mode: str = "handheld_selfie",
|
|
hardcore_camera_mode: str = "from_camera_config",
|
|
camera_detail: str = "from_camera_config",
|
|
softcore_expression_intensity: float = 0.45,
|
|
hardcore_expression_intensity: float = 0.85,
|
|
softcore_expression_enabled: bool = True,
|
|
hardcore_expression_enabled: bool = True,
|
|
hardcore_detail_density: str = "balanced",
|
|
hardcore_detail_density_choices: list[str] | tuple[str, ...] = tuple(HARDCORE_DETAIL_DENSITY_CHOICES),
|
|
) -> str:
|
|
hardcore_detail_density = (
|
|
hardcore_detail_density if hardcore_detail_density in hardcore_detail_density_choices else "balanced"
|
|
)
|
|
return json.dumps(
|
|
{
|
|
"softcore_cast": softcore_cast,
|
|
"hardcore_cast": hardcore_cast,
|
|
"hardcore_women_count": int(hardcore_women_count),
|
|
"hardcore_men_count": int(hardcore_men_count),
|
|
"softcore_level": softcore_level,
|
|
"hardcore_level": hardcore_level,
|
|
"platform_style": platform_style,
|
|
"continuity": continuity,
|
|
"hardcore_clothing_continuity": hardcore_clothing_continuity,
|
|
"softcore_camera_mode": softcore_camera_mode,
|
|
"hardcore_camera_mode": hardcore_camera_mode,
|
|
"camera_detail": camera_detail,
|
|
"softcore_expression_enabled": not _is_false(softcore_expression_enabled),
|
|
"hardcore_expression_enabled": not _is_false(hardcore_expression_enabled),
|
|
"softcore_expression_intensity": _clamped_float(softcore_expression_intensity, 0.45),
|
|
"hardcore_expression_intensity": _clamped_float(hardcore_expression_intensity, 0.85),
|
|
"hardcore_detail_density": hardcore_detail_density,
|
|
},
|
|
ensure_ascii=True,
|
|
sort_keys=True,
|
|
)
|
|
|
|
|
|
def parse_insta_of_options(
|
|
options_json: str | dict[str, Any] | None,
|
|
*,
|
|
camera_mode_choices: dict[str, str] | list[str] | tuple[str, ...],
|
|
camera_detail_choices: list[str] | tuple[str, ...],
|
|
hardcore_detail_density_choices: list[str] | tuple[str, ...] = tuple(HARDCORE_DETAIL_DENSITY_CHOICES),
|
|
) -> dict[str, Any]:
|
|
defaults = {
|
|
"softcore_cast": "solo",
|
|
"hardcore_cast": "use_counts",
|
|
"hardcore_women_count": 1,
|
|
"hardcore_men_count": 1,
|
|
"softcore_level": "lingerie_tease",
|
|
"hardcore_level": "hardcore",
|
|
"platform_style": "hybrid",
|
|
"continuity": "same_creator_same_room",
|
|
"hardcore_clothing_continuity": "partially_removed",
|
|
"softcore_camera_mode": "handheld_selfie",
|
|
"hardcore_camera_mode": "from_camera_config",
|
|
"camera_detail": "from_camera_config",
|
|
"softcore_expression_enabled": True,
|
|
"hardcore_expression_enabled": True,
|
|
"softcore_expression_intensity": 0.45,
|
|
"hardcore_expression_intensity": 0.85,
|
|
"hardcore_detail_density": "balanced",
|
|
}
|
|
if not options_json:
|
|
return defaults
|
|
if isinstance(options_json, dict):
|
|
raw = options_json
|
|
else:
|
|
try:
|
|
raw = json.loads(str(options_json))
|
|
except json.JSONDecodeError as exc:
|
|
raise ValueError(f"Invalid Insta/OF options JSON: {exc}") from exc
|
|
if not isinstance(raw, dict):
|
|
raise ValueError("Insta/OF options must be a JSON object")
|
|
|
|
valid_camera_modes = set(camera_mode_choices) if isinstance(camera_mode_choices, dict) else set(camera_mode_choices)
|
|
parsed = {**defaults, **raw}
|
|
parsed["softcore_cast"] = parsed["softcore_cast"] if parsed["softcore_cast"] in ("solo", "same_as_hardcore") else defaults["softcore_cast"]
|
|
parsed["hardcore_cast"] = parsed["hardcore_cast"] if parsed["hardcore_cast"] in ("use_counts", "couple", "threesome", "group") else defaults["hardcore_cast"]
|
|
parsed["softcore_level"] = parsed["softcore_level"] if parsed["softcore_level"] in INSTA_OF_SOFT_LEVELS else defaults["softcore_level"]
|
|
parsed["hardcore_level"] = parsed["hardcore_level"] if parsed["hardcore_level"] in INSTA_OF_HARDCORE_LEVELS else defaults["hardcore_level"]
|
|
parsed["platform_style"] = parsed["platform_style"] if parsed["platform_style"] in INSTA_OF_PLATFORM_STYLES else defaults["platform_style"]
|
|
parsed["continuity"] = parsed["continuity"] if parsed["continuity"] in ("same_creator_same_room", "same_creator_new_scene") else defaults["continuity"]
|
|
parsed["hardcore_clothing_continuity"] = (
|
|
parsed["hardcore_clothing_continuity"]
|
|
if parsed["hardcore_clothing_continuity"] in INSTA_OF_HARDCORE_CLOTHING_CONTINUITY
|
|
else defaults["hardcore_clothing_continuity"]
|
|
)
|
|
parsed["softcore_camera_mode"] = (
|
|
parsed["softcore_camera_mode"]
|
|
if parsed["softcore_camera_mode"] in valid_camera_modes or parsed["softcore_camera_mode"] == "from_camera_config"
|
|
else defaults["softcore_camera_mode"]
|
|
)
|
|
if (
|
|
parsed["hardcore_camera_mode"] not in valid_camera_modes
|
|
and parsed["hardcore_camera_mode"] not in ("from_camera_config", "same_as_softcore")
|
|
):
|
|
parsed["hardcore_camera_mode"] = defaults["hardcore_camera_mode"]
|
|
parsed["camera_detail"] = (
|
|
parsed["camera_detail"]
|
|
if parsed["camera_detail"] in camera_detail_choices or parsed["camera_detail"] == "from_camera_config"
|
|
else defaults["camera_detail"]
|
|
)
|
|
parsed["softcore_expression_enabled"] = not _is_false(parsed.get("softcore_expression_enabled", True))
|
|
parsed["hardcore_expression_enabled"] = not _is_false(parsed.get("hardcore_expression_enabled", True))
|
|
parsed["softcore_expression_intensity"] = _clamped_float(
|
|
parsed.get("softcore_expression_intensity"),
|
|
defaults["softcore_expression_intensity"],
|
|
)
|
|
parsed["hardcore_expression_intensity"] = _clamped_float(
|
|
parsed.get("hardcore_expression_intensity"),
|
|
defaults["hardcore_expression_intensity"],
|
|
)
|
|
parsed["hardcore_detail_density"] = (
|
|
parsed["hardcore_detail_density"]
|
|
if parsed.get("hardcore_detail_density") in hardcore_detail_density_choices
|
|
else defaults["hardcore_detail_density"]
|
|
)
|
|
for key in ("hardcore_women_count", "hardcore_men_count"):
|
|
try:
|
|
parsed[key] = max(0, min(12, int(parsed[key])))
|
|
except (TypeError, ValueError):
|
|
parsed[key] = defaults[key]
|
|
return parsed
|
|
|
|
|
|
def 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 softcore_category(level: str) -> tuple[str, str]:
|
|
subcategory = INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL.get(
|
|
level,
|
|
INSTA_OF_SOFTCORE_SUBCATEGORY_BY_LEVEL["lingerie_tease"],
|
|
)
|
|
exact_choice = category_policy.split_exact_subcategory_choice(
|
|
category_policy.load_category_library(),
|
|
subcategory,
|
|
)
|
|
category = exact_choice[0]["name"] if exact_choice else subcategory.split(" / ", 1)[0]
|
|
return category, subcategory
|
|
|
|
|
|
def softcore_outfit_pool(level: str) -> list[str]:
|
|
return list(INSTA_OF_SOFTCORE_OUTFITS.get(level, INSTA_OF_SOFTCORE_OUTFITS["lingerie_tease"]))
|
|
|
|
|
|
def softcore_pose_pool(level: str) -> list[str]:
|
|
return list(INSTA_OF_SOFTCORE_POSES.get(level, INSTA_OF_SOFTCORE_POSES["lingerie_tease"]))
|
|
|
|
|
|
def softcore_item_prompt_label(level: str) -> str:
|
|
return "Body exposure" if level == "explicit_nude" else "Outfit"
|