140 lines
5.0 KiB
Python
140 lines
5.0 KiB
Python
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)
|