diff --git a/krea_formatter.py b/krea_formatter.py index 467c885..41b2951 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -218,12 +218,12 @@ def _cast_prose(text: str, central_label: str = "Woman A") -> tuple[str, list[st return (f"{central_label} is {_clean(text)}" if _clean(text) else "", []) labels = [label for label, _descriptor in entries] if labels == ["Woman A"]: - return f"A {entries[0][1]}", labels + return _with_indefinite_article(entries[0][1]), labels if labels == ["Man A"]: - return f"A {entries[0][1]}", labels + return _with_indefinite_article(entries[0][1]), labels if set(labels) == {"Woman A", "Man A"} and len(labels) == 2: by_label = {label: descriptor for label, descriptor in entries} - return f"A {by_label['Woman A']} alongside a {by_label['Man A']}", labels + return f"{_with_indefinite_article(by_label['Woman A'])} alongside {_with_indefinite_article(by_label['Man A'])}", labels sentences = [] for label, descriptor in entries: sentences.append(f"{label} is {descriptor}.") @@ -239,9 +239,14 @@ def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str: if len(labels) < 3: text = re.sub(r"\s*(?:while|as)\s+another partner watches\b", "", text, flags=re.IGNORECASE) text = re.sub(r"\banother partner watches\b", "", text, flags=re.IGNORECASE) + text = re.sub(r",?\s*\bone partner held between two bodies\b", "", text, flags=re.IGNORECASE) + text = re.sub(r",?\s*\bthree bodies locked together\b", "", text, flags=re.IGNORECASE) + text = re.sub(r",?\s*\bthree bodies\b", "", text, flags=re.IGNORECASE) + text = re.sub(r"\bwith\s*,\s*", "with ", text, flags=re.IGNORECASE) text = re.sub(r"\bwhile blowjob\b", "during a blowjob", text, flags=re.IGNORECASE) text = re.sub(r"\bfeaturing blowjob\b", "featuring a blowjob", text, flags=re.IGNORECASE) text = re.sub(r"\s+,", ",", text) + text = re.sub(r",\s*,", ",", text) text = re.sub(r"\s{2,}", " ", text).strip(" ,") return text @@ -275,7 +280,227 @@ def _natural_clothing_state(text: Any) -> str: return text -def _hardcore_action_sentence(role_graph: str, hard_item: str) -> str: +def _hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "") -> str: + text = " ".join(_clean(part).lower() for part in (role_graph, hard_item, composition) if _clean(part)) + item_text = _clean(hard_item).lower() + if not text: + return "" + if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text: + if "bed-edge" in text or "edge-of-bed" in text: + return "bed-edge front-and-back double-penetration pose" + if "standing supported" in text: + return "standing supported front-and-back double-penetration pose" + if "kneeling" in text: + return "kneeling front-and-back double-penetration pose" + return "front-and-back double-penetration pose" + if "face-sitting" in text: + return "face-sitting oral pose" + if "sixty-nine" in text: + return "sixty-nine 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" + if "straddled" in text: + return "straddled cunnilingus pose" + return "open-thigh cunnilingus pose" + if "oral" in text or "blowjob" in text or "penis in her mouth" in text or "penis in mouth" in text: + if "side-lying oral position" in item_text: + return "side-lying oral pose" + 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" + if "standing oral position" in item_text: + return "standing oral pose" + if "chair oral position" in item_text: + return "chair oral pose" + if "kneeling oral position" in item_text or "kneeling" in text: + return "kneeling oral pose" + if "standing" in text: + return "standing oral pose" + 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" + if "spread-leg" in text: + return "spread-leg oral pose" + if "chair oral" in text: + return "chair oral pose" + return "mouth-to-genitals oral pose" + if "anal" in text or "ass" in text or "rear-entry" in text: + if "bed-edge" in text or "edge-of-bed" in text: + return "bed-edge rear-entry anal pose" + if "bent-over" in text: + return "bent-over rear-entry anal pose" + if "doggy" in text: + return "doggy-style anal pose" + return "rear-entry anal pose" + positions = ( + "missionary", + "cowgirl", + "reverse cowgirl", + "doggy style", + "standing sex", + "spooning sex", + "edge-of-bed", + "kneeling straddle", + "lotus", + "bent-over", + ) + for position in positions: + if position in text: + return f"{position.replace('doggy style', 'doggy-style')} pose" + if "threesome" in text or "three-body" in text: + return "three-body explicit sex pose" + if "group" in text or "orgy" in text: + return "multi-body explicit sex pose" + if "penetrat" in text or "thrust" in text: + return "hip-aligned penetrative sex pose" + return "" + + +def _hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, composition: str = "") -> str: + text = " ".join(_clean(part).lower() for part in (anchor, role_graph, hard_item, composition) if _clean(part)) + if not text: + return "" + if "double-penetration" in text or "double penetration" in text: + if "toy" in text: + return "with the woman's hips spread and front-and-back alignment visible" + return "with the central body held between front-and-back contact" + if "anal" in text or "rear-entry" in text: + return "with the woman's hips raised, ass exposed, and penetration alignment visible" + if "cunnilingus" in text or "mouth on her pussy" in text or "pussy licking" in text: + return "with the woman's thighs open and the giver's mouth pressed to her pussy" + if "oral" in text or "blowjob" in text or "penis in her mouth" in text or "penis in mouth" in text: + if "sixty-nine" in text: + return "with both bodies arranged mouth-to-genitals" + if "takes the man's penis in her mouth" in text or "penis in her mouth" in text: + return "with the woman's mouth close to the man's hips" + return "with mouth and genitals aligned clearly" + if "threesome" in text or "three-body" in text: + return "with all three adult bodies clearly placed around the central subject" + if "group" in text or "orgy" in text: + return "with each adult body readable in the shared sex act" + if "penetrat" in text or "thrust" in text: + return "with hips aligned and legs open around the contact point" + return "" + + +def _hardcore_item_detail(hard_item: str) -> str: + text = _clean(hard_item).rstrip(".") + if not text: + return "" + text = re.sub(r"^hardcore\s+", "", text, flags=re.IGNORECASE) + text = re.sub(r"^explicit\s+", "", text, flags=re.IGNORECASE) + text = re.sub(r"^(?:mouth-to-genitals|double-contact sex|adult group pile|sex pile)\s+pose:\s*", "", text, flags=re.IGNORECASE) + text = re.sub(r"^(?:oral|threesome|orgy)\s+scene\s+with\s+", "", text, flags=re.IGNORECASE) + text = re.sub(r"^(?:threesome|orgy)\s+pose:\s*", "", text, flags=re.IGNORECASE) + act_patterns = ( + r"(?:penis and toy|toy and strap-on|toy-assisted|front-and-back|hardcore|deep|kneeling|standing supported)?\s*double penetration", + r"toy-assisted vaginal and anal penetration at the same time", + r"vaginal and anal penetration at the same time", + r"one penis in pussy and one penis in ass", + r"anal penetration with visible genital contact", + r"rear-entry anal penetration", + r"anal sex with spread cheeks", + r"ass stretched around a penis", + r"penis entering ass", + r"deep anal sex", + r"bent-over anal sex", + r"hardcore anal thrusting", + r"vaginal penetration with visible genital contact", + r"penis entering pussy", + r"pussy stretched around a penis", + r"deep vaginal sex", + r"explicit penetrative sex", + r"hardcore vaginal thrusting", + r"full-body penetrative sex", + r"close-contact vaginal sex", + r"fellatio with penis in mouth", + r"deepthroat blowjob", + r"blowjob", + r"penis sucking with visible saliva", + r"cunnilingus with tongue on pussy", + r"face-sitting cunnilingus", + r"pussy licking with thighs spread", + r"oral sex with tongue and fingers", + r"mouth on genitals with explicit contact", + r"sixty-nine oral sex", + ) + act_pattern = "|".join(act_patterns) + position_pattern = ( + 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"straddled oral position|side-lying oral position|spread-leg oral position|chair oral position" + ) + text = re.sub( + rf"^({position_pattern})\s+(?:while|with|featuring)\s+(?:{act_pattern})\s*,?\s*", + r"\1, ", + text, + flags=re.IGNORECASE, + ) + text = re.sub( + rf"^(?:{act_pattern})\s*(?:in|from|on|with|while|featuring)?\s*", + "", + text, + flags=re.IGNORECASE, + ) + text = re.sub(r"^(?:position|pose)\s+", "", text, flags=re.IGNORECASE) + text = re.sub(r"^with\s+", "", text, flags=re.IGNORECASE) + text = re.sub(r"\bwith with\b", "with", text, flags=re.IGNORECASE) + text = re.sub(r",\s*with\s+", ", ", text, flags=re.IGNORECASE) + text = re.sub(r",\s+and\s+", ", ", text) + text = re.sub(r"\s*,\s*", ", ", text).strip(" ,;") + return _clean(text) + + +def _dedupe_hardcore_detail(detail: str, anchor: str) -> str: + detail = _clean(detail) + anchor_lower = anchor.lower() + duplicate_phrases = { + "front-and-back": (r"front-and-back contact",), + "side-lying oral": (r"side-lying oral position",), + "kneeling oral": (r"kneeling oral position",), + "face-sitting": (r"face-sitting position",), + "sixty-nine": ( + r"sixty-nine position", + r"sixty-nine oral sex", + r"kneeling oral position", + r"face-sitting position", + r"edge-of-bed oral position", + r"standing oral position", + r"reclining cunnilingus position", + r"straddled oral position", + r"side-lying oral position", + r"spread-leg oral position", + r"chair oral position", + ), + "edge-of-bed oral": (r"edge-of-bed oral position",), + "standing oral": (r"standing oral position",), + "spread-leg oral": (r"spread-leg oral position",), + "chair oral": (r"chair oral position",), + "reclining cunnilingus": (r"reclining cunnilingus position",), + "straddled cunnilingus": (r"straddled oral position", r"straddled cunnilingus position"), + "open-thigh cunnilingus": (r"reclining cunnilingus position", r"straddled cunnilingus position"), + "bent-over": (r"bent-over position",), + "missionary": (r"missionary position",), + "cowgirl": (r"cowgirl position",), + "reverse cowgirl": (r"reverse cowgirl position",), + "doggy-style": (r"doggy style position",), + "edge-of-bed": (r"edge-of-bed position",), + } + for anchor_token, phrases in duplicate_phrases.items(): + if anchor_token in anchor_lower: + for phrase in phrases: + detail = re.sub(rf"\b{phrase}\b,?\s*", "", detail, flags=re.IGNORECASE) + detail = re.sub(r"^\s*,\s*", "", detail) + detail = re.sub(r",\s*,", ",", detail) + return _clean(detail).strip(" ,;") + + +def _hardcore_action_sentence(role_graph: str, hard_item: str, composition: str = "") -> str: role_graph = _clean(role_graph).rstrip(".") hard_item = _clean(hard_item).rstrip(".") role_graph = re.sub( @@ -284,12 +509,30 @@ def _hardcore_action_sentence(role_graph: str, hard_item: str) -> str: role_graph, flags=re.IGNORECASE, ) + role_graph = re.sub( + r"\bthe man thrusts his penis into the woman while a toy adds a second penetration point\b", + "the man's penis thrusts into the woman while a toy adds the second penetration point", + role_graph, + flags=re.IGNORECASE, + ) + role_graph = re.sub( + r"\bthe man thrusts his penis into the woman\b", + "the man's penis thrusts into the woman", + role_graph, + flags=re.IGNORECASE, + ) role_graph = re.sub( r"\bthe man penetrates the woman anally\b", "the man's penis thrusts into the woman's ass", role_graph, flags=re.IGNORECASE, ) + role_graph = re.sub( + r"\bthe man thrusts his penis into the woman's ass\b", + "the man's penis thrusts into the woman's ass", + role_graph, + flags=re.IGNORECASE, + ) role_graph = re.sub( r"\bthe man penetrates the woman\b", "the man's penis thrusts into the woman", @@ -308,13 +551,52 @@ def _hardcore_action_sentence(role_graph: str, hard_item: str) -> str: role_graph, flags=re.IGNORECASE, ) - if hard_item and role_graph: - return f"Explicit hardcore action: {role_graph}; {hard_item}" - if role_graph: - return f"Explicit hardcore action: {role_graph}" - if hard_item: - return f"Explicit hardcore action: {hard_item}" - return "" + detail = _hardcore_item_detail(hard_item) + anchor = _hardcore_pose_anchor(role_graph, hard_item, composition) + detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail + arrangement = _hardcore_pose_arrangement(anchor, role_graph, hard_item, composition) + anchor_phrase = _with_indefinite_article(anchor) if anchor else "" + if arrangement and anchor_phrase: + anchor_phrase = f"{anchor_phrase} {arrangement}" + if role_graph and anchor_phrase: + sentence = f"In {anchor_phrase}, {role_graph}" + elif role_graph: + sentence = role_graph + elif detail and anchor_phrase: + sentence = f"In {anchor_phrase}, {detail}" + detail = "" + else: + sentence = detail or hard_item + detail = "" + if detail: + sentence = f"{sentence}; {detail}" + return sentence + + +def _composition_phrase(composition: Any, action: str = "", prefix: str = "framed as") -> str: + composition = _clean(composition) + if not composition: + return "" + action_lower = _clean(action).lower() + composition_lower = composition.lower() + oral_pose_tokens = ( + "kneeling oral", + "side-lying oral", + "spread-leg oral", + "standing oral", + "edge-of-bed oral", + "face-sitting", + "sixty-nine", + "reclining cunnilingus", + "straddled oral", + "chair oral", + ) + if "oral" in action_lower: + composition_oral_tokens = [token for token in oral_pose_tokens if token in composition_lower] + if composition_oral_tokens and not any(token in action_lower for token in composition_oral_tokens): + match = re.search(r"\bwith\s+(.+)$", composition, flags=re.IGNORECASE) + return f"framed with {match.group(1)}" if match else "" + return f"{prefix} {composition}" def _clean_age(age: Any) -> str: @@ -454,9 +736,11 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) cast_prose, cast_labels = _cast_prose(cast_descriptor_text) if not cast_labels and women_count == 1 and men_count == 1: cast_labels = ["Woman A", "Man A"] - role_graph = _natural_label_text(_clean(row.get("role_graph")), cast_labels) + role_graph = _sanitize_scene_text_for_cast(row.get("role_graph"), cast_labels) + item = _sanitize_scene_text_for_cast(item, cast_labels) + role_graph = _natural_label_text(role_graph, cast_labels) item = _natural_label_text(item, cast_labels) - action = _hardcore_action_sentence(role_graph, item) + action = _hardcore_action_sentence(role_graph, item, composition) parts = [ action, cast_prose, @@ -464,7 +748,7 @@ def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) f"The cast includes {cast}" if cast and not cast_prose and not (women_count == 1 and men_count == 1) else "", f"The setting is {scene}" if scene else "", f"Facial expressions are {expression}" if expression else "", - f"The image is framed as {composition}" if composition else "", + _composition_phrase(composition, action, "The image is framed as"), camera, style if detail_level != "concise" else "", ] @@ -550,7 +834,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) hard_role_graph = _sanitize_scene_text_for_cast(hard.get("role_graph"), hard_labels) hard_item = _natural_label_text(hard_item, hard_labels) hard_role_graph = _natural_label_text(hard_role_graph, hard_labels) - hard_action = _hardcore_action_sentence(hard_role_graph, hard_item) + hard_action = _hardcore_action_sentence(hard_role_graph, hard_item, hard_composition) same_soft_cast = options.get("softcore_cast") == "same_as_hardcore" soft_cast_presence = ( f"{_label_join(soft_labels)} are together in a non-explicit teaser pose, with no sex act or genital contact" @@ -586,7 +870,7 @@ def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) hard_cast_prose, f"set in {hard_scene}" if hard_scene else "", f"with {hard.get('expression')}" if hard.get("expression") else "", - f"framed as {hard_composition}" if hard_composition else "", + _composition_phrase(hard_composition, hard_action), hard_camera, hard_style if detail_level != "concise" else "", ]