1911 lines
86 KiB
Python
1911 lines
86 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
|
|
|
|
TRIGGER_CANDIDATES = (
|
|
"sxcpinup_coloredpencil",
|
|
"sxcppnl7",
|
|
)
|
|
HARDCORE_DETAIL_DENSITY_CHOICES = {"compact", "balanced", "dense"}
|
|
|
|
PROMPT_FIELD_LABELS = (
|
|
"Ages",
|
|
"Body types",
|
|
"Cast",
|
|
"Cast descriptors",
|
|
"Characters",
|
|
"Scene",
|
|
"Setting",
|
|
"Pose",
|
|
"Sexual pose",
|
|
"Facial expression",
|
|
"Facial expressions",
|
|
"Clothing",
|
|
"Erotic outfit",
|
|
"Prop/detail",
|
|
"Composition",
|
|
"Role graph",
|
|
"Use",
|
|
"Avoid",
|
|
)
|
|
|
|
|
|
def _clean(value: Any) -> str:
|
|
text = "" if value is None else str(value)
|
|
text = text.replace("\n", " ")
|
|
text = re.sub(r"\s+", " ", text).strip()
|
|
text = re.sub(r"\s+([,.;:])", r"\1", text)
|
|
return text
|
|
|
|
|
|
HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS = (
|
|
(r"\bstacked bodies on the bed\b", "close body alignment"),
|
|
(r"\bstacked bodies with close body alignment\b", "close body alignment"),
|
|
(r"\boverhead tangled-body anal frame\b", "overhead rear-entry anal frame"),
|
|
(r"\btangled-body\b", "close-body"),
|
|
(r"\bthree bodies tangled on the bed\b", "three bodies tangled in close contact"),
|
|
(r"\ba triangle of bodies on the mattress\b", "a triangle of bodies in close contact"),
|
|
(r"\bbodies tangled on the sheets\b", "bodies tangled in close contact"),
|
|
(r"\bwet bodies tangled on sheets\b", "wet bodies tangled in close contact"),
|
|
(r"\bbody arched on rumpled sheets\b", "body arched with clear skin contact"),
|
|
(r"\bface-down ass-up on the mattress\b", "face-down ass-up position"),
|
|
(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", "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"),
|
|
(r"\bone hand pressing into the mattress\b", "one hand braced beside the body"),
|
|
(r"\bone foot planted on the bed\b", "one foot planted for leverage"),
|
|
(r"\bfingers gripping sheets and skin\b", "fingers gripping skin"),
|
|
(r"\bfingers gripping sheets\b", "fingers gripping skin"),
|
|
(r"\bhands gripping sheets\b", "hands gripping skin"),
|
|
(r"\bone hand gripping the sheets\b", "one hand gripping skin"),
|
|
(r"\brumpled bed sheets\b", "wrinkled body-contact fabric"),
|
|
(r"\bwet sheets beneath the bodies\b", "visible wetness beneath the bodies"),
|
|
(r"\bsexual fluids on sheets\b", "sexual fluids visible on skin"),
|
|
(r"\bcum dripping onto sheets\b", "cum visible on skin"),
|
|
(r"\bfluid dripping onto sheets\b", "fluid visible on skin"),
|
|
(r"\bsquirting fluid on the sheets\b", "squirting fluid visible on skin"),
|
|
(r"\bsoft sheets\b", "soft fabric"),
|
|
(r"\bsilk sheets\b", "silk fabric"),
|
|
(r"\bsheets\b", "fabric"),
|
|
(r"\bmattress\b", "low support surface"),
|
|
(r"\ba low support surface\b", "a low body support"),
|
|
(r"\ba low mattress\b", "a low body support"),
|
|
(r"\ba wide couch\b", "a wide body support"),
|
|
(r"\bwide couch\b", "wide body support"),
|
|
(r"\bcouch\b", "body support"),
|
|
(r"\bsofa\b", "body support"),
|
|
(r"\bon the bed\b", "on a body support"),
|
|
(r"\bon a bed\b", "on a body support"),
|
|
(r"\bbedroom-floor\b", "floor-level"),
|
|
(r"\bbedroom floor\b", "floor-level"),
|
|
)
|
|
|
|
|
|
def _sanitize_hardcore_environment_anchors(value: Any) -> str:
|
|
text = _clean(value)
|
|
if not text:
|
|
return ""
|
|
for pattern, replacement in HARDCORE_ENVIRONMENT_ANCHOR_REPLACEMENTS:
|
|
text = re.sub(pattern, replacement, text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\s+,", ",", text)
|
|
text = re.sub(r",\s*,", ",", text)
|
|
text = re.sub(r"\s{2,}", " ", text)
|
|
return text.strip()
|
|
|
|
|
|
def _sanitize_hardcore_axis_values(values: Any) -> dict[str, str]:
|
|
if not isinstance(values, dict):
|
|
return {}
|
|
return {str(key): _sanitize_hardcore_environment_anchors(value) for key, value in values.items()}
|
|
|
|
|
|
def _is_false(value: Any) -> bool:
|
|
if isinstance(value, bool):
|
|
return value is False
|
|
if isinstance(value, str):
|
|
return value.strip().lower() in ("false", "0", "no", "off")
|
|
return False
|
|
|
|
|
|
def _expression_disabled(row: dict[str, Any]) -> bool:
|
|
return bool(row.get("expression_disabled")) or _is_false(row.get("expression_enabled", True))
|
|
|
|
|
|
def _normalize_hardcore_detail_density(value: Any) -> str:
|
|
text = _clean(value).lower()
|
|
return text if text in HARDCORE_DETAIL_DENSITY_CHOICES else "balanced"
|
|
|
|
|
|
def _sentence(text: str) -> str:
|
|
text = _clean(text).strip(" ,;")
|
|
if not text:
|
|
return ""
|
|
text = text[:1].upper() + text[1:]
|
|
if text[-1] not in ".!?":
|
|
text += "."
|
|
return text
|
|
|
|
|
|
def _paragraph(parts: list[str]) -> str:
|
|
return " ".join(part for part in (_sentence(part) for part in parts) if part)
|
|
|
|
|
|
def _with_indefinite_article(text: str) -> str:
|
|
text = _clean(text)
|
|
if not text or text.lower().startswith(("a ", "an ")):
|
|
return text
|
|
article = "an" if text[:1].lower() in "aeiou" else "a"
|
|
return f"{article} {text}"
|
|
|
|
|
|
def _maybe_json(text: str) -> dict[str, Any] | None:
|
|
text = _clean(text)
|
|
if not text.startswith("{"):
|
|
return None
|
|
try:
|
|
value = json.loads(text)
|
|
except json.JSONDecodeError:
|
|
return None
|
|
return value if isinstance(value, dict) else None
|
|
|
|
|
|
def _row_from_inputs(source_text: str, metadata_json: str, input_hint: str) -> tuple[dict[str, Any] | None, str]:
|
|
candidates: list[tuple[str, str]] = []
|
|
if input_hint in ("auto", "metadata_json"):
|
|
candidates.append((metadata_json, "metadata_json"))
|
|
candidates.append((source_text, "source_json"))
|
|
for text, method in candidates:
|
|
row = _maybe_json(text)
|
|
if row is not None:
|
|
return row, method
|
|
return None, "text"
|
|
|
|
|
|
def _strip_trigger(text: str, preserve_trigger: bool) -> str:
|
|
text = _clean(text)
|
|
if preserve_trigger:
|
|
return text
|
|
for trigger in TRIGGER_CANDIDATES:
|
|
if text.lower().startswith(trigger.lower() + ","):
|
|
return text[len(trigger) + 1 :].strip(" ,")
|
|
if text.lower().startswith(trigger.lower() + "."):
|
|
return text[len(trigger) + 1 :].strip(" ,")
|
|
return text
|
|
|
|
|
|
def _split_avoid(text: str) -> tuple[str, str]:
|
|
match = re.search(r"\bAvoid:\s*(.*)$", text)
|
|
if not match:
|
|
return text, ""
|
|
return text[: match.start()].strip(" ."), match.group(1).strip(" .")
|
|
|
|
|
|
def _prompt_field(text: str, label: str) -> str:
|
|
text = _clean(text)
|
|
if not text:
|
|
return ""
|
|
labels = "|".join(re.escape(name) for name in PROMPT_FIELD_LABELS)
|
|
pattern = rf"{re.escape(label)}:\s*(.*?)(?=\. (?:{labels}):|\. Use\b|\. Avoid\b|$)"
|
|
match = re.search(pattern, text)
|
|
if not match:
|
|
return ""
|
|
return _clean(match.group(1)).rstrip(".")
|
|
|
|
|
|
def _row_value(row: dict[str, Any], key: str, labels: tuple[str, ...] = ()) -> str:
|
|
value = _clean(row.get(key, ""))
|
|
if value:
|
|
return value
|
|
prompt = _clean(row.get("prompt", ""))
|
|
for label in labels:
|
|
value = _prompt_field(prompt, label)
|
|
if value:
|
|
return value
|
|
return ""
|
|
|
|
|
|
def _body_phrase(body: Any, figure_note: Any = "") -> str:
|
|
body = _clean(body)
|
|
figure_note = _clean(figure_note)
|
|
if not body:
|
|
return figure_note
|
|
if not figure_note:
|
|
return f"{body} figure"
|
|
if "figure" in figure_note.lower():
|
|
return f"{body} build and {figure_note}"
|
|
return f"{body} figure with {figure_note}"
|
|
|
|
|
|
def _single_caption_front(row: dict[str, Any]) -> dict[str, str]:
|
|
caption = _strip_trigger(_clean(row.get("caption")), False)
|
|
if not caption:
|
|
return {}
|
|
subject = _clean(row.get("primary_subject"))
|
|
age = _clean(row.get("age_band") or row.get("age"))
|
|
body = _clean(row.get("body_phrase"))
|
|
if not body:
|
|
body_type = _clean(row.get("body_type") or row.get("body"))
|
|
figure = _clean(row.get("figure"))
|
|
body = _body_phrase(body_type, figure)
|
|
front = f"{subject}, {age}, {body}, "
|
|
if subject in ("woman", "man") and age and body and caption.startswith(front):
|
|
try:
|
|
skin, hair, eyes, _rest = caption[len(front) :].split(", ", 3)
|
|
except ValueError:
|
|
return {}
|
|
return {"body_phrase": body, "skin": skin, "hair": hair, "eyes": eyes}
|
|
return {}
|
|
|
|
|
|
def _combine_negative(*parts: str) -> str:
|
|
cleaned = [_clean(part).strip(" ,.") for part in parts if _clean(part).strip(" ,.")]
|
|
return ", ".join(cleaned)
|
|
|
|
|
|
def _prompt_cast_descriptors(text: str) -> str:
|
|
return _clean(text).replace("Woman A / primary creator:", "Woman A:")
|
|
|
|
|
|
def _cast_entries(text: str) -> list[tuple[str, str]]:
|
|
text = _prompt_cast_descriptors(text)
|
|
entries: list[tuple[str, str]] = []
|
|
for part in text.split(";"):
|
|
part = _clean(part)
|
|
match = re.match(r"^((?:Woman|Man) [A-Z]):\s*(.+)$", part)
|
|
if match:
|
|
entries.append((match.group(1), _clean(match.group(2))))
|
|
return entries
|
|
|
|
|
|
def _label_join(labels: list[str]) -> str:
|
|
labels = [_clean(label) for label in labels if _clean(label)]
|
|
if not labels:
|
|
return "the named adults"
|
|
if set(labels) == {"Woman A", "Man A"}:
|
|
return "the woman and man"
|
|
if len(labels) == 1:
|
|
if labels[0] == "Woman A":
|
|
return "the woman"
|
|
if labels[0] == "Man A":
|
|
return "the man"
|
|
return labels[0]
|
|
if len(labels) == 2:
|
|
return f"{labels[0]} and {labels[1]}"
|
|
return f"{', '.join(labels[:-1])}, and {labels[-1]}"
|
|
|
|
|
|
def _natural_label_text(text: Any, labels: list[str]) -> str:
|
|
text = _clean(text)
|
|
if not text:
|
|
return ""
|
|
if set(labels) == {"Woman A", "Man A"}:
|
|
text = re.sub(r"\bWoman A\b", "the woman", text)
|
|
text = re.sub(r"\bMan A\b", "the man", text)
|
|
elif labels == ["Woman A"]:
|
|
text = re.sub(r"\bWoman A\b", "the woman", text)
|
|
elif labels == ["Man A"]:
|
|
text = re.sub(r"\bMan A\b", "the man", text)
|
|
return text
|
|
|
|
|
|
def _cast_prose(
|
|
text: str,
|
|
central_label: str = "Woman A",
|
|
omit_labels: list[str] | set[str] | tuple[str, ...] = (),
|
|
) -> tuple[str, list[str]]:
|
|
raw_entries = _cast_entries(text)
|
|
omitted = set(omit_labels or [])
|
|
entries = [(label, descriptor) for label, descriptor in raw_entries if label not in omitted]
|
|
if raw_entries and not entries:
|
|
return "", []
|
|
if not entries:
|
|
return (f"{central_label} is {_clean(text)}" if _clean(text) else "", [])
|
|
labels = [label for label, _descriptor in entries]
|
|
if labels == ["Woman A"]:
|
|
return _with_indefinite_article(entries[0][1]), labels
|
|
if labels == ["Man A"]:
|
|
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"{_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}.")
|
|
if central_label in labels:
|
|
sentences.append(f"{central_label} is the central subject.")
|
|
return " ".join(sentences), labels
|
|
|
|
|
|
def _pov_labels_from_value(value: Any) -> list[str]:
|
|
labels: list[str] = []
|
|
if isinstance(value, list):
|
|
candidates = value
|
|
else:
|
|
candidates = re.split(r"[,;]\s*", _clean(value)) if _clean(value) else []
|
|
for candidate in candidates:
|
|
label = _clean(candidate)
|
|
if re.match(r"^Man [A-Z]$", label) and label not in labels:
|
|
labels.append(label)
|
|
return labels
|
|
|
|
|
|
def _merge_labels(*groups: list[str]) -> list[str]:
|
|
merged: list[str] = []
|
|
for group in groups:
|
|
for label in group:
|
|
if label and label not in merged:
|
|
merged.append(label)
|
|
return merged
|
|
|
|
|
|
def _filter_pov_labeled_clauses(text: Any, pov_labels: list[str]) -> str:
|
|
rendered = _clean(text)
|
|
if not rendered or not pov_labels:
|
|
return rendered
|
|
clauses = [clause.strip() for clause in rendered.split(";") if clause.strip()]
|
|
filtered = [
|
|
clause
|
|
for clause in clauses
|
|
if not any(re.match(rf"^{re.escape(label)}\b", clause) for label in pov_labels)
|
|
]
|
|
return "; ".join(filtered)
|
|
|
|
|
|
def _pov_action_phrase(action: Any, pov_labels: list[str]) -> str:
|
|
rendered = _clean(action)
|
|
if not rendered or not pov_labels:
|
|
return rendered
|
|
for label in sorted(pov_labels, key=len, reverse=True):
|
|
escaped = re.escape(label)
|
|
rendered = re.sub(rf"\b{escaped}'s\b", "the POV viewer's", rendered)
|
|
rendered = re.sub(rf"\b{escaped}\b", "the POV viewer", rendered)
|
|
if "Man A" in pov_labels:
|
|
rendered = re.sub(r"\bthe man's\b", "the POV viewer's", rendered, flags=re.IGNORECASE)
|
|
rendered = re.sub(r"\bthe man\b", "the POV viewer", rendered, flags=re.IGNORECASE)
|
|
rendered = re.sub(r"\bhe\b", "the POV viewer", rendered, flags=re.IGNORECASE)
|
|
rendered = re.sub(r"\bhim\b", "the POV viewer", rendered, flags=re.IGNORECASE)
|
|
rendered = re.sub(r"\bhis\b", "the POV viewer's", rendered, flags=re.IGNORECASE)
|
|
rendered = re.sub(
|
|
r"\bthe POV viewer lies on the POV viewer's back under her\b",
|
|
"the POV viewer reclines underneath her",
|
|
rendered,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
rendered = re.sub(
|
|
r"\bthe POV viewer lies on the POV viewer's back\b",
|
|
"the POV viewer reclines",
|
|
rendered,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
rendered = re.sub(r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
|
|
return rendered
|
|
|
|
|
|
def _pov_camera_phrase(pov_labels: list[str], softcore: bool = False) -> str:
|
|
if not pov_labels:
|
|
return ""
|
|
if softcore:
|
|
return (
|
|
"first-person POV creator framing from the male participant; "
|
|
"the woman is the visible subject, and the participant is implied by camera position or foreground cues"
|
|
)
|
|
return (
|
|
"first-person POV from the male participant; keep the visible partners readable from the viewer's position, "
|
|
"using only foreground hands, body position, or camera perspective cues for the POV participant"
|
|
)
|
|
|
|
|
|
def _pov_composition_text(composition: Any, pov_labels: list[str]) -> str:
|
|
text = _clean(composition)
|
|
if not text or not pov_labels:
|
|
return text
|
|
text = re.sub(r"\ball participants visible\b", "visible partners readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\ball adult bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\ball bodies visible\b", "visible partners readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\ball three bodies readable\b", "visible partner bodies readable", text, flags=re.IGNORECASE)
|
|
text = re.sub(r"\bwide group-sex composition\b", "first-person group-sex POV composition", text, flags=re.IGNORECASE)
|
|
if "pov" not in text.lower() and "first-person" not in text.lower():
|
|
text = f"{text}, adapted for first-person POV with the POV participant kept off-camera"
|
|
return _clean(text)
|
|
|
|
|
|
def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
|
|
text = _clean(text)
|
|
if not text:
|
|
return ""
|
|
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
|
|
|
|
|
|
def _natural_clothing_state(text: Any) -> str:
|
|
text = _clean(text)
|
|
if not text:
|
|
return ""
|
|
text = re.sub(r"^Clothing state:\s*", "", text, flags=re.IGNORECASE)
|
|
body_exposure = re.match(r"^Body exposure:\s*(.*?)\.?$", text, flags=re.IGNORECASE)
|
|
if body_exposure:
|
|
return _clean(body_exposure.group(1)).rstrip(".")
|
|
if re.search(r"\bfully nude\b|\bbody is fully exposed\b|\bno clothing covering\b", text, flags=re.IGNORECASE):
|
|
owner = "the woman"
|
|
owner_match = re.match(r"^\s*((?:Woman|Man) [A-Z])\b", text)
|
|
if owner_match:
|
|
owner = _natural_label_text(owner_match.group(1), ["Woman A", "Man A"]) or owner
|
|
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
|
|
match = re.match(
|
|
r"^(.*?)\b(?:softcore|teaser) outfit is (.*?)(?: for the (?:hardcore|sex) scene)?;\s*(?:softcore visual reference|teaser outfit detail):\s*(.*?)\.?$",
|
|
text,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
if match:
|
|
owner = _natural_label_text(match.group(1).strip(" 's"), ["Woman A", "Man A"]).strip() or "the woman"
|
|
state = _clean(match.group(2)).lower()
|
|
outfit = _clean(match.group(3)).rstrip(".")
|
|
if "fully nude" in state or "fully exposed" in state or "no clothing covering" in state:
|
|
return f"{owner.capitalize()}'s body is fully exposed, bare skin unobstructed"
|
|
if "nude-adjacent" in state:
|
|
return f"{owner.capitalize()}'s body is partly exposed"
|
|
if "partially removed" in state or "pushed aside" in state:
|
|
return f"{owner.capitalize()}'s {outfit} is pushed aside and partly removed, exposing the sexual contact clearly"
|
|
if "keeps" in state:
|
|
return f"{owner.capitalize()} keeps the {outfit} on while the sexual contact stays visible"
|
|
text = re.sub(r";\s*(?:softcore visual reference|teaser outfit detail):\s*", ". Visual clothing state: ", text, flags=re.IGNORECASE)
|
|
text = text.replace("softcore outfit", "outfit")
|
|
text = text.replace("teaser outfit", "outfit")
|
|
text = text.replace("hardcore scene", "sex scene")
|
|
return text
|
|
|
|
|
|
def _axis_values_text(axis_values: Any) -> str:
|
|
if not isinstance(axis_values, dict):
|
|
return ""
|
|
priority = (
|
|
"position",
|
|
"body_position",
|
|
"body_arrangement",
|
|
"arrangement",
|
|
"angle",
|
|
"surface",
|
|
"body_contact",
|
|
"leg_detail",
|
|
"oral_act",
|
|
"oral_detail",
|
|
"penetration_act",
|
|
"penetration_detail",
|
|
"anal_act",
|
|
"double_act",
|
|
"threesome_act",
|
|
"group_act",
|
|
)
|
|
parts = [_clean(axis_values.get(key)) for key in priority if _clean(axis_values.get(key))]
|
|
return " ".join(parts)
|
|
|
|
|
|
def _position_context_text(role_graph: str, hard_item: str, composition: str = "", axis_values: Any = None) -> str:
|
|
return " ".join(
|
|
_clean(part).lower()
|
|
for part in (role_graph, hard_item, composition, _axis_values_text(axis_values))
|
|
if _clean(part)
|
|
)
|
|
|
|
|
|
def _is_toy_assisted_double_text(*parts: Any) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
if "toy" not in text:
|
|
return False
|
|
return any(
|
|
token in text
|
|
for token in (
|
|
"double penetration",
|
|
"double-penetration",
|
|
"vaginal and anal penetration",
|
|
"second penetration point",
|
|
"second point of contact",
|
|
"second contact",
|
|
)
|
|
)
|
|
|
|
|
|
def _mentions_ass(text: str) -> bool:
|
|
return bool(
|
|
re.search(
|
|
r"\bass\b|ass[- ](?:up|raised|exposed|lifted)|spread cheeks|lower back and ass|cum (?:on|dripping from) ass|pussy, ass|ass and",
|
|
text,
|
|
)
|
|
)
|
|
|
|
|
|
def _mentions_rear_entry(text: str) -> bool:
|
|
return bool(
|
|
re.search(
|
|
r"ass[- ](?:up|raised|exposed|lifted|stretched)|penis entering ass|cum (?:on|dripping from) ass|spread cheeks|lower back and ass|pussy, ass|rear[- ]entry",
|
|
text,
|
|
)
|
|
)
|
|
|
|
|
|
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)):
|
|
if "face-down ass-up" in text or "face-down" in text:
|
|
return "toy-assisted face-down rear-entry double-penetration pose"
|
|
if "doggy style" in text or "doggy-style" in text or "all fours" in text or "rear-entry" in text:
|
|
return "toy-assisted rear-entry double-penetration pose"
|
|
if "bent-over" in text or "bent forward" in text:
|
|
return "toy-assisted bent-over double-penetration pose"
|
|
if "spooning anal" in text or "side-lying anal" in text or "side-lying" in text:
|
|
return "toy-assisted side-lying double-penetration pose"
|
|
if "edge-supported" in text or "bed-edge" in text or "edge-of-bed" in text:
|
|
return "toy-assisted edge-supported double-penetration pose"
|
|
if "standing anal" in text or "standing supported" in text or "standing" in text:
|
|
return "toy-assisted standing double-penetration pose"
|
|
if "kneeling anal" in text or "kneeling" in text:
|
|
return "toy-assisted kneeling rear-entry double-penetration pose"
|
|
return "toy-assisted rear-entry double-penetration pose"
|
|
if "double penetration" in text or "vaginal and anal penetration" in text or "front-and-back" in text:
|
|
if "face-down ass-up" in text:
|
|
return "face-down rear-entry double-penetration pose"
|
|
if "doggy style" in text or "doggy-style" in text:
|
|
return "doggy-style double-penetration pose"
|
|
if "bent-over" in text:
|
|
return "bent-over double-penetration pose"
|
|
if "spooning anal" in text or "side-lying anal" in text:
|
|
return "side-lying double-penetration pose"
|
|
if "bed-edge" in text or "edge-of-bed" in text:
|
|
return "bed-edge front-and-back double-penetration pose"
|
|
if "standing anal" in text or "standing supported" in text:
|
|
return "standing supported front-and-back double-penetration pose"
|
|
if "kneeling anal" in text:
|
|
return "kneeling rear-entry 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 "sixty-nine" in position_text or ("sixty-nine" in text and not position_text):
|
|
return "sixty-nine oral pose"
|
|
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"
|
|
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-supported 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-supported 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 _mentions_rear_entry(text) or "rear-entry" in text:
|
|
if "face-down ass-up" in text:
|
|
return "face-down ass-up rear-entry anal pose"
|
|
if "doggy style" in text or "doggy-style" in text:
|
|
return "doggy-style anal pose"
|
|
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 "spooning anal" in text or "side-lying anal" in text:
|
|
return "side-lying rear-entry anal pose"
|
|
if "kneeling anal" in text:
|
|
return "kneeling rear-entry anal pose"
|
|
if "standing anal" in text:
|
|
return "standing rear-entry anal pose"
|
|
if "doggy" in text:
|
|
return "doggy-style anal pose"
|
|
return "rear-entry anal pose"
|
|
positions = (
|
|
"missionary",
|
|
"reverse cowgirl",
|
|
"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 = "", 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
|
|
is_double = "double-penetration" in text or "double penetration" in text
|
|
|
|
def cast_phrase(mixed: str, generic: str) -> str:
|
|
return mixed if mixed_woman_man else generic
|
|
|
|
def double_tail() -> str:
|
|
return "" if "toy" in text else ", with the second penetration point aligned"
|
|
|
|
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 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 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",
|
|
"with the receiver seated with legs apart and the giver positioned at the hips",
|
|
)
|
|
return cast_phrase(
|
|
"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 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 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 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 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 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",
|
|
"with the receiver standing and the giver kneeling at hip height",
|
|
)
|
|
return cast_phrase(
|
|
"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 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",
|
|
"with the receiver seated in the chair and the giver kneeling between the legs at hip level",
|
|
)
|
|
return cast_phrase(
|
|
"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 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 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",
|
|
"with the giver kneeling in front of the receiver's hips",
|
|
)
|
|
if "mouth on her pussy" in text or "uses his mouth on" in text:
|
|
return cast_phrase(
|
|
"with the man kneeling between the woman's open thighs, his mouth at her pussy",
|
|
"with the giver kneeling between the receiver's open thighs",
|
|
)
|
|
return "with the giver kneeling at the receiver's hips"
|
|
if "reverse cowgirl" in text:
|
|
return cast_phrase(
|
|
"with the man lying on his back under the woman while she straddles his hips facing away",
|
|
"with the lower partner lying on their back while the upper partner straddles them facing away",
|
|
)
|
|
if "cowgirl" in text:
|
|
return cast_phrase(
|
|
"with the man lying on his back under the woman while she straddles his hips on top",
|
|
"with the lower partner lying on their back while the upper partner straddles their hips on top",
|
|
)
|
|
if "missionary" in text:
|
|
return cast_phrase(
|
|
"with the woman lying on her back under the man, legs open around his hips",
|
|
"with the receiving partner lying on their back under the penetrating partner, legs open around the hips",
|
|
)
|
|
if "lotus" in text:
|
|
return cast_phrase(
|
|
"with the man seated upright and the woman seated in his lap facing him, legs wrapped around his hips",
|
|
"with one partner seated upright and the other seated in their lap facing them, legs wrapped around the hips",
|
|
)
|
|
if "kneeling straddle" in text:
|
|
return cast_phrase(
|
|
"with the woman straddling the man's kneeling lap, both torsos upright and hips pressed together",
|
|
"with one partner straddling the other's kneeling lap, torsos upright and hips pressed together",
|
|
)
|
|
if "doggy-style" in text:
|
|
return cast_phrase(
|
|
f"with the woman on all fours and the man positioned behind her at hip level{double_tail() if is_double else ''}",
|
|
f"with the receiving partner on all fours and the penetrating partner positioned behind at hip level{double_tail() if is_double else ''}",
|
|
)
|
|
if "face-down" in text:
|
|
return cast_phrase(
|
|
f"with the woman face-down, hips raised, and the man positioned behind her{double_tail() if is_double else ''}",
|
|
f"with the receiving partner face-down, hips raised, and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "bent-over" in text:
|
|
return cast_phrase(
|
|
f"with the woman bent forward at the waist and the man positioned behind her{double_tail() if is_double else ''}",
|
|
f"with the receiving partner bent forward at the waist and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "spooning" in text or ("side-lying" in text and "oral" not in text):
|
|
return cast_phrase(
|
|
f"with both lying on their sides and the man positioned behind the woman{double_tail() if is_double else ''}",
|
|
f"with both bodies lying on their sides and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "edge-of-bed" in text or "bed-edge" in text:
|
|
return cast_phrase(
|
|
f"with the woman lying at the bed edge, hips at the edge, and the man kneeling between her legs{double_tail() if is_double else ''}",
|
|
f"with the receiver lying at the bed edge, hips at the edge, and the penetrating partner kneeling between the legs{double_tail() if is_double else ''}",
|
|
)
|
|
if "standing" in text:
|
|
return cast_phrase(
|
|
f"with the woman braced standing and the man aligned at her hips{double_tail() if is_double else ''}",
|
|
f"with both partners standing and the penetrating partner aligned at the receiver's hips{double_tail() if is_double else ''}",
|
|
)
|
|
if "kneeling" in text and ("anal" in text or "rear-entry" in text):
|
|
return cast_phrase(
|
|
f"with the woman kneeling forward and the man positioned behind her{double_tail() if is_double else ''}",
|
|
f"with the receiving partner kneeling forward and the penetrating partner positioned behind{double_tail() if is_double else ''}",
|
|
)
|
|
if "double-penetration" in text or "double penetration" in text:
|
|
if "toy" in text:
|
|
return cast_phrase(
|
|
"with the woman on all fours and the man positioned behind her at hip level",
|
|
"with the receiving body on all fours and the penetrating partner positioned behind at hip level",
|
|
)
|
|
if "from the front" in text:
|
|
return cast_phrase(
|
|
"with the woman held between the man behind her and a second partner in front",
|
|
"with the receiving body held between one partner behind and a second partner in front",
|
|
)
|
|
return cast_phrase(
|
|
"with the woman held in a front-and-back position so both contact points are visible",
|
|
"with the central body held in a front-and-back position so both contact points are visible",
|
|
)
|
|
if "anal" in text or _mentions_rear_entry(text) or "rear-entry" in text:
|
|
return cast_phrase(
|
|
"with the woman's hips raised, ass exposed, and the man positioned behind her",
|
|
"with the receiving partner's hips raised and the penetrating partner positioned behind",
|
|
)
|
|
if "cunnilingus" in text or "mouth on her pussy" in text or "pussy licking" in text:
|
|
return cast_phrase(
|
|
"with the woman's thighs open and the man's mouth pressed to her pussy",
|
|
"with the receiver's thighs open and the giver's mouth pressed to genitals",
|
|
)
|
|
if "oral" in text or "blowjob" in text or "penis in her mouth" in text or "penis in mouth" in text:
|
|
if "takes the man's penis in her mouth" in text or "penis in her mouth" in text:
|
|
return cast_phrase(
|
|
"with the woman's mouth at the man's hips",
|
|
"with the giver's mouth positioned at the receiver'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 _arrangement_duplicates_role(arrangement: str, role_graph: str) -> bool:
|
|
arrangement_lower = _clean(arrangement).lower()
|
|
role_lower = _clean(role_graph).lower()
|
|
if not arrangement_lower or not role_lower:
|
|
return False
|
|
markers = (
|
|
"bed edge",
|
|
"on all fours",
|
|
"face-down",
|
|
"hips raised",
|
|
"bent forward",
|
|
"straddl",
|
|
"on her back",
|
|
"on their sides",
|
|
"on her side",
|
|
"seated in",
|
|
"sits in",
|
|
"lap",
|
|
"kneeling between",
|
|
"kneels between",
|
|
"kneeling in front",
|
|
"kneels in front",
|
|
"positioned behind",
|
|
"standing",
|
|
)
|
|
return any(marker in arrangement_lower and marker in role_lower for marker in markers)
|
|
|
|
|
|
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"^(?:orgasm|climax)\s+scene:\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"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|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(
|
|
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-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",),
|
|
"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",),
|
|
"face-down": (r"face-down ass-up position",),
|
|
"missionary": (r"missionary position",),
|
|
"reverse cowgirl": (r"reverse cowgirl position",),
|
|
"cowgirl": (r"cowgirl position",),
|
|
"doggy-style": (r"doggy style position",),
|
|
"edge-of-bed": (r"edge-of-bed position",),
|
|
"lotus": (r"lotus sex position",),
|
|
"standing sex": (r"standing sex position",),
|
|
"spooning": (r"spooning sex position", r"spooning anal 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 _dedupe_toy_double_detail(detail: str) -> str:
|
|
detail = _clean(detail)
|
|
if not detail:
|
|
return ""
|
|
angle_view = (
|
|
r"(?:rear-view|side-profile|low-angle|mirror-reflected|overhead|close-up|wide full-body|front-facing with hips turned)"
|
|
)
|
|
toy_act = (
|
|
r"(?:penis and toy double penetration|toy-assisted vaginal and anal penetration at the same time|toy and strap-on double penetration)"
|
|
)
|
|
detail = re.sub(
|
|
rf"\b({angle_view}\s+view of\s+){toy_act}\b",
|
|
r"\1the rear-entry contact",
|
|
detail,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
detail = re.sub(rf",?\s*\b{toy_act}\b", "", detail, flags=re.IGNORECASE)
|
|
duplicate_phrases = (
|
|
"toy-assisted second contact aligned behind the body",
|
|
"toy aligned for a second penetration point",
|
|
"rear-entry body alignment",
|
|
"close body alignment",
|
|
"stacked bodies in close contact",
|
|
"one body between two partners",
|
|
"one partner behind and one partner in front",
|
|
"two partners penetrating at once",
|
|
"one partner held between two bodies",
|
|
"front-and-back contact",
|
|
"three bodies locked together",
|
|
"kneeling center partner",
|
|
)
|
|
for phrase in duplicate_phrases:
|
|
detail = re.sub(rf",?\s*\b{re.escape(phrase)}\b", "", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"^\s*,\s*", "", detail)
|
|
detail = re.sub(r",\s*,", ",", detail)
|
|
return _clean(detail).strip(" ,;")
|
|
|
|
|
|
def _detail_clauses(detail: str) -> list[str]:
|
|
return [part.strip(" ,;") for part in re.split(r",\s*(?:and\s+)?", _clean(detail)) if part.strip(" ,;")]
|
|
|
|
|
|
def _join_detail_clauses(clauses: list[str]) -> str:
|
|
cleaned: list[str] = []
|
|
seen: set[str] = set()
|
|
for clause in clauses:
|
|
clause = _clean(clause).strip(" ,;")
|
|
key = clause.lower()
|
|
if clause and key not in seen:
|
|
cleaned.append(clause)
|
|
seen.add(key)
|
|
return ", ".join(cleaned)
|
|
|
|
|
|
def _action_position_phrase(action: str) -> str:
|
|
action = _clean(action).lower()
|
|
if "face-down" in action and "ass raised" in action:
|
|
return "face-down raised-hip position"
|
|
if "on all fours" in action:
|
|
return "all-fours raised-hip position"
|
|
if "bends forward" in action or "bent forward" in action:
|
|
return "bent-over raised-hip position"
|
|
if "lies on her back" in action and ("thighs open" in action or "legs open" in action):
|
|
return "open-thigh reclined position"
|
|
if "lies at the bed edge" in action or "bed edge" in action:
|
|
return "bed-edge position"
|
|
if "lies on her side" in action:
|
|
return "side-lying position"
|
|
if "kneels in front" in action:
|
|
return "kneeling-at-hip-height position"
|
|
if "straddles" in action or "squats over" in action:
|
|
return "straddling position"
|
|
if "sits in the man's lap" in action:
|
|
return "lap-straddling position"
|
|
if "stands braced" in action:
|
|
return "standing braced position"
|
|
if "held between" in action or "front-and-back" in action:
|
|
return "front-and-back position"
|
|
if "lies between" in action:
|
|
return "between-partners position"
|
|
return ""
|
|
|
|
|
|
def _normalize_climax_view_clause(clause: str, role_graph: str) -> str:
|
|
lower = clause.lower()
|
|
if "view" not in lower and "frame" not in lower:
|
|
return clause
|
|
angle_match = re.search(
|
|
r"\b(front-facing|close-up|wide full-body|wide|overhead|mirror-reflected|low-angle|side-profile|bed-level)\b",
|
|
lower,
|
|
)
|
|
if not angle_match:
|
|
return clause
|
|
angle = angle_match.group(1)
|
|
if angle == "wide":
|
|
angle = "wide full-body"
|
|
position = _action_position_phrase(role_graph)
|
|
if position:
|
|
return f"{angle} aftermath view with the {position} readable"
|
|
return f"{angle} aftermath view"
|
|
|
|
|
|
def _climax_clause_duplicates_role(clause: str, role_graph: str) -> bool:
|
|
clause_lower = clause.lower()
|
|
role_lower = role_graph.lower()
|
|
role_has_ejaculation = any(token in role_lower for token in ("ejaculates semen", "visible semen", "semen lands"))
|
|
if role_has_ejaculation and re.search(
|
|
r"\b(?:cum clearly visible|explicit semen aftermath visible|hardcore ejaculation detail visible|"
|
|
r"post-ejaculation fluids anatomically clear|sexual fluids and body contact visible|"
|
|
r"visible external ejaculation|hardcore ejaculation scene|visible orgasm aftermath)\b",
|
|
clause_lower,
|
|
):
|
|
return True
|
|
duplicate_pairs = (
|
|
(("lower back", "ass"), ("lower back", "ass")),
|
|
(("ass",), ("ass",)),
|
|
(("pussy", "thigh"), ("pussy", "thigh")),
|
|
(("face", "lips"), ("face", "lips")),
|
|
(("tongue", "chin"), ("face", "lips", "mouth", "tongue")),
|
|
(("breast",), ("breast", "chest")),
|
|
(("belly",), ("belly", "torso")),
|
|
(("body",), ("body",)),
|
|
)
|
|
if any(token in clause_lower for token in ("cum", "semen", "fluid")):
|
|
for clause_tokens, role_tokens in duplicate_pairs:
|
|
if any(token in clause_lower for token in clause_tokens) and any(token in role_lower for token in role_tokens):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _limit_detail_for_density(detail: str, density: str, is_climax: bool) -> str:
|
|
density = _normalize_hardcore_detail_density(density)
|
|
if density == "compact":
|
|
return ""
|
|
clauses = _detail_clauses(detail)
|
|
if not clauses:
|
|
return ""
|
|
if density == "balanced":
|
|
limit = 1 if is_climax else 2
|
|
else:
|
|
limit = 3 if is_climax else 4
|
|
return _join_detail_clauses(clauses[:limit])
|
|
|
|
|
|
def _is_climax_text(*parts: str) -> bool:
|
|
text = " ".join(_clean(part).lower() for part in parts if _clean(part))
|
|
return any(
|
|
token in text
|
|
for token in (
|
|
"cumshot",
|
|
"ejaculation",
|
|
"post-orgasm",
|
|
"post-climax",
|
|
"orgasm aftermath",
|
|
"orgasm scene",
|
|
"orgasm during",
|
|
"shared climax",
|
|
"hardcore climax",
|
|
"external cumshot",
|
|
"visible external ejaculation",
|
|
"climaxes on",
|
|
"climax lands",
|
|
)
|
|
)
|
|
|
|
|
|
def _climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) -> str:
|
|
role_graph = _clean(role_graph).rstrip(".")
|
|
text = " ".join(part.lower() for part in (role_graph, _clean(hard_item), _axis_values_text(axis_values)) if part)
|
|
if "the woman" not in text or "the man" not in text:
|
|
return role_graph
|
|
if "lying between two partners" in text or "lies between" in text:
|
|
return "the woman lies between two partners, the man under her hips and another partner over her torso as visible semen lands on her body"
|
|
if "held between front-and-back partners" in text:
|
|
return "the woman is held between the man behind her and another partner in front of her as visible semen lands across her body"
|
|
if "kneeling between standing partners" in text:
|
|
return "the woman kneels between standing partners gathered around her face and torso for visible ejaculation"
|
|
if "side-lying with thighs parted" in text:
|
|
return "the woman lies on her side with thighs parted while the man kneels beside her hips and ejaculates semen across her thighs and pussy"
|
|
if "sitting on the edge of the bed" in text:
|
|
return "the woman sits on the edge of the bed with knees spread while the man stands close between her legs and ejaculates semen across her body"
|
|
if "lying at the bed edge with thighs open" in text:
|
|
return "the woman lies at the bed edge with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs"
|
|
if "reclining with thighs open" in text or "lying on the back with legs spread" in text:
|
|
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs"
|
|
if "on all fours with hips raised" in text:
|
|
return "the woman is on all fours with hips raised while the man is positioned behind her and ejaculates semen across her ass, thighs, and lower back"
|
|
if "face-down ass-up" in text or "lies face-down" in text or "face down" in text:
|
|
return "the woman lies face-down with ass raised while the man is positioned behind her and ejaculates semen across her lower back and ass"
|
|
if "bent over with ass raised" in text or "bent over" in text:
|
|
return "the woman bends forward with hips raised while the man stands behind her with visible semen across her lower back, ass, and thighs"
|
|
if "kneeling with mouth open" in text:
|
|
return "the woman kneels in front of the man at hip height as he ejaculates semen onto her face, lips, and chest"
|
|
if "kneeling in front of a standing partner" in text:
|
|
return "the woman kneels in front of the man at hip height while he stands over her for visible ejaculation"
|
|
if "standing with cum on the body" in text:
|
|
return "the woman stands braced in front of the man while he stands close at hip level and ejaculates semen across her body"
|
|
if "squatting on top of a partner" in text:
|
|
return "the woman squats over the man's hips while the man lies on his back under her and ejaculates semen onto her body"
|
|
if "reverse cowgirl over a partner's hips" in text:
|
|
return "the woman straddles the man's hips facing away while the man lies on his back under her and ejaculates semen onto her body"
|
|
if "straddles" in text or "straddling a partner" in text or "straddling a partner's hips" in text or "shared climax after penetration" in text:
|
|
return "the woman straddles the man's hips while the man lies on his back under her and ejaculates semen onto her body"
|
|
if "seated in a partner's lap facing them" in text:
|
|
return "the woman sits in the man's lap facing him, legs wrapped around his hips as he ejaculates semen across her body"
|
|
if "lower back" in text or "cum dripping from ass" in text or "cum on lower back" in text or _mentions_rear_entry(text):
|
|
return "the woman bends forward with hips raised while the man stands behind her with visible semen across her lower back, ass, and thighs"
|
|
if "cum on face" in text or "cum on tongue" in text or "cum on lips" in text or "cum on tongue and chin" in text:
|
|
return "the woman kneels in front of the man at hip height as he ejaculates semen onto her face, lips, and chest"
|
|
if (
|
|
"cum dripping from pussy" in text
|
|
or "arousal dripping from pussy" in text
|
|
or "open thighs" in text
|
|
):
|
|
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs"
|
|
if role_graph:
|
|
return role_graph
|
|
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her body"
|
|
|
|
|
|
def _dedupe_climax_detail(detail: str, role_graph: str, density: str = "balanced") -> str:
|
|
detail = _clean(detail)
|
|
lower = role_graph.lower()
|
|
patterns: list[str] = []
|
|
if "lies on her back" in lower:
|
|
patterns.extend((r"lying on the back with legs spread and hips lifted", r"reclining with thighs open", r"lying on the back with legs spread"))
|
|
detail = re.sub(r"\bcum on lower back and ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"\bcum (?:on|dripping from) ass\b", "cum across thighs and pussy", detail, flags=re.IGNORECASE)
|
|
if "straddles" in lower:
|
|
patterns.extend(
|
|
(
|
|
r"straddling a partner's hips in cowgirl position",
|
|
r"reverse cowgirl over a partner's hips",
|
|
r"straddling a partner",
|
|
r"squatting on top of a partner",
|
|
)
|
|
)
|
|
if "squats over" in lower:
|
|
patterns.append(r"squatting on top of a partner")
|
|
if "sits in the man's lap" in lower:
|
|
patterns.append(r"seated in a partner's lap facing them")
|
|
if "bends forward" in lower:
|
|
patterns.append(r"bent over with ass raised")
|
|
if "on all fours" in lower:
|
|
patterns.append(r"on all fours with hips raised")
|
|
if "face-down" in lower:
|
|
patterns.append(r"face-down ass-up on the mattress")
|
|
if "lies on her side" in lower:
|
|
patterns.append(r"side-lying with thighs parted")
|
|
if "sits on the edge" in lower:
|
|
patterns.append(r"sitting on the edge of the bed")
|
|
if "bed edge" in lower:
|
|
patterns.append(r"lying at the bed edge with thighs open")
|
|
if "kneels in front" in lower:
|
|
patterns.extend((r"kneeling with mouth open", r"kneeling in front of a standing partner"))
|
|
if "stands braced" in lower:
|
|
patterns.append(r"standing with cum on the body")
|
|
for pattern in patterns:
|
|
detail = re.sub(rf"\b{pattern}\b,?\s*", "", detail, flags=re.IGNORECASE)
|
|
if not any(token in lower for token in ("face", "mouth", "lips", "tongue")):
|
|
detail = re.sub(r"\bsaliva and cum mixed on the mouth\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"\bcum on tongue and chin\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"\bcum on face and lips\b", "visible semen on skin", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r",\s*,", ",", detail)
|
|
detail = re.sub(r"\bwith\s*,\s*", "with ", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"^with\s+", "", detail, flags=re.IGNORECASE)
|
|
detail = re.sub(r"^and\s+", "", detail, flags=re.IGNORECASE)
|
|
clauses: list[str] = []
|
|
for clause in _detail_clauses(detail):
|
|
normalized = _normalize_climax_view_clause(clause, role_graph)
|
|
if _climax_clause_duplicates_role(normalized, role_graph):
|
|
continue
|
|
if density != "dense" and normalized.lower() in ("orgasm during penetration", "post-orgasm visible release"):
|
|
continue
|
|
clauses.append(normalized)
|
|
return _limit_detail_for_density(_join_detail_clauses(clauses), density, True)
|
|
|
|
|
|
def _hardcore_action_sentence(
|
|
role_graph: str,
|
|
hard_item: str,
|
|
composition: str = "",
|
|
axis_values: Any = None,
|
|
detail_density: str = "balanced",
|
|
) -> str:
|
|
detail_density = _normalize_hardcore_detail_density(detail_density)
|
|
role_graph = _clean(role_graph).rstrip(".")
|
|
hard_item = _clean(hard_item).rstrip(".")
|
|
role_graph = re.sub(
|
|
r"\bthe man penetrates the woman while a toy adds a second point of contact\b",
|
|
"the man's penis thrusts into the woman while a toy is positioned at the second penetration point",
|
|
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 is positioned at 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",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe woman and the man are in mutual oral contact with mouth-to-genital contact visible\b",
|
|
"the woman has the man's penis in her mouth while the man uses his mouth on her pussy",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
role_graph = re.sub(
|
|
r"\bthe woman gives oral to the man\b",
|
|
"the woman takes the man's penis in her mouth",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
is_climax = _is_climax_text(role_graph, hard_item, composition, _axis_values_text(axis_values))
|
|
if is_climax:
|
|
role_graph = _climax_role_graph(role_graph, hard_item, axis_values)
|
|
detail = _hardcore_item_detail(hard_item)
|
|
anchor = _hardcore_pose_anchor(role_graph, hard_item, composition, axis_values)
|
|
if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)):
|
|
role_graph = re.sub(
|
|
r"\s+while a toy adds (?:the|a) second penetration point\b",
|
|
" while a toy is positioned at the second penetration point",
|
|
role_graph,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
if is_climax:
|
|
anchor = ""
|
|
detail = _dedupe_climax_detail(detail, role_graph, detail_density)
|
|
else:
|
|
detail = _dedupe_hardcore_detail(detail, anchor) if anchor else detail
|
|
if _is_toy_assisted_double_text(role_graph, hard_item, composition, _axis_values_text(axis_values)):
|
|
detail = _dedupe_toy_double_detail(detail)
|
|
detail = _limit_detail_for_density(detail, detail_density, False)
|
|
arrangement = _hardcore_pose_arrangement(anchor, role_graph, hard_item, composition, axis_values)
|
|
anchor_phrase = _with_indefinite_article(anchor) if anchor else ""
|
|
if arrangement and anchor_phrase and not _arrangement_duplicates_role(arrangement, role_graph):
|
|
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",
|
|
detail_density: str = "balanced",
|
|
) -> str:
|
|
composition = _clean(composition)
|
|
if not composition:
|
|
return ""
|
|
action_lower = _clean(action).lower()
|
|
composition_lower = composition.lower()
|
|
detail_density = _normalize_hardcore_detail_density(detail_density)
|
|
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 ""
|
|
position = _action_position_phrase(action)
|
|
close_or_aftermath = any(
|
|
token in composition_lower
|
|
for token in ("close-up", "close crop", "tight", "direct-flash", "subscriber-view", "post-ejaculation", "aftermath")
|
|
)
|
|
if position and close_or_aftermath:
|
|
if detail_density == "compact":
|
|
return f"{prefix} {composition}, with the {position} still readable"
|
|
return f"{prefix} {composition}, keeping the {position} and action geography readable"
|
|
return f"{prefix} {composition}"
|
|
|
|
|
|
def _clean_age(age: Any) -> str:
|
|
return _clean(age)
|
|
|
|
|
|
def _age_detail_phrase(age: Any) -> str:
|
|
text = _clean(age)
|
|
text = re.sub(r"\s+adults?$", "", text).strip()
|
|
return text.replace("-year-old", " years old")
|
|
|
|
|
|
def _age_subject(row: dict[str, Any], fallback_subject: str = "adult person") -> str:
|
|
subject = _clean(row.get("subject_phrase") or row.get("primary_subject") or row.get("subject") or fallback_subject)
|
|
age = _clean_age(row.get("age_band") or row.get("age"))
|
|
if row.get("subject_type") == "configured_cast":
|
|
return _clean(row.get("subject_phrase") or subject)
|
|
if subject in ("woman", "man"):
|
|
if age:
|
|
return f"{age} {subject}" if "adult" in age.lower() else f"{age} adult {subject}"
|
|
return f"adult {subject}"
|
|
if age and "adult" not in subject.lower():
|
|
return f"{age} {subject}"
|
|
return subject or fallback_subject
|
|
|
|
|
|
def _appearance_phrase(row: dict[str, Any]) -> str:
|
|
front = _single_caption_front(row)
|
|
parts = [
|
|
_row_value(row, "body_phrase") or front.get("body_phrase"),
|
|
_row_value(row, "skin") or front.get("skin"),
|
|
_row_value(row, "hair") or front.get("hair"),
|
|
_row_value(row, "eyes") or front.get("eyes"),
|
|
]
|
|
return ", ".join(_clean(part) for part in parts if _clean(part))
|
|
|
|
|
|
def _expression_phrase(expression: Any) -> str:
|
|
expression = _clean(expression)
|
|
if not expression:
|
|
return ""
|
|
if ";" in expression or re.search(r"\b(?:Woman|Man) [A-Z] has\b|\bthe (?:woman|man) has\b", expression):
|
|
return f"Expressions: {expression}"
|
|
return f"with {expression}"
|
|
|
|
|
|
def _camera_phrase(row: dict[str, Any]) -> str:
|
|
directive = _clean(row.get("camera_directive"))
|
|
if directive:
|
|
return directive
|
|
config = row.get("camera_config")
|
|
if isinstance(config, dict):
|
|
detail = _clean(config.get("camera_detail"))
|
|
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
|
return ""
|
|
custom = _clean(config.get("custom_camera_prompt"))
|
|
if custom:
|
|
base = _clean(config.get("camera_mode")).replace("_", " ")
|
|
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
|
return "Camera: " + ", ".join(pieces)
|
|
mode = _clean(config.get("camera_mode")).replace("_", " ")
|
|
shot = _clean(config.get("shot_size")).replace("_", " ")
|
|
angle = _clean(config.get("angle")).replace("_", " ")
|
|
pieces = [piece for piece in (mode, shot, angle) if piece and piece != "auto" and piece != "standard"]
|
|
if pieces:
|
|
return "Camera: " + ", ".join(pieces)
|
|
return ""
|
|
|
|
|
|
def _camera_phrase_from_config(config: Any) -> str:
|
|
if not isinstance(config, dict):
|
|
return ""
|
|
detail = _clean(config.get("camera_detail"))
|
|
if detail == "off" or _clean(config.get("camera_mode")) == "disabled":
|
|
return ""
|
|
custom = _clean(config.get("custom_camera_prompt"))
|
|
if custom:
|
|
base = _clean(config.get("camera_mode")).replace("_", " ")
|
|
pieces = [piece for piece in (base, custom) if piece and piece != "standard"]
|
|
return "Camera: " + ", ".join(pieces)
|
|
values = [
|
|
_clean(config.get("camera_mode")).replace("_", " "),
|
|
_clean(config.get("shot_size")).replace("_", " "),
|
|
_clean(config.get("angle")).replace("_", " "),
|
|
_clean(config.get("lens")).replace("_", " "),
|
|
_clean(config.get("distance")).replace("_", " "),
|
|
_clean(config.get("orientation")).replace("_", " "),
|
|
_clean(config.get("phone_visibility")).replace("_", " "),
|
|
]
|
|
pieces = [value for value in values if value and value not in ("auto", "standard")]
|
|
if not pieces:
|
|
return ""
|
|
return "Camera: " + ", ".join(pieces)
|
|
|
|
|
|
def _pair_camera_phrase(directive: Any, config: Any, row: dict[str, Any]) -> str:
|
|
directive_text = _clean(directive)
|
|
if directive_text:
|
|
return directive_text
|
|
if isinstance(config, dict) and (
|
|
_clean(config.get("camera_detail")) == "off" or _clean(config.get("camera_mode")) == "disabled"
|
|
):
|
|
return ""
|
|
return _camera_phrase_from_config(config) or _camera_phrase(row)
|
|
|
|
|
|
def _style_phrase(row: dict[str, Any], style_mode: str) -> str:
|
|
if style_mode == "minimal":
|
|
return ""
|
|
if style_mode == "photographic":
|
|
return "realistic creator-shot photography with natural lighting, tactile skin and fabric detail, and clean social-media composition"
|
|
style = _clean(row.get("style"))
|
|
suffix = _clean(row.get("positive_suffix")) or _prompt_field(_clean(row.get("prompt")), "Use")
|
|
if style and suffix:
|
|
return f"{style}; {suffix}"
|
|
return style or suffix
|
|
|
|
|
|
def _couple_clothing_phrase(item: str) -> str:
|
|
item = _clean(item)
|
|
lower = item.lower()
|
|
partner_text = re.sub(r"\bPartner ([AB]) wears\b", r"Partner \1 wearing", item)
|
|
partner_text = re.sub(r"\bPartner ([AB]) has\b", r"Partner \1 with", partner_text)
|
|
if lower.startswith("partner a "):
|
|
return f"The outfits show {partner_text}"
|
|
if lower.startswith(("two ", "paired ", "coordinated ")):
|
|
return f"The outfits are {partner_text}"
|
|
return f"The couple wears {item}"
|
|
|
|
|
|
def _normal_row_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str]:
|
|
subject_type = _clean(row.get("subject_type"))
|
|
primary = _clean(row.get("primary_subject"))
|
|
item = _row_value(row, "item", ("Sexual pose", "Erotic outfit", "Clothing")) or _clean(row.get("custom_item"))
|
|
item = re.sub(r",?\s*(fashion editorial|resort) styling$", "", item, flags=re.IGNORECASE)
|
|
scene = _row_value(row, "scene_text", ("Setting", "Scene")) or _clean(row.get("scene"))
|
|
pose = _row_value(row, "pose", ("Sexual pose", "Pose"))
|
|
expression = ""
|
|
if not _expression_disabled(row):
|
|
expression = _row_value(row, "character_expression_text") or _row_value(row, "expression", ("Facial expressions", "Facial expression"))
|
|
composition = re.sub(r"^vertical\s+", "", _row_value(row, "composition", ("Composition",)), flags=re.IGNORECASE)
|
|
source_composition = re.sub(
|
|
r"^vertical\s+",
|
|
"",
|
|
_clean(row.get("source_composition")) or composition,
|
|
flags=re.IGNORECASE,
|
|
)
|
|
camera = _camera_phrase(row)
|
|
style = _style_phrase(row, style_mode)
|
|
|
|
if subject_type == "configured_cast" or _clean(row.get("cast_summary")):
|
|
subject = _clean(row.get("subject_phrase") or primary or "adult sexual scene")
|
|
cast = _clean(row.get("cast_summary"))
|
|
try:
|
|
women_count = int(row.get("women_count") or 0)
|
|
men_count = int(row.get("men_count") or 0)
|
|
except (TypeError, ValueError):
|
|
women_count = men_count = 0
|
|
cast_descriptor_text = (
|
|
_clean(row.get("cast_descriptor_text"))
|
|
or _prompt_field(_clean(row.get("prompt")), "Characters")
|
|
or _prompt_field(_clean(row.get("prompt")), "Cast descriptors")
|
|
)
|
|
pov_labels = _pov_labels_from_value(row.get("pov_character_labels"))
|
|
cast_prose, cast_labels = _cast_prose(cast_descriptor_text, omit_labels=pov_labels)
|
|
if not cast_labels and women_count == 1 and men_count == 1:
|
|
cast_labels = ["Woman A", "Man A"]
|
|
cast_labels = _merge_labels(cast_labels, pov_labels)
|
|
expression = _filter_pov_labeled_clauses(expression, pov_labels)
|
|
expression = _natural_label_text(expression, cast_labels)
|
|
composition = _sanitize_hardcore_environment_anchors(composition)
|
|
source_composition = _sanitize_hardcore_environment_anchors(source_composition)
|
|
role_graph = _sanitize_scene_text_for_cast(
|
|
_sanitize_hardcore_environment_anchors(row.get("source_role_graph") or row.get("role_graph")),
|
|
cast_labels,
|
|
)
|
|
item = _sanitize_scene_text_for_cast(_sanitize_hardcore_environment_anchors(item), cast_labels)
|
|
role_graph = _natural_label_text(role_graph, cast_labels)
|
|
item = _natural_label_text(item, cast_labels)
|
|
axis_values = _sanitize_hardcore_axis_values(row.get("item_axis_values"))
|
|
detail_density = _normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
|
|
action = _hardcore_action_sentence(role_graph, item, source_composition, axis_values, detail_density)
|
|
action = _pov_action_phrase(action, pov_labels)
|
|
output_composition = _pov_composition_text(composition, pov_labels)
|
|
parts = [
|
|
action,
|
|
_pov_camera_phrase(pov_labels),
|
|
cast_prose,
|
|
f"A consensual explicit adult scene with {subject}" if not action else "",
|
|
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 "",
|
|
_expression_phrase(expression),
|
|
_composition_phrase(output_composition, action, "The image is framed as", detail_density),
|
|
camera,
|
|
style if detail_level != "concise" else "",
|
|
]
|
|
return _paragraph(parts), "metadata(configured_cast)"
|
|
|
|
if primary in ("woman", "man") or subject_type in ("woman", "man", "single_any"):
|
|
subject = _age_subject(row, "adult woman")
|
|
appearance = _appearance_phrase(row)
|
|
parts = [
|
|
_with_indefinite_article(subject),
|
|
f"with {appearance}" if appearance else "",
|
|
f"wearing {item}" if item else "",
|
|
f"{pose}" if pose else "",
|
|
f"with {expression}" if expression else "",
|
|
f"in {scene}" if scene else "",
|
|
f"framed as {composition}" if composition else "",
|
|
camera,
|
|
style if detail_level != "concise" else "",
|
|
]
|
|
return _paragraph([", ".join(part for part in parts[:6] if part), *parts[6:]]), "metadata(single)"
|
|
|
|
if subject_type == "couple" or primary in ("two women", "two men", "a woman and a man"):
|
|
subject = _clean(row.get("subject_phrase") or primary or "adult couple")
|
|
if subject == "woman and man":
|
|
subject = "a woman and a man"
|
|
ages = _age_detail_phrase(_row_value(row, "age", ("Ages",)) or row.get("age_band"))
|
|
body = _row_value(row, "body", ("Body types",)) or _clean(row.get("body_type"))
|
|
parts = [
|
|
f"An adult couple: {subject}, all visibly adult",
|
|
f"Age detail: {ages}" if ages else "",
|
|
f"Body types: {body}" if body else "",
|
|
_couple_clothing_phrase(item) if item else "",
|
|
f"The pose is {pose}" if pose 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 "",
|
|
camera,
|
|
style if detail_level != "concise" else "",
|
|
]
|
|
return _paragraph(parts), "metadata(couple)"
|
|
|
|
subject = _age_subject(row, primary or "adult scene")
|
|
parts = [
|
|
f"{subject}",
|
|
f"featuring {item}" if item else "",
|
|
f"in {scene}" if scene else "",
|
|
f"with {expression}" if expression else "",
|
|
f"framed as {composition}" if composition else "",
|
|
camera,
|
|
style if detail_level != "concise" else "",
|
|
]
|
|
return _paragraph(parts), "metadata(generic)"
|
|
|
|
|
|
def _insta_pair_to_krea(row: dict[str, Any], detail_level: str, style_mode: str) -> tuple[str, str, str, str]:
|
|
descriptor = _clean(row.get("shared_descriptor"))
|
|
cast_descriptors = row.get("shared_cast_descriptors")
|
|
if isinstance(cast_descriptors, list):
|
|
cast_descriptor_text = "; ".join(_clean(item) for item in cast_descriptors if _clean(item))
|
|
else:
|
|
cast_descriptor_text = _clean(cast_descriptors)
|
|
cast_descriptor_text = _prompt_cast_descriptors(cast_descriptor_text)
|
|
soft = row.get("softcore_row") if isinstance(row.get("softcore_row"), dict) else {}
|
|
hard = row.get("hardcore_row") if isinstance(row.get("hardcore_row"), dict) else {}
|
|
soft_camera = _pair_camera_phrase(row.get("softcore_camera_directive"), row.get("softcore_camera_config"), soft)
|
|
hard_camera = _pair_camera_phrase(row.get("hardcore_camera_directive"), row.get("hardcore_camera_config"), hard)
|
|
soft_style = _style_phrase(soft, style_mode)
|
|
hard_style = _style_phrase(hard, style_mode)
|
|
options = row.get("options") if isinstance(row.get("options"), dict) else {}
|
|
soft_level = _clean(options.get("softcore_level")).replace("_", " ")
|
|
hard_level = _clean(options.get("hardcore_level")).replace("_", " ")
|
|
same_room = options.get("continuity") == "same_creator_same_room"
|
|
hard_scene = soft.get("scene_text") if same_room and soft.get("scene_text") else hard.get("scene_text")
|
|
hard_composition = _sanitize_hardcore_environment_anchors(hard.get("composition"))
|
|
hard_source_composition = _sanitize_hardcore_environment_anchors(hard.get("source_composition") or hard_composition)
|
|
pov_labels = _merge_labels(
|
|
_pov_labels_from_value(row.get("pov_character_labels")),
|
|
_pov_labels_from_value(soft.get("pov_character_labels")),
|
|
_pov_labels_from_value(hard.get("pov_character_labels")),
|
|
)
|
|
soft_cast_descriptor_text = (
|
|
cast_descriptor_text
|
|
if options.get("softcore_cast") == "same_as_hardcore"
|
|
else f"Woman A: {descriptor}"
|
|
)
|
|
soft_cast_prose, soft_labels = _cast_prose(
|
|
soft_cast_descriptor_text,
|
|
omit_labels=pov_labels if options.get("softcore_cast") == "same_as_hardcore" else [],
|
|
)
|
|
hard_cast_prose, hard_labels = _cast_prose(cast_descriptor_text, omit_labels=pov_labels)
|
|
soft_labels = _merge_labels(soft_labels, pov_labels if options.get("softcore_cast") == "same_as_hardcore" else [])
|
|
hard_labels = _merge_labels(hard_labels, pov_labels)
|
|
hard_item = _sanitize_scene_text_for_cast(_sanitize_hardcore_environment_anchors(hard.get("item")), hard_labels)
|
|
hard_role_graph = _sanitize_scene_text_for_cast(
|
|
_sanitize_hardcore_environment_anchors(hard.get("source_role_graph") or 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_axis_values = _sanitize_hardcore_axis_values(hard.get("item_axis_values"))
|
|
hard_detail_density = _normalize_hardcore_detail_density(
|
|
hard.get("hardcore_detail_density") or row.get("hardcore_detail_density") or options.get("hardcore_detail_density")
|
|
)
|
|
hard_action = _hardcore_action_sentence(
|
|
hard_role_graph,
|
|
hard_item,
|
|
hard_source_composition,
|
|
hard_axis_values,
|
|
hard_detail_density,
|
|
)
|
|
hard_action = _pov_action_phrase(hard_action, pov_labels)
|
|
hard_output_composition = _pov_composition_text(hard_composition, pov_labels)
|
|
same_soft_cast = options.get("softcore_cast") == "same_as_hardcore"
|
|
soft_output_composition = _pov_composition_text(soft.get("composition"), pov_labels if same_soft_cast else [])
|
|
if same_soft_cast and pov_labels:
|
|
soft_cast_presence = (
|
|
"the woman is framed from the POV participant's first-person camera in a soft creator-teaser pose, "
|
|
"with the POV participant kept off-camera as the viewpoint and implied by camera position or foreground cues"
|
|
)
|
|
else:
|
|
soft_cast_presence = (
|
|
f"{_label_join(soft_labels)} share the frame in a soft creator-teaser pose"
|
|
if same_soft_cast
|
|
else "The image focuses on the woman 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 = ""
|
|
partner_outfit_text = _filter_pov_labeled_clauses(partner_outfit_text, pov_labels)
|
|
if pov_labels:
|
|
partner_pose = ""
|
|
partner_outfit_text = _natural_label_text(partner_outfit_text, soft_labels)
|
|
|
|
soft_expression = ""
|
|
if not _expression_disabled(soft):
|
|
soft_expression_source = _filter_pov_labeled_clauses(
|
|
_clean(soft.get("character_expression_text")) or _clean(soft.get("expression")),
|
|
pov_labels,
|
|
)
|
|
soft_expression = _natural_label_text(
|
|
soft_expression_source,
|
|
soft_labels,
|
|
)
|
|
hard_expression = ""
|
|
if not _expression_disabled(hard):
|
|
hard_expression_source = _filter_pov_labeled_clauses(
|
|
_clean(hard.get("character_expression_text")) or _clean(hard.get("expression")),
|
|
pov_labels,
|
|
)
|
|
hard_expression = _natural_label_text(
|
|
hard_expression_source,
|
|
hard_labels,
|
|
)
|
|
soft_item = _clean(soft.get("item"))
|
|
soft_item_label = _clean(soft.get("softcore_item_prompt_label"))
|
|
soft_item_phrase = ""
|
|
if soft_item:
|
|
soft_item_phrase = f"body exposure: {soft_item}" if soft_item_label == "Body exposure" else f"wearing {soft_item}"
|
|
|
|
soft_parts = [
|
|
soft_cast_prose,
|
|
soft_cast_presence,
|
|
partner_outfit_text,
|
|
partner_pose,
|
|
_pov_camera_phrase(pov_labels, softcore=True) if same_soft_cast else "",
|
|
soft_item_phrase,
|
|
f"{soft.get('pose')}" if soft.get("pose") else "",
|
|
_expression_phrase(soft_expression),
|
|
f"in {soft.get('scene_text')}" if soft.get("scene_text") else "",
|
|
f"framed as {soft_output_composition}" if soft_output_composition else "",
|
|
soft_camera,
|
|
soft_style if detail_level != "concise" else "",
|
|
]
|
|
hard_parts = [
|
|
hard_action,
|
|
_pov_camera_phrase(pov_labels),
|
|
_natural_label_text(
|
|
_filter_pov_labeled_clauses(_natural_clothing_state(row.get("hardcore_clothing_state")), pov_labels),
|
|
hard_labels,
|
|
),
|
|
hard_cast_prose,
|
|
f"set in {hard_scene}" if hard_scene else "",
|
|
_expression_phrase(hard_expression),
|
|
_composition_phrase(hard_output_composition, hard_action, detail_density=hard_detail_density),
|
|
hard_camera,
|
|
hard_style if detail_level != "concise" else "",
|
|
]
|
|
return (
|
|
_paragraph(soft_parts),
|
|
_combine_negative(row.get("softcore_negative_prompt")),
|
|
_paragraph(hard_parts),
|
|
_combine_negative(row.get("hardcore_negative_prompt")),
|
|
)
|
|
|
|
|
|
def _fallback_text_to_krea(
|
|
source_text: str,
|
|
preserve_trigger: bool,
|
|
detail_level: str,
|
|
style_mode: str,
|
|
) -> tuple[str, str, str]:
|
|
positive, negative = _split_avoid(_strip_trigger(source_text, preserve_trigger))
|
|
positive = re.sub(r"\b(?:Scene|Setting):", "The setting is", positive)
|
|
positive = re.sub(r"\b(?:Pose|Sexual pose):", "The pose is", positive)
|
|
positive = re.sub(r"\bFacial expressions?:", "The facial expression is", positive)
|
|
positive = re.sub(r"\bComposition:", "The composition is", positive)
|
|
positive = re.sub(r"\bRole graph:", "The role choreography is", positive)
|
|
positive = re.sub(r"\bUse\b", "Use", positive)
|
|
positive = _clean(positive)
|
|
return _paragraph([positive]), negative, "text(fallback)"
|
|
|
|
|
|
def format_krea2_prompt(
|
|
source_text: str,
|
|
metadata_json: str = "",
|
|
negative_prompt: str = "",
|
|
input_hint: str = "auto",
|
|
target: str = "auto",
|
|
detail_level: str = "balanced",
|
|
style_mode: str = "preserve",
|
|
preserve_trigger: bool = False,
|
|
extra_positive: str = "",
|
|
extra_negative: str = "",
|
|
) -> dict[str, str]:
|
|
detail_level = detail_level if detail_level in ("concise", "balanced", "dense") else "balanced"
|
|
style_mode = style_mode if style_mode in ("preserve", "photographic", "minimal") else "preserve"
|
|
target = target if target in ("auto", "single", "softcore", "hardcore") else "auto"
|
|
row, method = _row_from_inputs(source_text, metadata_json, input_hint)
|
|
extracted_negative = ""
|
|
|
|
if row and row.get("mode") == "Insta/OF":
|
|
soft_prompt, soft_negative, hard_prompt, hard_negative = _insta_pair_to_krea(row, detail_level, style_mode)
|
|
selected = hard_prompt if target == "hardcore" else soft_prompt if target == "softcore" else soft_prompt
|
|
selected_negative = hard_negative if target == "hardcore" else soft_negative
|
|
if extra_positive.strip():
|
|
selected = f"{selected.rstrip()} {extra_positive.strip()}"
|
|
soft_prompt = f"{soft_prompt.rstrip()} {extra_positive.strip()}"
|
|
hard_prompt = f"{hard_prompt.rstrip()} {extra_positive.strip()}"
|
|
negative = _combine_negative(selected_negative, negative_prompt, extra_negative)
|
|
return {
|
|
"krea_prompt": selected,
|
|
"negative_prompt": negative,
|
|
"krea_softcore_prompt": soft_prompt,
|
|
"krea_hardcore_prompt": hard_prompt,
|
|
"softcore_negative_prompt": _combine_negative(soft_negative, extra_negative),
|
|
"hardcore_negative_prompt": _combine_negative(hard_negative, extra_negative),
|
|
"method": f"{method}:krea2(insta_of_pair)",
|
|
}
|
|
|
|
if row:
|
|
prompt, kind = _normal_row_to_krea(row, detail_level, style_mode)
|
|
extracted_negative = _clean(row.get("negative_prompt"))
|
|
method = f"{method}:krea2({kind})"
|
|
else:
|
|
prompt, extracted_negative, method = _fallback_text_to_krea(source_text, preserve_trigger, detail_level, style_mode)
|
|
|
|
if extra_positive.strip():
|
|
prompt = f"{prompt.rstrip()} {extra_positive.strip()}"
|
|
negative = _combine_negative(extracted_negative, negative_prompt, extra_negative)
|
|
return {
|
|
"krea_prompt": prompt,
|
|
"negative_prompt": negative,
|
|
"krea_softcore_prompt": "",
|
|
"krea_hardcore_prompt": "",
|
|
"softcore_negative_prompt": "",
|
|
"hardcore_negative_prompt": "",
|
|
"method": method,
|
|
}
|