Extract shared POV policy
This commit is contained in:
@@ -182,6 +182,10 @@ Already isolated:
|
|||||||
camera-scene prose lives in `scene_camera_adapters.py`; row-level camera
|
camera-scene prose lives in `scene_camera_adapters.py`; row-level camera
|
||||||
insertion, contextual coworking composition mutation, subject-kind detection,
|
insertion, contextual coworking composition mutation, subject-kind detection,
|
||||||
and POV suppression live in `row_camera.py`.
|
and POV suppression live in `row_camera.py`.
|
||||||
|
- shared POV slot detection, label merging/filtering, builder-side POV
|
||||||
|
directives, source role-graph viewer replacement, and shared composition
|
||||||
|
cleanup live in `pov_policy.py`; prompt builder and Krea POV routes delegate
|
||||||
|
to it.
|
||||||
- shared hardcore environment-anchor cleanup lives in
|
- shared hardcore environment-anchor cleanup lives in
|
||||||
`hardcore_text_cleanup.py` and normalizes malformed pool joins before metadata
|
`hardcore_text_cleanup.py` and normalizes malformed pool joins before metadata
|
||||||
reaches formatter routes.
|
reaches formatter routes.
|
||||||
@@ -263,8 +267,9 @@ Already isolated:
|
|||||||
POV formatter routes.
|
POV formatter routes.
|
||||||
- `hardcore_action_metadata.py` owns shared action-family constants,
|
- `hardcore_action_metadata.py` owns shared action-family constants,
|
||||||
normalization, and inference used by the builder and Krea formatter route.
|
normalization, and inference used by the builder and Krea formatter route.
|
||||||
- `krea_pov.py` owns POV labels, POV label filtering, and POV camera/composition
|
- `pov_policy.py` owns shared POV labels, label filtering, source role-graph
|
||||||
support text.
|
viewer replacement, and composition cleanup; `krea_pov.py` owns Krea-specific
|
||||||
|
POV camera support text while delegating shared POV policy.
|
||||||
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
|
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
|
||||||
density limiting for Krea action prose.
|
density limiting for Krea action prose.
|
||||||
- `krea_action_positions.py` owns non-POV pose anchors, body-arrangement text,
|
- `krea_action_positions.py` owns non-POV pose anchors, body-arrangement text,
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ Core helper ownership:
|
|||||||
| `hardcore_role_climax.py` | Climax and ejaculation aftermath role graph wording for face/body/ass, lap, open-thigh, side-lying, and group front/back placement. |
|
| `hardcore_role_climax.py` | Climax and ejaculation aftermath role graph wording for face/body/ass, lap, open-thigh, side-lying, and group front/back placement. |
|
||||||
| `hardcore_action_metadata.py` | Source action-family and position-family metadata used by Krea2, SDXL, and caption routes. |
|
| `hardcore_action_metadata.py` | Source action-family and position-family metadata used by Krea2, SDXL, and caption routes. |
|
||||||
| `route_metadata.py` | Shared row-level route metadata readers for normalized action family, position family/keys, and formatter hints used by Krea2, SDXL, and caption routes. |
|
| `route_metadata.py` | Shared row-level route metadata readers for normalized action family, position family/keys, and formatter hints used by Krea2, SDXL, and caption routes. |
|
||||||
|
| `pov_policy.py` | Shared POV slot detection, POV label merging/filtering, builder POV directives, source role-graph viewer replacement, and shared POV composition cleanup used by builder and Krea2 routes. |
|
||||||
| `scene_camera_adapters.py` | Location-aware camera/scene prose such as coworking lounge camera layout. |
|
| `scene_camera_adapters.py` | Location-aware camera/scene prose such as coworking lounge camera layout. |
|
||||||
| `row_camera.py` | Row-level camera insertion, contextual coworking composition mutation, subject-kind detection, POV label fallback, and POV suppression of normal camera directives. |
|
| `row_camera.py` | Row-level camera insertion, contextual coworking composition mutation, subject-kind detection, POV label fallback, and POV suppression of normal camera directives. |
|
||||||
| `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. |
|
| `krea_cast.py` | Shared formatter cast descriptor parsing, cast labels, cast prose, natural cast descriptor text, and label replacement used by Krea2 and caption routes. |
|
||||||
@@ -334,6 +335,8 @@ Edit targets:
|
|||||||
- Krea2 climax role/detail cleanup: `krea_action_climax.py`.
|
- Krea2 climax role/detail cleanup: `krea_action_climax.py`.
|
||||||
- Krea2 non-POV action-family routing: `krea_action_dispatch.py`.
|
- Krea2 non-POV action-family routing: `krea_action_dispatch.py`.
|
||||||
- Krea2 non-POV action sentence assembly: `krea_actions.py`.
|
- Krea2 non-POV action sentence assembly: `krea_actions.py`.
|
||||||
|
- Shared POV labels/composition cleanup: `pov_policy.py`.
|
||||||
|
- Krea2 POV camera support: `krea_pov.py`.
|
||||||
- Krea2 POV position rewrite: `krea_pov_actions.py`.
|
- Krea2 POV position rewrite: `krea_pov_actions.py`.
|
||||||
|
|
||||||
### Composition
|
### Composition
|
||||||
@@ -637,7 +640,8 @@ Key Krea2 ownership:
|
|||||||
- Climax role/detail cleanup: `krea_action_climax.py`.
|
- Climax role/detail cleanup: `krea_action_climax.py`.
|
||||||
- Non-POV action-family routing: `krea_action_dispatch.py`.
|
- Non-POV action-family routing: `krea_action_dispatch.py`.
|
||||||
- Non-POV hardcore action sentence: `krea_actions.hardcore_action_sentence`.
|
- Non-POV hardcore action sentence: `krea_actions.hardcore_action_sentence`.
|
||||||
- POV labels, filtering, and camera/composition support: `krea_pov.py`.
|
- Shared POV labels/filtering/composition cleanup: `pov_policy.py`.
|
||||||
|
- Krea POV camera support: `krea_pov.py`.
|
||||||
- Detail clause splitting and density limiting: `krea_detail.py`.
|
- Detail clause splitting and density limiting: `krea_detail.py`.
|
||||||
- POV hardcore sentence: `krea_pov_actions.pov_action_phrase`.
|
- POV hardcore sentence: `krea_pov_actions.pov_action_phrase`.
|
||||||
- Clothing state cleanup: `krea_clothing.natural_clothing_state`.
|
- Clothing state cleanup: `krea_clothing.natural_clothing_state`.
|
||||||
@@ -871,8 +875,8 @@ Use these traces to narrow a problem in one pass.
|
|||||||
2. Confirm Krea input uses metadata, not plain prompt fallback.
|
2. Confirm Krea input uses metadata, not plain prompt fallback.
|
||||||
3. Inspect `source_role_graph`, `item`, `source_composition`, and
|
3. Inspect `source_role_graph`, `item`, `source_composition`, and
|
||||||
`item_axis_values`.
|
`item_axis_values`.
|
||||||
4. Inspect `krea_pov.py` if the label omission, camera phrase, or POV
|
4. Inspect `pov_policy.py` if label omission or POV composition cleanup is
|
||||||
composition cleanup is wrong.
|
wrong; inspect `krea_pov.py` if the Krea camera phrase is wrong.
|
||||||
5. Edit `krea_pov_actions.py` if the first-person body geometry is wrong.
|
5. Edit `krea_pov_actions.py` if the first-person body geometry is wrong.
|
||||||
6. 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.
|
for any formatter to infer a good POV prompt.
|
||||||
|
|||||||
+8
-50
@@ -1,50 +1,23 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
def _clean(value: Any) -> str:
|
from . import pov_policy
|
||||||
text = "" if value is None else str(value)
|
except ImportError: # Allows local smoke tests with top-level imports.
|
||||||
text = text.replace("\n", " ")
|
import pov_policy
|
||||||
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]:
|
def pov_labels_from_value(value: Any) -> list[str]:
|
||||||
labels: list[str] = []
|
return pov_policy.pov_labels_from_value(value)
|
||||||
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]:
|
def merge_labels(*groups: list[str]) -> list[str]:
|
||||||
merged: list[str] = []
|
return pov_policy.merge_labels(*groups)
|
||||||
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:
|
def filter_pov_labeled_clauses(text: Any, pov_labels: list[str]) -> str:
|
||||||
rendered = _clean(text)
|
return pov_policy.filter_pov_labeled_clauses(text, pov_labels)
|
||||||
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:
|
def pov_camera_phrase(pov_labels: list[str], softcore: bool = False) -> str:
|
||||||
@@ -60,19 +33,4 @@ def pov_camera_phrase(pov_labels: list[str], softcore: bool = False) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def pov_composition_text(composition: Any, pov_labels: list[str]) -> str:
|
def pov_composition_text(composition: Any, pov_labels: list[str]) -> str:
|
||||||
text = _clean(composition)
|
return pov_policy.pov_composition_formatter_text(composition, pov_labels)
|
||||||
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)
|
|
||||||
|
|||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
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)
|
||||||
+8
-41
@@ -39,6 +39,7 @@ try:
|
|||||||
from . import pair_output
|
from . import pair_output
|
||||||
from . import pair_rows
|
from . import pair_rows
|
||||||
from . import pair_options
|
from . import pair_options
|
||||||
|
from . import pov_policy
|
||||||
from . import row_normalization as row_policy
|
from . import row_normalization as row_policy
|
||||||
from . import row_camera as row_camera_policy
|
from . import row_camera as row_camera_policy
|
||||||
from . import row_location as row_location_policy
|
from . import row_location as row_location_policy
|
||||||
@@ -81,6 +82,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
import pair_output
|
import pair_output
|
||||||
import pair_rows
|
import pair_rows
|
||||||
import pair_options
|
import pair_options
|
||||||
|
import pov_policy
|
||||||
import row_normalization as row_policy
|
import row_normalization as row_policy
|
||||||
import row_camera as row_camera_policy
|
import row_camera as row_camera_policy
|
||||||
import row_location as row_location_policy
|
import row_location as row_location_policy
|
||||||
@@ -1790,9 +1792,7 @@ def _normalize_presence_mode(value: Any, subject_type: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
|
def _slot_is_pov(slot: dict[str, Any] | None) -> bool:
|
||||||
if not slot:
|
return pov_policy.slot_is_pov(slot)
|
||||||
return False
|
|
||||||
return slot.get("subject_type") == "man" and slot.get("presence_mode") == "pov"
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_slot_expression_intensity(value: Any) -> float:
|
def _normalize_slot_expression_intensity(value: Any) -> float:
|
||||||
@@ -2457,56 +2457,23 @@ def _pov_character_labels(
|
|||||||
label_map: dict[str, dict[str, Any]],
|
label_map: dict[str, dict[str, Any]],
|
||||||
men_count: int | None = None,
|
men_count: int | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
if men_count is None:
|
return pov_policy.pov_character_labels(label_map, men_count)
|
||||||
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 _pov_text_with_viewer(text: Any, pov_labels: list[str]) -> str:
|
def _pov_text_with_viewer(text: Any, pov_labels: list[str]) -> str:
|
||||||
rendered = str(text or "").strip()
|
return pov_policy.pov_text_with_viewer(text, pov_labels)
|
||||||
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_prompt_punctuation(rendered)
|
|
||||||
|
|
||||||
|
|
||||||
def _pov_role_graph_prompt(role_graph: Any, pov_labels: list[str]) -> str:
|
def _pov_role_graph_prompt(role_graph: Any, pov_labels: list[str]) -> str:
|
||||||
role_graph_text = str(role_graph or "").strip()
|
return pov_policy.pov_role_graph_prompt(role_graph, pov_labels)
|
||||||
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:
|
def _pov_prompt_directive(pov_labels: list[str]) -> str:
|
||||||
if not pov_labels:
|
return pov_policy.pov_prompt_directive(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_prompt(composition: Any, pov_labels: list[str]) -> str:
|
def _pov_composition_prompt(composition: Any, pov_labels: list[str]) -> str:
|
||||||
text = str(composition or "").strip()
|
return pov_policy.pov_composition_prompt(composition, pov_labels)
|
||||||
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_prompt_punctuation(text)
|
|
||||||
|
|
||||||
|
|
||||||
def _body_exposure_scene_text(scene: Any) -> str:
|
def _body_exposure_scene_text(scene: Any) -> str:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import krea_formatter # noqa: E402
|
|||||||
import location_config # noqa: E402
|
import location_config # noqa: E402
|
||||||
import loop_nodes # noqa: E402
|
import loop_nodes # noqa: E402
|
||||||
import prompt_builder as pb # noqa: E402
|
import prompt_builder as pb # noqa: E402
|
||||||
|
import pov_policy # noqa: E402
|
||||||
import row_normalization # noqa: E402
|
import row_normalization # noqa: E402
|
||||||
import route_metadata # noqa: E402
|
import route_metadata # noqa: E402
|
||||||
import row_camera # noqa: E402
|
import row_camera # noqa: E402
|
||||||
@@ -50,6 +51,7 @@ import server_routes # noqa: E402
|
|||||||
import sdxl_formatter # noqa: E402
|
import sdxl_formatter # noqa: E402
|
||||||
import sdxl_presets # noqa: E402
|
import sdxl_presets # noqa: E402
|
||||||
import seed_config # noqa: E402
|
import seed_config # noqa: E402
|
||||||
|
import krea_pov # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
Trigger = "sxcppnl7"
|
Trigger = "sxcppnl7"
|
||||||
@@ -798,6 +800,32 @@ def smoke_character_config_policy() -> None:
|
|||||||
_expect(hair.get("styles") == ["messy_bun", "straight"], "Hair style config merge changed")
|
_expect(hair.get("styles") == ["messy_bun", "straight"], "Hair style config merge changed")
|
||||||
_expect(pb._hair_phrase_from_parts("platinum_blonde", "long", "messy_bun") == "long platinum-blonde hair in a messy bun", "Hair phrase helper changed")
|
_expect(pb._hair_phrase_from_parts("platinum_blonde", "long", "messy_bun") == "long platinum-blonde hair in a messy bun", "Hair phrase helper changed")
|
||||||
_expect(character_config.normalize_presence_mode("pov", "woman") == "visible", "POV presence should stay man-only")
|
_expect(character_config.normalize_presence_mode("pov", "woman") == "visible", "POV presence should stay man-only")
|
||||||
|
pov_slot = {"subject_type": "man", "presence_mode": "pov"}
|
||||||
|
visible_slot = {"subject_type": "man", "presence_mode": "visible"}
|
||||||
|
_expect(pb._slot_is_pov(pov_slot) is True, "Prompt builder POV slot helper should delegate to POV policy")
|
||||||
|
_expect(pov_policy.slot_is_pov(visible_slot) is False, "Visible man slot should not be POV")
|
||||||
|
_expect(
|
||||||
|
pb._pov_character_labels({"Man A": pov_slot, "Man B": visible_slot}, 2) == ["Man A"],
|
||||||
|
"POV label selection should keep only POV men in count order",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
pb._pov_role_graph_prompt("Man A is positioned behind Woman A", ["Man A"])
|
||||||
|
== "First-person POV from Man A; the POV camera is positioned behind Woman A",
|
||||||
|
"Builder POV role graph prompt should use shared viewer replacement",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
pb._pov_composition_prompt("wide group-sex composition with all bodies visible", ["Man A"])
|
||||||
|
== "first-person group-sex POV composition with visible partners readable",
|
||||||
|
"Builder POV composition prompt should use shared POV composition replacements",
|
||||||
|
)
|
||||||
|
_expect(
|
||||||
|
krea_pov.pov_composition_text(
|
||||||
|
"wide group-sex composition with all bodies visible, adapted for first-person POV with the POV participant kept off-camera",
|
||||||
|
["Man A"],
|
||||||
|
)
|
||||||
|
== "first-person group-sex POV composition with visible partners readable",
|
||||||
|
"Krea POV composition cleanup should delegate shared replacements and strip builder annotation",
|
||||||
|
)
|
||||||
_expect(character_config.normalize_slot_seed(0xFFFFFFFF + 99) == 0xFFFFFFFF, "Slot seed clamp changed")
|
_expect(character_config.normalize_slot_seed(0xFFFFFFFF + 99) == 0xFFFFFFFF, "Slot seed clamp changed")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user