from __future__ import annotations import re from typing import Any def clean_pov_text(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) text = re.sub(r"(?:,\s*){2,}", ", ", text) text = re.sub(r"\.\s*\.", ".", text) text = re.sub(r":\s*\.", ".", text) return text.strip() def slot_is_pov(slot: dict[str, Any] | None) -> bool: if not slot: return False return slot.get("subject_type") == "man" and slot.get("presence_mode") == "pov" def pov_labels_from_value(value: Any) -> list[str]: labels: list[str] = [] if isinstance(value, list): candidates = value else: text = clean_pov_text(value) candidates = re.split(r"[,;]\s*", text) if text else [] for candidate in candidates: label = clean_pov_text(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 pov_character_labels( label_map: dict[str, dict[str, Any]], men_count: int | None = None, ) -> list[str]: if men_count is None: labels = sorted(label for label in label_map if label.startswith("Man ")) else: labels = [f"Man {chr(ord('A') + index)}" for index in range(max(0, men_count))] return [label for label in labels if slot_is_pov(label_map.get(label))] def filter_pov_labeled_clauses(text: Any, pov_labels: list[str]) -> str: rendered = clean_pov_text(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_text_with_viewer(text: Any, pov_labels: list[str]) -> str: rendered = clean_pov_text(text) 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) rendered = re.sub( r"\bthe POV viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE, ) return clean_pov_text(rendered) def pov_role_graph_prompt(role_graph: Any, pov_labels: list[str]) -> str: role_graph_text = clean_pov_text(role_graph) if not role_graph_text or not pov_labels: return role_graph_text viewer_text = pov_text_with_viewer(role_graph_text, pov_labels) label_text = ", ".join(pov_labels) return f"First-person POV from {label_text}; {viewer_text}" def pov_prompt_directive(pov_labels: list[str]) -> str: if not pov_labels: return "" label_text = ", ".join(pov_labels) return ( f"POV participant: {label_text} is the first-person camera viewpoint; " "he remains the off-camera viewpoint, represented by foreground hands, body position, or camera perspective cues when needed." ) def pov_composition_base_text(composition: Any, pov_labels: list[str]) -> str: text = clean_pov_text(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) return clean_pov_text(text) def pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str: text = pov_composition_base_text(composition, pov_labels) if not text or not pov_labels: return text 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_pov_text(text) def pov_composition_formatter_text(composition: Any, pov_labels: list[str]) -> str: text = pov_composition_base_text(composition, pov_labels) if not text or not pov_labels: return text text = re.sub( r",?\s*adapted for first-person POV with the POV participant kept off-camera\b", "", text, flags=re.IGNORECASE, ) text = re.sub(r",?\s*with the POV participant kept off-camera\b", "", text, flags=re.IGNORECASE) return clean_pov_text(text)