From 6c5a529e2904947af42a1db103273f735871bee6 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 15:40:41 +0200 Subject: [PATCH] Extract Krea POV support helpers --- docs/prompt-architecture-improvement-plan.md | 4 +- docs/prompt-pool-routing-map.md | 7 +- krea_formatter.py | 80 ++++---------------- krea_pov.py | 78 +++++++++++++++++++ 4 files changed, 100 insertions(+), 69 deletions(-) create mode 100644 krea_pov.py diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 61e9d02..4df10dd 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -149,11 +149,13 @@ Already isolated: - `krea_action_context.py` owns shared action-family predicates, axis context text, climax detection, and detail-density normalization used by action and POV formatter routes. +- `krea_pov.py` owns POV labels, POV label filtering, and POV camera/composition + support text. Improve later: - split semantic blocks into modules: - `krea_actions.py`, `krea_pov.py`; + `krea_actions.py`, `krea_pov_actions.py`; - add route-level smoke fixtures for representative metadata rows; - make `_hardcore_action_sentence` dispatch by action family instead of long conditional chains. diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 3c86abf..cdc2bf0 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -548,6 +548,7 @@ Key Krea2 ownership: - Cast descriptor naturalization: `krea_cast.cast_prose`, `krea_cast.natural_label_text`. - Action context and family predicates: `krea_action_context.py`. +- POV labels, filtering, and camera/composition support: `krea_pov.py`. - Hardcore action sentence: `_hardcore_action_sentence`. - POV hardcore sentence: `_pov_hardcore_pose_sentence`, `_pov_action_phrase`. - Clothing state cleanup: `krea_clothing.natural_clothing_state`. @@ -744,9 +745,11 @@ Use these traces to narrow a problem in one pass. 2. Confirm Krea input uses metadata, not plain prompt fallback. 3. Inspect `source_role_graph`, `item`, `source_composition`, and `item_axis_values`. -4. Edit `_pov_hardcore_pose_sentence` if the first-person body geometry is +4. Inspect `krea_pov.py` if the label omission, camera phrase, or POV + composition cleanup is wrong. +5. Edit `_pov_hardcore_pose_sentence` if the first-person body geometry is wrong. -5. Edit `sexual_poses.json` if the raw action lacks enough body-position anchor +6. Edit `sexual_poses.json` if the raw action lacks enough body-position anchor for any formatter to infer a good POV prompt. ### Camera disappears or becomes too generic diff --git a/krea_formatter.py b/krea_formatter.py index 327df53..b216003 100644 --- a/krea_formatter.py +++ b/krea_formatter.py @@ -29,6 +29,13 @@ try: prompt_cast_descriptors as _prompt_cast_descriptors, ) from .krea_clothing import natural_clothing_state as _natural_clothing_state + from .krea_pov import ( + filter_pov_labeled_clauses as _filter_pov_labeled_clauses, + merge_labels as _merge_labels, + pov_camera_phrase as _pov_camera_phrase, + pov_composition_text as _pov_composition_text, + pov_labels_from_value as _pov_labels_from_value, + ) from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text except ImportError: # Allows local smoke tests with `python -c`. from krea_action_context import ( @@ -55,6 +62,13 @@ except ImportError: # Allows local smoke tests with `python -c`. prompt_cast_descriptors as _prompt_cast_descriptors, ) from krea_clothing import natural_clothing_state as _natural_clothing_state + from krea_pov import ( + filter_pov_labeled_clauses as _filter_pov_labeled_clauses, + merge_labels as _merge_labels, + pov_camera_phrase as _pov_camera_phrase, + pov_composition_text as _pov_composition_text, + pov_labels_from_value as _pov_labels_from_value, + ) from prompt_hygiene import sanitize_negative_text, sanitize_prose_text @@ -230,41 +244,6 @@ def _combine_negative(*parts: str) -> str: return ", ".join(cleaned) -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_ejaculation_target(context: str) -> str: if any(token in context for token in ("face", "mouth", "lips", "tongue", "chin")): return "onto her face and chest" @@ -562,37 +541,6 @@ def _pov_action_phrase( return rendered -def _pov_camera_phrase(pov_labels: list[str], softcore: bool = False) -> str: - if not pov_labels: - return "" - if softcore: - return ( - "Camera is the male participant's first-person creator view in one continuous frame, with him implied by perspective or foreground cues" - ) - return ( - "Camera is the male participant's first-person view in one continuous frame; only his foreground hands or body cues appear" - ) - - -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) - 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(text) - - def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str: text = _clean(text) if not text: diff --git a/krea_pov.py b/krea_pov.py new file mode 100644 index 0000000..b7bc7e7 --- /dev/null +++ b/krea_pov.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +import re +from typing import Any + + +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 + + +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_camera_phrase(pov_labels: list[str], softcore: bool = False) -> str: + if not pov_labels: + return "" + if softcore: + return ( + "Camera is the male participant's first-person creator view in one continuous frame, with him implied by perspective or foreground cues" + ) + return ( + "Camera is the male participant's first-person view in one continuous frame; only his foreground hands or body cues appear" + ) + + +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) + 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(text)