diff --git a/README.md b/README.md index 7790c48..bfd37d8 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,10 @@ When the hardcore cast includes partners, pair mode also creates deterministic shared cast descriptors such as `woman A / primary creator` and `man A`. Use `softcore_cast=same_as_hardcore`, `hardcore_cast=couple`, and `continuity=same_creator_same_room` when you want a soft prompt with the same -woman, man, and location as the hardcore prompt. +woman, man, and location as the hardcore prompt. In that setup, the softcore +prompt keeps the same listed adult cast physically present together in a +non-explicit teaser pose, with deterministic non-explicit partner outfits and a +shared cast pose. It outputs: diff --git a/caption_naturalizer.py b/caption_naturalizer.py index 0df719f..f13d6ee 100644 --- a/caption_naturalizer.py +++ b/caption_naturalizer.py @@ -485,17 +485,34 @@ def _insta_of_pair_from_row(row: dict[str, Any], detail_level: str, keep_style: soft_text, _soft_method = _metadata_to_prose(soft_row, detail_level, keep_style) hard_text, _hard_method = _metadata_to_prose(hard_row_for_text, detail_level, keep_style) descriptor = _clean_text(row.get("shared_descriptor")) + options = row.get("options") if isinstance(row.get("options"), dict) else {} cast_descriptors = row.get("shared_cast_descriptors") if isinstance(cast_descriptors, list): cast_descriptor_text = _human_join([_clean_text(item) for item in cast_descriptors if _clean_text(item)]) else: cast_descriptor_text = _clean_text(cast_descriptors) + same_soft_cast = options.get("softcore_cast") == "same_as_hardcore" + parts = [] - if cast_descriptor_text: + if cast_descriptor_text and same_soft_cast: parts.append(f"The shared cast descriptors are {cast_descriptor_text}") elif descriptor: - parts.append(f"The shared creator descriptor is {descriptor}") + parts.append(f"The softcore primary creator descriptor is {descriptor}") + if cast_descriptor_text and not same_soft_cast: + parts.append(f"The hardcore cast descriptors are {cast_descriptor_text}") + if same_soft_cast: + parts.append("The softcore version keeps the same adult cast present together in a non-explicit teaser setup") + partner_styling = row.get("softcore_partner_styling") + if isinstance(partner_styling, dict): + outfits = partner_styling.get("outfits") + if isinstance(outfits, list): + outfit_text = _human_join([_clean_text(item) for item in outfits if _clean_text(item)]) + if outfit_text: + parts.append(f"Softcore partner styling: {outfit_text}") + pose = _clean_text(partner_styling.get("pose")) + if pose: + parts.append(f"The shared softcore cast pose is {pose}") if soft_text: parts.append(f"Softcore version: {soft_text}") if hard_text: diff --git a/categories/sexual_poses.json b/categories/sexual_poses.json index 2c5510b..c19bd56 100644 --- a/categories/sexual_poses.json +++ b/categories/sexual_poses.json @@ -986,6 +986,10 @@ "bed-level" ], "body_contact": [ + "one hand gripping the sheets", + "hips lifted toward the camera", + "body arched on rumpled sheets", + "thighs tense against the bedding", "bodies still pressed together", "hands gripping hips", "thighs spread open", @@ -1015,7 +1019,8 @@ }, { "text": "wet orgasm during strap-on penetration", - "cast": "women_only" + "cast": "women_only", + "min_people": 2 }, { "text": "post-orgasm dripping arousal", @@ -1024,10 +1029,16 @@ "external cumshot", "visible external ejaculation", "messy post-climax release", - "orgasm during penetration", + { + "text": "orgasm during penetration", + "min_people": 2 + }, "post-orgasm hardcore climax", "visible orgasm aftermath", - "shared climax after penetration", + { + "text": "shared climax after penetration", + "min_people": 2 + }, "hardcore ejaculation scene" ], "expression_detail": [ diff --git a/krea_formatter.py b/krea_formatter.py index b048154..75d1229 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -299,10 +299,28 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) if options.get("softcore_cast") == "same_as_hardcore" else f"Woman A / primary creator: {descriptor}" ) + same_soft_cast = options.get("softcore_cast") == "same_as_hardcore" + soft_cast_presence = ( + "The same listed adult cast is present together in this softcore version in a non-explicit teaser pose, with no sex act or genital contact" + if same_soft_cast + else "The softcore version focuses on Woman A alone" + ) + partner_styling = row.get("softcore_partner_styling") + if isinstance(partner_styling, dict): + outfits = partner_styling.get("outfits") + partner_outfit_text = "; ".join(_clean(item) for item in outfits if _clean(item)) if isinstance(outfits, list) else "" + partner_pose = _clean(partner_styling.get("pose")) + else: + partner_outfit_text = "" + partner_pose = "" soft_parts = [ f"A visibly adult creator, {descriptor}", - f"Shared cast descriptors: {soft_cast_descriptor_text}" if soft_cast_descriptor_text else "", + f"Shared cast descriptors: {soft_cast_descriptor_text}" if same_soft_cast and soft_cast_descriptor_text else "", + f"Softcore primary creator descriptor: {soft_cast_descriptor_text}" if not same_soft_cast and soft_cast_descriptor_text else "", + soft_cast_presence, + f"Partner softcore styling: {partner_outfit_text}" if partner_outfit_text else "", + f"The shared softcore cast pose is {partner_pose}" if partner_pose else "", f"shown in a {soft_level or 'softcore'} Insta/OF creator image", f"wearing {soft.get('item')}" if soft.get("item") else "", f"{soft.get('pose')}" if soft.get("pose") else "", @@ -314,7 +332,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) ] hard_parts = [ f"The same visibly adult creator, {descriptor}, is the visually central woman in a consensual explicit adult {hard_level or 'hardcore'} scene", - f"Shared cast descriptors: {cast_descriptor_text}" if cast_descriptor_text else "", + f"{'Shared' if same_soft_cast else 'Hardcore'} cast descriptors: {cast_descriptor_text}" if cast_descriptor_text else "", f"all participants are 21+ and visibly adult; the cast includes {hard_cast_text}" if hard_cast_text else "all participants are 21+ and visibly adult", _clean(hard.get("role_graph")), f"The sexual action is {hard.get('item')}" if hard.get("item") else "", diff --git a/prompt_builder.py b/prompt_builder.py index 4c6e810..fa1e037 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -343,6 +343,29 @@ def _heuristic_cast_compatible(text: str, women_count: int, men_count: int) -> b if not text: return True total = women_count + men_count + if total == 1: + solo_blocked_terms = ( + "partner", + "partners", + "two bodies", + "three bodies", + "bodies still pressed", + "bodies pressed", + "bodies tangled", + "wet bodies", + "chests heaving together", + "straddling a partner", + "shared climax", + "between two", + "from both sides", + "front-and-back", + "body contact", + ) + if any(term in text for term in solo_blocked_terms): + return False + solo_toy_terms = ("toy", "dildo", "finger", "fingers", "self") + if "penetration" in text and not any(term in text for term in solo_toy_terms): + return False if total < 3 and "threesome" in text: return False if total != 3 and ("centered threesome" in text or "three-way" in text): @@ -395,6 +418,7 @@ def _heuristic_cast_compatible(text: str, women_count: int, men_count: int) -> b "blowjob", "fellatio", "deepthroat", + "ejaculation", "semen", ) if any(term in text for term in male_terms) and not any(term in text for term in toy_terms): @@ -1270,6 +1294,16 @@ def _role_graph( ] return f" {extra} {rng.choice(actions)}." + if people_count == 1: + solo = people[0] + if women_count == 1: + if "cumshot" in slug or "climax" in slug: + return f"{solo} is shown in a solo explicit climax pose with thighs open, one hand on her body, and visible arousal on skin and sheets." + return f"{solo} is shown in a solo explicit adult pose with self-touch, open body framing, and direct camera awareness." + if "cumshot" in slug or "climax" in slug: + return f"{solo} is shown in a solo explicit climax pose with one hand on his cock, body angled toward the camera, and visible ejaculation detail." + return f"{solo} is shown in a solo explicit adult pose with direct camera awareness and clear body framing." + if women_count > 0 and men_count == 0: a, b = _pick_distinct(rng, women, 2) c = any_woman({a, b}) if len(women) >= 3 else "" @@ -2074,6 +2108,40 @@ def _insta_of_cast_phrase(women_count: int, men_count: int) -> str: return context["cast_summary"] +SOFTCORE_CAST_POSES = [ + "standing together for a mirror selfie with bodies close but no sexual contact", + "posing shoulder-to-shoulder in a creator-shot group teaser", + "leaning together on the bed in a non-explicit subscriber preview", + "sitting close together with hands kept above clothing", + "arranged around Woman A in a flirtatious non-explicit teaser pose", + "posing in the same room as a coordinated adult creator set", + "standing near the phone tripod with relaxed teasing body language", + "framed together in a softcore cast reveal with no sex act", +] + + +def _insta_of_partner_styling( + seed_config: dict[str, int], + seed: int, + row_number: int, + women_count: int, + men_count: int, +) -> dict[str, Any]: + content_rng = _axis_rng(seed_config, "content", seed, row_number + 421) + pose_rng = _axis_rng(seed_config, "pose", seed, row_number + 421) + outfits: list[str] = [] + for index in range(max(0, women_count - 1)): + label = chr(ord("B") + index) + outfits.append(f"Woman {label} wears {g.choose(content_rng, g.WOMEN_CLOTHES_MINIMAL)}") + for index in range(max(0, men_count)): + label = chr(ord("A") + index) + outfits.append(f"Man {label} wears {g.choose(content_rng, g.MEN_CLOTHES_MINIMAL)}") + return { + "outfits": outfits, + "pose": g.choose(pose_rng, SOFTCORE_CAST_POSES), + } + + def _insta_of_active_trigger(prompt: str, trigger: str, enabled: bool) -> str: return _prepend_trigger(prompt, trigger, enabled) @@ -2167,6 +2235,16 @@ def build_insta_of_pair( if options["softcore_cast"] == "same_as_hardcore" else f"Woman A / primary creator: {descriptor}" ) + soft_partner_styling = _insta_of_partner_styling( + parsed_seed_config, + seed, + row_number, + hard_women_count if options["softcore_cast"] == "same_as_hardcore" else 1, + hard_men_count if options["softcore_cast"] == "same_as_hardcore" else 0, + ) + if options["softcore_cast"] != "same_as_hardcore": + soft_partner_styling = {"outfits": [], "pose": ""} + soft_partner_outfit_text = "; ".join(soft_partner_styling["outfits"]) platform_style = INSTA_OF_PLATFORM_STYLES[options["platform_style"]] soft_level = INSTA_OF_SOFT_LEVELS[options["softcore_level"]] hard_level = INSTA_OF_HARDCORE_LEVELS[options["hardcore_level"]] @@ -2186,12 +2264,24 @@ def build_insta_of_pair( if options["softcore_cast"] == "solo" else f"non-explicit teaser setup with the same adult cast as the hardcore version: {_insta_of_cast_phrase(hard_women_count, hard_men_count)}" ) + soft_cast_presence = ( + "Show the same listed adult cast together in the softcore version, present in the same location in a non-explicit teaser pose with no sex act or genital contact. " + if options["softcore_cast"] == "same_as_hardcore" + else "Keep the softcore version focused on Woman A alone. " + ) + soft_cast_styling_sentence = ( + f"Partner softcore styling: {soft_partner_outfit_text}. Shared softcore cast pose: {soft_partner_styling['pose']}. " + if options["softcore_cast"] == "same_as_hardcore" and soft_partner_outfit_text + else "" + ) hard_cast = _insta_of_cast_phrase(hard_women_count, hard_men_count) soft_prompt = ( f"Insta/OF softcore mode: {platform_style}. Shared primary creator descriptor: {descriptor}. " f"Softcore setup: {soft_level}. Cast continuity: {soft_cast}. " f"Shared cast descriptors: {soft_cast_descriptor_text}. " + f"{soft_cast_presence}" + f"{soft_cast_styling_sentence}" f"Outfit: {soft_row['item']}. Pose: {soft_row['pose']}. Setting: {soft_row['scene_text']}. " f"Facial expression: {soft_row['expression']}. Composition: {soft_row['composition']}. " f"{soft_camera_sentence}" @@ -2217,11 +2307,20 @@ def build_insta_of_pair( 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']}, " - f"{soft_camera_config['camera_mode'].replace('_', ' ')} camera" - ) + soft_caption_parts = [ + active_trigger, + "Insta/OF softcore mode", + descriptor, + soft_level, + soft_row["item"], + soft_row["pose"], + soft_partner_outfit_text, + soft_partner_styling["pose"], + soft_row["scene_text"], + soft_row["composition"], + f"{soft_camera_config['camera_mode'].replace('_', ' ')} camera", + ] + soft_caption = ", ".join(str(part).strip() for part in soft_caption_parts if str(part).strip()) hard_caption = ( f"{active_trigger}, Insta/OF hardcore mode, same primary creator descriptor, {descriptor}, " f"{hard_cast}, {hard_row['role_graph']}, {hard_row['item']}, {hard_scene}, {hard_composition}, " @@ -2232,6 +2331,7 @@ def build_insta_of_pair( "options": options, "shared_descriptor": descriptor, "shared_cast_descriptors": cast_descriptors, + "softcore_partner_styling": soft_partner_styling, "softcore_prompt": soft_prompt, "hardcore_prompt": hard_prompt, "softcore_negative_prompt": soft_negative,