diff --git a/krea_formatter.py b/krea_formatter.py index cfdffc9..c8b0ff5 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -55,7 +55,7 @@ HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS = ( (r"\bsitting on the edge of the bed\b", "sitting on a raised edge"), (r"\blying at the bed edge with thighs open\b", "lying near a raised edge with thighs open"), (r"\bedge[- ]of[- ]bed\b", "edge-supported"), - (r"\bbed[- ]edge\b", "edge-supported"), + (r"\bbed[- ]edge\b", "raised edge"), (r"\bedge of (?:the )?bed\b", "raised edge"), (r"\bbed edge\b", "raised edge"), (r"\bhands? braced on the bed\b", "hands braced beside the body"), @@ -544,6 +544,9 @@ def _mentions_rear_entry(text: str) -> bool: def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str: text = _position_context_text(role_graph, hard_item, composition, axis_values) item_text = " ".join(part for part in (_clean(hard_item).lower(), _axis_values_text(axis_values).lower()) if part) + position_text = "" + if isinstance(axis_values, dict): + position_text = _clean(axis_values.get("position", "")).lower() if not text: return "" if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)): @@ -582,10 +585,30 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "" if "kneeling" in text: return "kneeling front-and-back double-penetration pose" return "front-and-back double-penetration pose" - if "sixty-nine" in text: + if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text): return "sixty-nine oral pose" - if "face-sitting" in text: + if "face-sitting" in position_text or ("face-sitting" in text and not position_text): return "face-sitting oral pose" + if "side-lying oral" in position_text or (("side-lying oral position" in item_text or "side-lying oral" in text) and not position_text): + return "side-lying oral pose" + if ( + "edge-of-bed oral" in position_text + or "edge-supported oral" in position_text + or (("edge-of-bed oral position" in item_text or "edge-of-bed oral" in text or "edge-supported oral" in text) and not position_text) + ): + return "edge-supported oral pose" + if "standing oral" in position_text or (("standing oral position" in item_text or "standing oral" in text) and not position_text): + return "standing oral pose" + if "chair oral" in position_text or (("chair oral position" in item_text or "chair oral" in text) and not position_text): + return "chair oral pose" + if "kneeling oral" in position_text or (("kneeling oral position" in item_text or "kneeling oral" in text) and not position_text): + return "kneeling oral pose" + if "straddled oral" in position_text or (("straddled oral position" in item_text or "straddled oral" in text) and not position_text): + return "straddled cunnilingus pose" + if "reclining cunnilingus" in position_text or (("reclining cunnilingus position" in item_text or "reclining cunnilingus" in text) and not position_text): + return "reclining cunnilingus pose" + if "spread-leg oral" in position_text or (("spread-leg oral position" in item_text or "spread-leg oral" in text) and not position_text): + return "spread-leg oral pose" if "cunnilingus" in text or "pussy licking" in text or "mouth on her pussy" in text: if "reclining" in text: return "reclining cunnilingus pose" @@ -598,7 +621,7 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "" if "spread-leg oral position" in item_text: return "spread-leg oral pose" if "edge-of-bed oral position" in item_text: - return "edge-of-bed oral pose" + return "edge-supported oral pose" if "standing oral position" in item_text: return "standing oral pose" if "chair oral position" in item_text: @@ -610,7 +633,7 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "" if "side-lying" in text: return "side-lying oral pose" if "edge-of-bed" in text or "bed-edge" in text: - return "edge-of-bed oral pose" + return "edge-supported oral pose" if "spread-leg" in text: return "spread-leg oral pose" if "chair oral" in text: @@ -660,6 +683,9 @@ def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "" def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str: text = _position_context_text(anchor, f"{role_graph} {hard_item}", composition, axis_values) + position_text = "" + if isinstance(axis_values, dict): + position_text = _clean(axis_values.get("position", "")).lower() if not text: return "" mixed_woman_man = "the woman" in text and "the man" in text @@ -671,17 +697,21 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com def double_tail() -> str: return "" if "toy" in text else ", with the second penetration point aligned" - if "sixty-nine" in text: + if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text): return cast_phrase( "with the woman and man inverted head-to-hips so both mouths align with genitals", "with both bodies inverted head-to-hips so both mouths align with genitals", ) - if "face-sitting" in text: + if "face-sitting" in position_text or ("face-sitting" in text and not position_text): return cast_phrase( "with the man lying back while the woman straddles his face", "with one partner lying back while the other straddles the face", ) - if "reclining cunnilingus" in text or "spread-leg oral" in text: + if ( + "reclining cunnilingus" in position_text + or "spread-leg oral" in position_text + or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text) + ): if "takes the man's penis" in text or "penis in her mouth" in text: return cast_phrase( "with the man seated with legs apart and the woman positioned at his hips", @@ -691,22 +721,30 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com "with the woman lying back, thighs spread, and the man positioned between her legs", "with the receiving partner lying back, thighs spread, and the giver positioned between the legs", ) - if "straddled cunnilingus" in text or "straddled oral" in text: + if ( + "straddled oral" in position_text + or (("straddled cunnilingus" in text or "straddled oral" in text) and not position_text) + ): return cast_phrase( "with the woman straddling above the man's mouth and her thighs framing his face", "with the receiver straddling above the giver's mouth", ) - if "edge-of-bed oral" in text: + if ( + "edge-of-bed oral" in position_text + or "edge-supported oral" in position_text + or ("edge-of-bed oral" in text and not position_text) + or ("edge-supported oral" in text and not position_text) + ): if "takes the man's penis" in text or "penis in her mouth" in text: return cast_phrase( - "with the man at the bed edge and the woman kneeling at his hips", - "with the receiver at the bed edge and the giver positioned at hip height", + "with the man at a raised edge and the woman kneeling at his hips", + "with the receiver at a raised edge and the giver positioned at hip height", ) return cast_phrase( - "with the woman lying at the bed edge and the man positioned between her open thighs", - "with the receiver lying at the bed edge and the giver positioned between open thighs", + "with the woman lying at a raised edge and the man positioned between her open thighs", + "with the receiver lying at a raised edge and the giver positioned between open thighs", ) - if "standing oral" in text: + if "standing oral" in position_text or ("standing oral" in text and not position_text): if "takes the man's penis" in text or "penis in her mouth" in text: return cast_phrase( "with the man standing and the woman kneeling in front of his hips", @@ -716,7 +754,7 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com "with the woman standing braced and the man kneeling between her thighs", "with the receiver standing braced and the giver kneeling between the thighs", ) - if "chair oral" in text: + if "chair oral" in position_text or ("chair oral" in text and not position_text): if "takes the man's penis" in text or "penis in her mouth" in text: return cast_phrase( "with the man seated in the chair and the woman kneeling between his legs at hip level", @@ -726,9 +764,9 @@ def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, com "with one partner seated in a chair and the other kneeling between the open thighs", "with the receiver seated in a chair and the giver kneeling between the open thighs", ) - if "side-lying oral" in text: + if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text): return "with both bodies lying on their sides and mouth aligned to genitals" - if "kneeling oral" in text: + if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text): if "takes the man's penis" in text or "penis in her mouth" in text: return cast_phrase( "with the woman kneeling in front of the man's hips, her mouth at penis level", @@ -917,7 +955,7 @@ def _hardcore_item_detail(hard_item: str) -> str: r"missionary position|cowgirl position|reverse cowgirl position|doggy style position|" r"standing sex position|spooning sex position|edge-of-bed position|kneeling straddle position|" r"lotus sex position|bent-over position|kneeling oral position|face-sitting position|" - r"sixty-nine position|edge-of-bed oral position|standing oral position|reclining cunnilingus position|" + r"sixty-nine position|edge-of-bed oral position|edge-supported oral position|standing oral position|reclining cunnilingus position|" r"straddled oral position|side-lying oral position|spread-leg oral position|chair oral position" ) text = re.sub( @@ -962,7 +1000,8 @@ def _dedupe_hardcore_detail(detail: str, anchor: str) -> str: r"spread-leg oral position", r"chair oral position", ), - "edge-of-bed oral": (r"edge-of-bed oral position",), + "edge-supported oral": (r"edge-of-bed oral position", r"edge-supported oral position"), + "edge-of-bed oral": (r"edge-of-bed oral position", r"edge-supported oral position"), "standing oral": (r"standing oral position",), "spread-leg oral": (r"spread-leg oral position",), "chair oral": (r"chair oral position",), diff --git a/prompt_builder.py b/prompt_builder.py index dc8a66d..3e107cb 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -6,7 +6,7 @@ import random import re from pathlib import Path from string import Formatter -from typing import Any +from typing import Any, Callable try: from . import generate_prompt_batches as g @@ -327,7 +327,10 @@ HARDCORE_POSITION_KEY_CHOICES = [ "lotus_lap", "face_sitting", "sixty_nine", + "reclining_oral", + "straddled_oral", "spread_leg_oral", + "chair_oral", "open_thighs", "front_back", ] @@ -361,7 +364,10 @@ HARDCORE_POSITION_KEY_MATCHES = { "lotus_lap": ("lotus", "lap", "seated in a partner's lap"), "face_sitting": ("face-sitting", "face sitting"), "sixty_nine": ("sixty-nine", "69"), + "reclining_oral": ("reclining cunnilingus",), + "straddled_oral": ("straddled oral",), "spread_leg_oral": ("spread-leg", "spread leg", "reclining cunnilingus"), + "chair_oral": ("chair oral",), "open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"), "front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"), } @@ -863,7 +869,7 @@ HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS = ( (r"\bsitting on the edge of the bed\b", "sitting on a raised edge"), (r"\blying at the bed edge with thighs open\b", "lying near a raised edge with thighs open"), (r"\bedge[- ]of[- ]bed\b", "edge-supported"), - (r"\bbed[- ]edge\b", "edge-supported"), + (r"\bbed[- ]edge\b", "raised edge"), (r"\bedge of (?:the )?bed\b", "raised edge"), (r"\bbed edge\b", "raised edge"), (r"\bhands? braced on the bed\b", "hands braced beside the body"), @@ -960,6 +966,33 @@ def _merged_axes(category: dict[str, Any], subcategory: dict[str, Any], item: An return axes +def _oral_acts_for_position(values: list[Any], position: str) -> list[Any]: + position_text = str(position or "").lower() + if not position_text: + return values + + def act_text(value: Any) -> str: + return _entry_text(value).lower() + + def filtered(predicate: Callable[[str], bool]) -> list[Any]: + matches = [value for value in values if predicate(act_text(value))] + return matches or values + + penis_terms = ("fellatio", "blowjob", "deepthroat", "penis sucking", "penis in mouth") + cunnilingus_terms = ("cunnilingus", "pussy licking", "tongue on pussy", "oral sex with tongue and fingers", "mouth on genitals") + if "sixty-nine" in position_text: + return filtered(lambda text: "sixty-nine" in text) + if "face-sitting" in position_text: + return filtered(lambda text: "face-sitting" in text or any(term in text for term in cunnilingus_terms)) + if "straddled oral" in position_text or "reclining cunnilingus" in position_text: + return filtered(lambda text: "sixty-nine" not in text and not any(term in text for term in penis_terms)) + if "spread-leg oral" in position_text: + return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text) + if any(term in position_text for term in ("standing oral", "kneeling oral", "edge-of-bed oral", "chair oral", "side-lying oral")): + return filtered(lambda text: "sixty-nine" not in text and "face-sitting" not in text) + return values + + def _compose_item( rng: random.Random, category: dict[str, Any], @@ -972,12 +1005,19 @@ def _compose_item( 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] - } + fields = [key for _, key, _, _ in Formatter().parse(template) if key] + unique_fields = list(dict.fromkeys(fields)) + axis_values: dict[str, str] = {} + if str(subcategory.get("slug") or "").lower() == "oral_sex" and "position" in unique_fields and axes.get("position"): + position_values = _compatible_entries(axes["position"], women_count, men_count) + axis_values["position"] = _entry_text(_weighted_choice(rng, position_values)) + for name in unique_fields: + if name in axis_values or name not in axes or not axes[name]: + continue + values = _compatible_entries(axes[name], women_count, men_count) + if str(subcategory.get("slug") or "").lower() == "oral_sex" and name == "oral_act": + values = _oral_acts_for_position(values, axis_values.get("position", "")) + axis_values[name] = _entry_text(_weighted_choice(rng, values)) item_text = _format(template, axis_values).strip() item_name = _item_name(item) or subcategory["name"] return item_text, item_name, axis_values @@ -1736,6 +1776,12 @@ def _hardcore_position_config_active(config: dict[str, Any]) -> bool: return bool(config.get("enabled")) +def _hardcore_position_template_required(config: dict[str, Any]) -> bool: + if not _hardcore_position_config_active(config): + return False + return bool(config.get("positions")) or _normalize_hardcore_position_family(config.get("family")) != "any" + + def _is_hardcore_sexual_category(category: dict[str, Any]) -> bool: return str(category.get("slug") or "").strip() == "hardcore_sexual_poses" or str(category.get("name") or "").strip().lower() == "hardcore sexual poses" @@ -1841,7 +1887,7 @@ def _hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> bo def _hardcore_subcategory_supports_positions(subcategory: dict[str, Any], config: dict[str, Any]) -> bool: - if not config.get("positions"): + if not _hardcore_position_template_required(config): return True axes = subcategory.get("item_axes") if not isinstance(axes, dict): @@ -1875,7 +1921,7 @@ def _filter_hardcore_templates(templates: list[Any], config: dict[str, Any]) -> for template in templates: text = _entry_text(template) fields = {key for _, key, _, _ in Formatter().parse(text) if key} - blocked = bool(config.get("positions")) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS) + blocked = _hardcore_position_template_required(config) and not bool(fields & HARDCORE_POSITION_AXIS_KEYS) blocked = blocked or any(_hardcore_text_blocked_by_action(text, field, config) for field in fields | {""}) if not blocked: filtered.append(template) @@ -4881,6 +4927,91 @@ def _role_graph( return f"{woman} kneels forward with hips raised while {man} kneels behind her and thrusts his penis into her ass." return f"{woman} is on all fours with hips raised while {man} is positioned behind her and thrusts his penis into her ass." + def oral_position_graph(woman: str, man: str) -> str: + position_text = str((item_axis_values or {}).get("position") or "").lower() + text = " ".join( + str(part or "").lower() + for part in ( + item_text, + *((item_axis_values or {}).values()), + ) + ) + woman_gives = any( + term in text + for term in ( + "fellatio", + "blowjob", + "deepthroat", + "penis sucking", + "penis in mouth", + "penis in her mouth", + "mouth stretched around a penis", + "lips wrapped", + ) + ) + man_gives = any( + term in text + for term in ( + "cunnilingus", + "pussy licking", + "tongue on pussy", + "mouth on pussy", + "pussy and tongue", + "face-sitting", + "tongue contact clearly visible", + ) + ) + if "mouth on genitals" in text and not woman_gives and not man_gives: + if any(term in text for term in ("face-sitting", "reclining", "straddled", "spread-leg", "open thighs")): + man_gives = True + else: + woman_gives = True + + if "sixty-nine" in position_text or ("sixty-nine" in text and not position_text): + return f"{woman} and {man} lie head-to-hips in a sixty-nine position, with {woman}'s mouth on {man}'s penis and {man}'s mouth on {woman}'s pussy." + if "face-sitting" in position_text or ("face-sitting" in text and not position_text): + return f"{man} lies on his back while {woman} straddles his face with her thighs around his head and {man}'s mouth pressed to her pussy." + if "straddled oral" in position_text or ("straddled oral" in text and not position_text): + if woman_gives and not man_gives: + return f"{man} straddles forward near {woman}'s face while {woman} kneels below him with her mouth on his penis." + return f"{woman} straddles above {man}'s face with her thighs framing his head while {man}'s mouth stays pressed to her pussy." + if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text): + if woman_gives and not man_gives: + return f"{man} lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes his penis in her mouth." + return f"{woman} lies on her side with her top thigh lifted while {man} lies beside her hips with his mouth pressed to her pussy." + if ( + "edge-of-bed oral" in position_text + or "edge of bed oral" in position_text + or "edge-supported oral" in position_text + or (("edge-of-bed oral" in text or "edge of bed oral" in text or "edge-supported oral" in text) and not position_text) + ): + if woman_gives and not man_gives: + return f"{man} sits at a raised edge with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." + return f"{woman} lies at a raised edge with thighs open while {man} kneels between her legs with his mouth on her pussy." + if "standing oral" in position_text or ("standing oral" in text and not position_text): + if man_gives and not woman_gives: + return f"{woman} stands braced with one thigh lifted while {man} kneels between her legs with his mouth on her pussy." + return f"{man} stands with hips forward while {woman} kneels in front of him at hip height and takes his penis in her mouth." + if "chair oral" in position_text or ("chair oral" in text and not position_text): + if man_gives and not woman_gives: + return f"{woman} sits in a chair with thighs open while {man} kneels between her legs with his mouth pressed to her pussy." + return f"{man} sits in a chair with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." + if ( + "reclining cunnilingus" in position_text + or "spread-leg oral" in position_text + or (("reclining cunnilingus" in text or "spread-leg oral" in text) and not position_text) + ): + if woman_gives and not man_gives: + return f"{man} reclines with legs apart while {woman} kneels between his thighs and takes his penis in her mouth." + return f"{woman} reclines on her back with thighs spread while {man} kneels between her legs with his mouth on her pussy." + if "kneeling oral" in position_text or ("kneeling oral" in text and not position_text): + if man_gives and not woman_gives: + return f"{woman} kneels with thighs parted and hips angled forward while {man} kneels in front of her with his mouth on her pussy." + return f"{woman} kneels in front of {man}'s hips with her mouth at penis level while {man} stands or sits close above her." + if man_gives and not woman_gives: + return f"{woman} lies on her back with thighs open while {man} kneels between her legs with his mouth pressed to her pussy." + return f"{woman} kneels in front of {man}'s hips and takes his penis in her mouth while {man} keeps his hips aligned with her face." + if people_count == 1: solo = people[0] if women_count == 1: @@ -4932,22 +5063,7 @@ def _role_graph( man = any_man() third = any_person({woman, man}) if people_count >= 3 else "" if "oral" in slug: - if "sixty-nine" in item_text or ("blowjob" in item_text and ("cunnilingus" in item_text or "pussy" in item_text)): - graph = f"{woman} has {man}'s penis in her mouth while {man} uses his mouth on {woman}'s pussy, with both mouths pressed to genitals." - elif any( - term in item_text - for term in ( - "cunnilingus", - "pussy licking", - "tongue on pussy", - "mouth on pussy", - "pussy and tongue", - "tongue contact", - ) - ) or ("pussy" in item_text and "penis" not in item_text): - graph = f"{man} gives oral to {woman}, mouth on her pussy while {woman}'s thighs are held open for the camera." - else: - graph = f"{woman} takes {man}'s penis in her mouth while {man} holds her hair and hips." + graph = oral_position_graph(woman, man) elif "anal" in slug or "double" in slug: if "double" in item_text or "toy" in item_text: if people_count >= 3: