Extract Krea POV action helpers

This commit is contained in:
2026-06-26 15:49:43 +02:00
parent 6c5a529e29
commit 0b9ee3b8b1
5 changed files with 409 additions and 344 deletions
+9 -6
View File
@@ -135,8 +135,8 @@ Owner: `krea_formatter.py`.
Keep here: Keep here:
- Krea prose style; - Krea prose style;
- hardcore action sentence rewriting; - Krea route orchestration;
- POV sentence rewriting; - remaining hardcore action sentence dispatch;
- camera-scene preservation; - camera-scene preservation;
- fallback text parsing. - fallback text parsing.
@@ -151,11 +151,14 @@ Already isolated:
POV formatter routes. POV formatter routes.
- `krea_pov.py` owns POV labels, POV label filtering, and POV camera/composition - `krea_pov.py` owns POV labels, POV label filtering, and POV camera/composition
support text. support text.
- `krea_detail.py` owns generic detail-clause splitting, deduping, joining, and
density limiting for Krea action prose.
- `krea_pov_actions.py` owns POV hardcore action sentence rewriting and
first-person body geometry.
Improve later: Improve later:
- split semantic blocks into modules: - split the remaining non-POV hardcore action dispatcher into `krea_actions.py`;
`krea_actions.py`, `krea_pov_actions.py`;
- add route-level smoke fixtures for representative metadata rows; - add route-level smoke fixtures for representative metadata rows;
- make `_hardcore_action_sentence` dispatch by action family instead of long - make `_hardcore_action_sentence` dispatch by action family instead of long
conditional chains. conditional chains.
@@ -282,7 +285,7 @@ Near-term:
Medium-term: Medium-term:
- Dispatch action rewriting by action family. - Dispatch action rewriting by action family.
- Split Krea semantic helpers into smaller modules. - Continue splitting remaining Krea semantic helpers into smaller modules.
### SDXL ### SDXL
@@ -336,7 +339,7 @@ Medium-term:
## Recommended Next Passes ## Recommended Next Passes
1. Split Krea action/POV helpers into separate modules, using 1. Split the remaining Krea action dispatcher into `krea_actions.py`, using
`krea_cast.py` as the pattern for stable import aliases and smoke coverage. `krea_cast.py` as the pattern for stable import aliases and smoke coverage.
2. Split `__init__.py` node classes by family after behavior is covered by smoke 2. Split `__init__.py` node classes by family after behavior is covered by smoke
checks. checks.
+13 -11
View File
@@ -84,7 +84,7 @@ These recipes identify the intended road before editing prompt text.
| --- | --- | --- | --- | | --- | --- | --- | --- |
| Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, Krea `_hardcore_action_sentence` | | Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, Krea `_hardcore_action_sentence` |
| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `_hardcore_action_sentence` | | Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `_hardcore_action_sentence` |
| Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `_pov_hardcore_pose_sentence`, `_pov_action_phrase`, `krea_cast.cast_prose` omit-label handling | | Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `krea_pov_actions.py`, `krea_pov.py`, `krea_cast.cast_prose` omit-label handling |
| Generate porn-scene interaction beats | `Hardcore Position Pool` -> `Hardcore Action Filter` -> pair/builder | Use `focus=interaction_only` for kissing/body worship/transitions/guidance/camera/watching/aftercare, or `focus=manual_only` for fingering/clit/manual stimulation; constrain keys such as `camera_showing`, `wrist_pinning`, `fingering`, `aftercare` | `sexual_poses.json` interaction/manual subcategories, `_role_graph`, Krea `_is_foreplay_text` / `_hardcore_action_sentence` | | Generate porn-scene interaction beats | `Hardcore Position Pool` -> `Hardcore Action Filter` -> pair/builder | Use `focus=interaction_only` for kissing/body worship/transitions/guidance/camera/watching/aftercare, or `focus=manual_only` for fingering/clit/manual stimulation; constrain keys such as `camera_showing`, `wrist_pinning`, `fingering`, `aftercare` | `sexual_poses.json` interaction/manual subcategories, `_role_graph`, Krea `_is_foreplay_text` / `_hardcore_action_sentence` |
| Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields | | Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields |
| Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `_insta_of_partner_styling`, character slot clothing, pair Krea branch | | Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `_insta_of_partner_styling`, character slot clothing, pair Krea branch |
@@ -275,7 +275,8 @@ Edit targets:
`camera_performance`, `group_coordination`, and `aftercare_cleanup`. `camera_performance`, `group_coordination`, and `aftercare_cleanup`.
- Position filtering UI: `build_hardcore_position_pool_json`, - Position filtering UI: `build_hardcore_position_pool_json`,
`build_hardcore_action_filter_json`, `_apply_hardcore_position_config_to_subcategory`. `build_hardcore_action_filter_json`, `_apply_hardcore_position_config_to_subcategory`.
- Krea2 action rewrite, POV position rewrite, cleanup: `krea_formatter.py`. - Krea2 action rewrite orchestration: `krea_formatter.py`.
- Krea2 POV position rewrite: `krea_pov_actions.py`.
### Composition ### Composition
@@ -460,8 +461,9 @@ What each part owns:
- `sexual_poses.json`: available positions, families, action templates, role - `sexual_poses.json`: available positions, families, action templates, role
graph templates, interaction templates, and action-specific pool references. graph templates, interaction templates, and action-specific pool references.
- `prompt_builder.py`: filters which templates/axes remain available. - `prompt_builder.py`: filters which templates/axes remain available.
- `krea_formatter.py`: rewrites the selected action into model-readable prose, - `krea_formatter.py`: orchestrates the selected action rewrite into
including POV variants and cleanup. model-readable prose.
- `krea_pov_actions.py`: rewrites POV variants with first-person geometry.
Current broad hardcore families: Current broad hardcore families:
@@ -549,8 +551,9 @@ Key Krea2 ownership:
`krea_cast.natural_label_text`. `krea_cast.natural_label_text`.
- Action context and family predicates: `krea_action_context.py`. - Action context and family predicates: `krea_action_context.py`.
- POV labels, filtering, and camera/composition support: `krea_pov.py`. - POV labels, filtering, and camera/composition support: `krea_pov.py`.
- Detail clause splitting and density limiting: `krea_detail.py`.
- Hardcore action sentence: `_hardcore_action_sentence`. - Hardcore action sentence: `_hardcore_action_sentence`.
- POV hardcore sentence: `_pov_hardcore_pose_sentence`, `_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`.
- Camera scene preservation: `_camera_scene_phrase`. - Camera scene preservation: `_camera_scene_phrase`.
@@ -559,9 +562,9 @@ Krea2 field consumption:
| Branch | Reads most from | Key functions | | Branch | Reads most from | Key functions |
| --- | --- | --- | | --- | --- | --- |
| Normal single row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `_normal_row_to_krea` | | Normal single row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `_normal_row_to_krea` |
| Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `_normal_row_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase` | | Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `_normal_row_to_krea`, `_hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase` |
| Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `_insta_pair_to_krea` | | Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `_insta_pair_to_krea` |
| Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `_insta_pair_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase`, `krea_clothing.natural_clothing_state` | | Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `_insta_pair_to_krea`, `_hardcore_action_sentence`, `krea_pov_actions.pov_action_phrase`, `krea_clothing.natural_clothing_state` |
| Plain text fallback | `source_text` only | `_fallback_text_to_krea` | | Plain text fallback | `source_text` only | `_fallback_text_to_krea` |
If metadata is connected and `method` says `text(fallback)`, the formatter did If metadata is connected and `method` says `text(fallback)`, the formatter did
@@ -712,8 +715,8 @@ pair metadata through the core Python APIs, then verifies:
| Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `_apply_hardcore_position_config_to_subcategory`. | | Same hardcore action repeats | Hardcore filter config, `sexual_poses.json` weights, `_apply_hardcore_position_config_to_subcategory`. |
| Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and Krea `_is_foreplay_text` / `_hardcore_pose_anchor`. | | Hardcore interaction beat falls back to penetration/oral | `sexual_poses.json` interaction subcategory, `_role_graph`, and Krea `_is_foreplay_text` / `_hardcore_pose_anchor`. |
| Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. | | Raw hardcore prompt position is vague | `sexual_poses.json` item templates and role graph templates. |
| Krea2 hardcore prompt position is vague | `_hardcore_action_sentence` or `_pov_hardcore_pose_sentence`. | | Krea2 hardcore prompt position is vague | `_hardcore_action_sentence` or `krea_pov_actions.py`. |
| Man appears described in POV | POV labels, `_cast_prose` omit labels, `_pov_action_phrase`. | | Man appears described in POV | POV labels, `krea_cast.cast_prose` omit labels, `krea_pov_actions.pov_action_phrase`. |
| Camera prompt missing from Krea2 | Row `camera_directive` / `camera_scene_directive`, then Krea `_camera_phrase`. | | Camera prompt missing from Krea2 | Row `camera_directive` / `camera_scene_directive`, then Krea `_camera_phrase`. |
| Trigger missing in Krea2 fallback | `format_krea2_prompt` preserve-trigger fallback behavior. | | Trigger missing in Krea2 fallback | `format_krea2_prompt` preserve-trigger fallback behavior. |
| SDXL tags too weak/wrong style | `sdxl_formatter.py` presets and `_row_core_tags` / `_soft_tags` / `_hard_tags`. | | SDXL tags too weak/wrong style | `sdxl_formatter.py` presets and `_row_core_tags` / `_soft_tags` / `_hard_tags`. |
@@ -747,8 +750,7 @@ Use these traces to narrow a problem in one pass.
`item_axis_values`. `item_axis_values`.
4. Inspect `krea_pov.py` if the label omission, camera phrase, or POV 4. Inspect `krea_pov.py` if the label omission, camera phrase, or POV
composition cleanup is wrong. composition cleanup is wrong.
5. Edit `_pov_hardcore_pose_sentence` if the first-person body geometry is 5. Edit `krea_pov_actions.py` if the first-person body geometry is wrong.
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.
+47
View File
@@ -0,0 +1,47 @@
from __future__ import annotations
import re
from typing import Any
try:
from .krea_action_context import normalize_hardcore_detail_density
except ImportError: # Allows local smoke tests with `python -c`.
from krea_action_context import normalize_hardcore_detail_density
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 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 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])
+12 -327
View File
@@ -29,6 +29,11 @@ try:
prompt_cast_descriptors as _prompt_cast_descriptors, prompt_cast_descriptors as _prompt_cast_descriptors,
) )
from .krea_clothing import natural_clothing_state as _natural_clothing_state from .krea_clothing import natural_clothing_state as _natural_clothing_state
from .krea_detail import (
detail_clauses as _detail_clauses,
join_detail_clauses as _join_detail_clauses,
limit_detail_for_density as _limit_detail_for_density,
)
from .krea_pov import ( from .krea_pov import (
filter_pov_labeled_clauses as _filter_pov_labeled_clauses, filter_pov_labeled_clauses as _filter_pov_labeled_clauses,
merge_labels as _merge_labels, merge_labels as _merge_labels,
@@ -36,6 +41,7 @@ try:
pov_composition_text as _pov_composition_text, pov_composition_text as _pov_composition_text,
pov_labels_from_value as _pov_labels_from_value, pov_labels_from_value as _pov_labels_from_value,
) )
from .krea_pov_actions import pov_action_phrase as _pov_action_phrase
from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text from .prompt_hygiene import sanitize_negative_text, sanitize_prose_text
except ImportError: # Allows local smoke tests with `python -c`. except ImportError: # Allows local smoke tests with `python -c`.
from krea_action_context import ( from krea_action_context import (
@@ -62,6 +68,11 @@ except ImportError: # Allows local smoke tests with `python -c`.
prompt_cast_descriptors as _prompt_cast_descriptors, prompt_cast_descriptors as _prompt_cast_descriptors,
) )
from krea_clothing import natural_clothing_state as _natural_clothing_state from krea_clothing import natural_clothing_state as _natural_clothing_state
from krea_detail import (
detail_clauses as _detail_clauses,
join_detail_clauses as _join_detail_clauses,
limit_detail_for_density as _limit_detail_for_density,
)
from krea_pov import ( from krea_pov import (
filter_pov_labeled_clauses as _filter_pov_labeled_clauses, filter_pov_labeled_clauses as _filter_pov_labeled_clauses,
merge_labels as _merge_labels, merge_labels as _merge_labels,
@@ -69,6 +80,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
pov_composition_text as _pov_composition_text, pov_composition_text as _pov_composition_text,
pov_labels_from_value as _pov_labels_from_value, pov_labels_from_value as _pov_labels_from_value,
) )
from krea_pov_actions import pov_action_phrase as _pov_action_phrase
from prompt_hygiene import sanitize_negative_text, sanitize_prose_text from prompt_hygiene import sanitize_negative_text, sanitize_prose_text
@@ -244,303 +256,6 @@ def _combine_negative(*parts: str) -> str:
return ", ".join(cleaned) return ", ".join(cleaned)
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"
if any(token in context for token in ("lower back", "ass", "rear-entry", "face-down", "bent-over", "doggy")):
return "across her ass, thighs, and lower back"
if any(token in context for token in ("pussy", "open thighs", "thighs", "legs open")):
return "across her pussy and thighs"
return "onto her body"
def _pov_contact_clause(
action: Any,
role_graph: Any,
hard_item: Any,
axis_values: Any,
context: str,
) -> str:
is_climax = _is_climax_text(action, role_graph, hard_item, _axis_values_text(axis_values))
if is_climax:
return f"as he ejaculates semen {_pov_ejaculation_target(context)}"
is_anal = any(
token in context
for token in (
"anal",
"into her ass",
"penis entering ass",
"ass stretched",
"thrusts into her ass",
)
)
contact = "as his penis penetrates her ass" if is_anal else "as his penis penetrates her pussy"
if _is_toy_assisted_double_text(action, role_graph, hard_item, _axis_values_text(axis_values)):
contact = f"{contact} while a toy is positioned at the second penetration point"
return contact
def _pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
detail = _clean(detail).strip(" .;")
if not detail:
return ""
detail = re.sub(r"\bthe POV viewer\b", "the viewer", detail, flags=re.IGNORECASE)
detail = re.sub(r"\bthe man's\b", "the viewer's", detail, flags=re.IGNORECASE)
detail = re.sub(r"\bthe man\b", "the viewer", detail, flags=re.IGNORECASE)
detail = re.sub(
r"^(?:missionary|cowgirl|reverse cowgirl|doggy style|standing sex|spooning sex|edge-supported|edge-of-bed|raised edge|kneeling straddle|lotus sex|bent-over|face-down ass-up|side-lying|kneeling rear-entry)\s+(?:position|pose)\s+(?:featuring|with|while|,)?\s*",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(r"^(?:featuring|with)\s+", "", detail, flags=re.IGNORECASE)
detail = re.sub(
r"^(?:full-body|explicit|close-contact|deep|hardcore|vaginal|anal)?\s*(?:penetrative sex|vaginal sex|anal sex|penetration with visible genital contact|hardcore vaginal thrusting|hardcore anal thrusting),?\s*",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(
r"\b(?:front-facing|close-up|wide full-body|wide|overhead|mirror-reflected|low-angle|side-profile|bed-level)\s+view of\b",
"visible",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(
r",?\s*\bthe viewer is behind her at hip level with (?:his|the viewer's) hands on her hips in the foreground as (?:his|the viewer's) penis (?:thrusts into her|penetrates her pussy)\b",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(
r",?\s*\bthe woman is on all fours directly in front of the viewer with hips raised and back arched\b",
"",
detail,
flags=re.IGNORECASE,
)
if any(token in context for token in ("ass raised", "on all fours", "doggy", "rear-entry", "bent-over", "face-down")):
detail = re.sub(
r",?\s*\b(?:one body pinned under another|bodies stacked close together|bodies tangled on the sheets)\b",
"",
detail,
flags=re.IGNORECASE,
)
if "toy is positioned at the second penetration point" in context:
detail = re.sub(
r",?\s*\b(?:toy aligned for a second penetration point|toy-assisted second contact aligned behind the body)\b",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(r"\bwith with\b", "with", detail, flags=re.IGNORECASE)
detail = re.sub(r"\s*,\s*", ", ", detail)
detail = re.sub(r",\s*,", ",", detail).strip(" ,;")
return _limit_detail_for_density(detail, _normalize_hardcore_detail_density(detail_density), _is_climax_text(context, detail))
def _pov_hardcore_pose_sentence(
action: Any,
role_graph: Any,
hard_item: Any,
composition: Any = "",
axis_values: Any = None,
detail_density: str = "balanced",
) -> str:
context = _position_context_text(role_graph, hard_item, composition, axis_values)
action_text = _clean(action)
action_lower = action_text.lower()
if not context:
context = action_lower
def sentence(base: str) -> str:
details = ""
if ";" in action_text:
details = _pov_clean_detail(action_text.split(";", 1)[1], f"{context} {base}", detail_density)
return f"{base}; {details}" if details else base
def outercourse_sentence(base: str) -> str:
return _clean(base).rstrip(".")
if (
"face-sitting" in context
or "face sitting" in context
or ("straddles" in context and "face" in context and "pussy" in context)
):
return outercourse_sentence(
"The woman is above the camera in a close first-person underview, straddling the viewer's face with her thighs on both sides of his head; "
"her pussy is directly over the viewer's mouth in the lower foreground, tongue contact visible from below"
)
if _is_outercourse_text(context, action_lower):
if any(term in context for term in ("boobjob", "titjob", "breast sex", "breast-sex")):
return outercourse_sentence(
"The woman kneels between the viewer's open thighs with her torso bent forward over his pelvis and shoulders low; "
"both hands lift and press her breasts tightly around the viewer's penis shaft in the lower foreground, with the glans just below her lips"
)
if any(term in context for term in ("testicle", "balls licking", "balls-licking", "balls and mouth")):
return outercourse_sentence(
"The woman kneels very low between the viewer's open thighs with her torso bent forward and shoulders between his knees; "
"her head is tucked under the penis shaft at the base of the penis, mouth and tongue licking the viewer's balls while his penis points upward above her face in the lower foreground"
)
if any(term in context for term in ("penis licking", "penis-licking", "tongue along", "tongue licking")):
return outercourse_sentence(
"The woman bends forward between the viewer's open thighs, head low under the viewer's penis with her face directly under the penis; "
"her tongue runs along the underside from the penis shaft to the glans while one hand steadies the base of the penis in the lower foreground"
)
if any(term in context for term in ("handjob", "hand job", "hand wrapped", "hand stroking", "manual stimulation")):
return outercourse_sentence(
"The woman kneels between the viewer's open thighs with her torso leaning forward and face visible behind the penis shaft; "
"one hand wraps around the penis shaft in the lower foreground while the other hand steadies the base of the penis as she strokes toward the glans"
)
if any(term in context for term in ("footjob", "soles", "toes curled", "feet stroking")):
return outercourse_sentence(
"The woman faces the viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera; "
"her soles wrap around the penis shaft in the lower foreground, toes curled around the penis shaft with her face visible beyond her feet"
)
return outercourse_sentence(
"The woman stays close to the viewer's pelvis, keeping the non-penetrative contact centered in the lower foreground with her face visible behind the contact"
)
penetrative_tokens = (
"penetrat",
"thrust",
"anal",
"cowgirl",
"missionary",
"doggy",
"rear-entry",
"spooning",
"side-lying",
"bent-over",
"face-down",
"ejaculat",
"semen",
"cumshot",
"climax",
)
if not any(token in context or token in action_lower for token in penetrative_tokens):
return ""
oral_only = any(token in context for token in ("oral", "blowjob", "cunnilingus", "mouth on", "penis in her mouth"))
if oral_only and not any(token in context for token in ("penetrat", "thrust", "anal", "ejaculat", "semen", "cumshot", "climax")):
return ""
contact = _pov_contact_clause(action, role_graph, hard_item, axis_values, context)
if "reverse cowgirl" in context:
return sentence(
"POV reverse cowgirl position: the viewer lies on his back while the woman straddles his hips facing away; "
f"her back, ass, thighs, and the viewer's foreground legs are visible {contact}"
)
if "cowgirl" in context or "straddling a partner" in context or "squatting on top" in context:
return sentence(
"POV cowgirl position: the viewer lies on his back while the woman straddles his hips facing him; "
f"her torso, hips, and open thighs fill the frame from below {contact}"
)
if "lotus" in context or "seated in a partner's lap" in context:
return sentence(
"POV lotus position: the viewer sits upright while the woman sits in his lap facing him with her legs around his hips; "
f"her torso and hips stay close to the viewer {contact}"
)
if "kneeling straddle" in context:
return sentence(
"POV kneeling straddle position: the viewer kneels upright while the woman straddles his hips facing him; "
f"both torsos are upright and her hips press directly against him {contact}"
)
if "face-down" in context or "face down" in context:
return sentence(
"The woman is seen from behind with her ass raised toward the POV viewer, lying face-down with hips lifted; "
f"the viewer looks down at her raised ass with foreground hands on her hips {contact}"
)
if "bent-over" in context or "bent over" in context or "bent forward" in context:
return sentence(
"The woman is seen from behind with her ass raised toward the POV viewer, bent forward at the waist with hips lifted and head turned back; "
f"the viewer looks down at her raised ass from behind with foreground hands near her hips {contact}"
)
if "doggy" in context or "all fours" in context or "rear-entry" in context:
return sentence(
"The woman is seen from behind with her ass raised toward the POV viewer, on all fours directly in front of him with hips high and back arched; "
f"the viewer looks down at her raised ass with his hands on her hips in the foreground {contact}"
)
if "standing" in context:
return sentence(
"POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; "
f"the viewer stands behind her at hip level {contact}"
)
if "spooning" in context or "side-lying" in context or "lies on her side" in context:
return sentence(
"POV side-lying sex position: the woman lies on her side in front of the viewer with thighs parted; "
f"the viewer is behind her along the same body line {contact}"
)
if (
"edge-supported" in context
or "raised edge" in context
or "edge of bed" in context
or "bed edge" in context
or "kneels between her legs" in context
):
return sentence(
"POV raised-edge penetration position: the woman reclines at the raised edge with thighs open toward the viewer; "
f"the viewer kneels between her legs with his hands near her hips {contact}"
)
if "missionary" in context or ("lies on her back" in context and ("legs open" in context or "thighs open" in context)):
return sentence(
"POV missionary position: the woman lies on her back with legs open around the viewer's hips; "
f"the viewer is above her with foreground arms braced beside her body {contact}"
)
return sentence(
"POV penetrative sex position: the woman is directly in front of the viewer with legs open around his hips; "
f"the viewer's foreground hands and body position define the first-person angle {contact}"
)
def _pov_action_phrase(
action: Any,
pov_labels: list[str],
role_graph: Any = "",
hard_item: Any = "",
composition: Any = "",
axis_values: Any = None,
detail_density: str = "balanced",
) -> str:
rendered = _clean(action)
if not rendered or not pov_labels:
return rendered
if "Man A" in pov_labels:
pov_sentence = _pov_hardcore_pose_sentence(
rendered,
role_graph,
hard_item,
composition,
axis_values,
detail_density,
)
if pov_sentence:
return pov_sentence
for label in sorted(pov_labels, key=len, reverse=True):
escaped = re.escape(label)
rendered = re.sub(rf"\b{escaped}'s\b", "the viewer's", rendered)
rendered = re.sub(rf"\b{escaped}\b", "the viewer", rendered)
if "Man A" in pov_labels:
rendered = re.sub(r"\bthe man's\b", "the viewer's", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bthe man\b", "the viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhe\b", "the viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhim\b", "the viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhis\b", "the viewer's", rendered, flags=re.IGNORECASE)
rendered = re.sub(
r"\bthe viewer lies on the viewer's back under her\b",
"the viewer reclines underneath her",
rendered,
flags=re.IGNORECASE,
)
rendered = re.sub(
r"\bthe viewer lies on the viewer's back\b",
"the viewer reclines",
rendered,
flags=re.IGNORECASE,
)
rendered = re.sub(r"\bthe viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
return rendered
def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str: def _sanitize_scene_text_for_cast(text: Any, labels: list[str]) -> str:
text = _clean(text) text = _clean(text)
if not text: if not text:
@@ -1307,22 +1022,6 @@ def _dedupe_penetration_detail(detail: str, role_graph: str, hard_item: str = ""
return _join_detail_clauses(clauses) return _join_detail_clauses(clauses)
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: def _action_position_phrase(action: str) -> str:
action = _clean(action).lower() action = _clean(action).lower()
if _is_close_foreplay_text(action): if _is_close_foreplay_text(action):
@@ -1421,20 +1120,6 @@ def _climax_clause_duplicates_role(clause: str, role_graph: str) -> bool:
return False 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 _climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) -> str: def _climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None) -> str:
role_graph = _clean(role_graph).rstrip(".") 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) text = " ".join(part.lower() for part in (role_graph, _clean(hard_item), _axis_values_text(axis_values)) if part)
+328
View File
@@ -0,0 +1,328 @@
from __future__ import annotations
import re
from typing import Any
try:
from .krea_action_context import (
axis_values_text,
is_climax_text,
is_outercourse_text,
is_toy_assisted_double_text,
position_context_text,
)
from .krea_detail import limit_detail_for_density
except ImportError: # Allows local smoke tests with `python -c`.
from krea_action_context import (
axis_values_text,
is_climax_text,
is_outercourse_text,
is_toy_assisted_double_text,
position_context_text,
)
from krea_detail import limit_detail_for_density
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_ejaculation_target(context: str) -> str:
if any(token in context for token in ("face", "mouth", "lips", "tongue", "chin")):
return "onto her face and chest"
if any(token in context for token in ("lower back", "ass", "rear-entry", "face-down", "bent-over", "doggy")):
return "across her ass, thighs, and lower back"
if any(token in context for token in ("pussy", "open thighs", "thighs", "legs open")):
return "across her pussy and thighs"
return "onto her body"
def pov_contact_clause(
action: Any,
role_graph: Any,
hard_item: Any,
axis_values: Any,
context: str,
) -> str:
is_climax = is_climax_text(action, role_graph, hard_item, axis_values_text(axis_values))
if is_climax:
return f"as he ejaculates semen {pov_ejaculation_target(context)}"
is_anal = any(
token in context
for token in (
"anal",
"into her ass",
"penis entering ass",
"ass stretched",
"thrusts into her ass",
)
)
contact = "as his penis penetrates her ass" if is_anal else "as his penis penetrates her pussy"
if is_toy_assisted_double_text(action, role_graph, hard_item, axis_values_text(axis_values)):
contact = f"{contact} while a toy is positioned at the second penetration point"
return contact
def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
detail = _clean(detail).strip(" .;")
if not detail:
return ""
detail = re.sub(r"\bthe POV viewer\b", "the viewer", detail, flags=re.IGNORECASE)
detail = re.sub(r"\bthe man's\b", "the viewer's", detail, flags=re.IGNORECASE)
detail = re.sub(r"\bthe man\b", "the viewer", detail, flags=re.IGNORECASE)
detail = re.sub(
r"^(?:missionary|cowgirl|reverse cowgirl|doggy style|standing sex|spooning sex|edge-supported|edge-of-bed|raised edge|kneeling straddle|lotus sex|bent-over|face-down ass-up|side-lying|kneeling rear-entry)\s+(?:position|pose)\s+(?:featuring|with|while|,)?\s*",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(r"^(?:featuring|with)\s+", "", detail, flags=re.IGNORECASE)
detail = re.sub(
r"^(?:full-body|explicit|close-contact|deep|hardcore|vaginal|anal)?\s*(?:penetrative sex|vaginal sex|anal sex|penetration with visible genital contact|hardcore vaginal thrusting|hardcore anal thrusting),?\s*",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(
r"\b(?:front-facing|close-up|wide full-body|wide|overhead|mirror-reflected|low-angle|side-profile|bed-level)\s+view of\b",
"visible",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(
r",?\s*\bthe viewer is behind her at hip level with (?:his|the viewer's) hands on her hips in the foreground as (?:his|the viewer's) penis (?:thrusts into her|penetrates her pussy)\b",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(
r",?\s*\bthe woman is on all fours directly in front of the viewer with hips raised and back arched\b",
"",
detail,
flags=re.IGNORECASE,
)
if any(token in context for token in ("ass raised", "on all fours", "doggy", "rear-entry", "bent-over", "face-down")):
detail = re.sub(
r",?\s*\b(?:one body pinned under another|bodies stacked close together|bodies tangled on the sheets)\b",
"",
detail,
flags=re.IGNORECASE,
)
if "toy is positioned at the second penetration point" in context:
detail = re.sub(
r",?\s*\b(?:toy aligned for a second penetration point|toy-assisted second contact aligned behind the body)\b",
"",
detail,
flags=re.IGNORECASE,
)
detail = re.sub(r"\bwith with\b", "with", detail, flags=re.IGNORECASE)
detail = re.sub(r"\s*,\s*", ", ", detail)
detail = re.sub(r",\s*,", ",", detail).strip(" ,;")
return limit_detail_for_density(detail, detail_density, is_climax_text(context, detail))
def pov_hardcore_pose_sentence(
action: Any,
role_graph: Any,
hard_item: Any,
composition: Any = "",
axis_values: Any = None,
detail_density: str = "balanced",
) -> str:
context = position_context_text(role_graph, hard_item, composition, axis_values)
action_text = _clean(action)
action_lower = action_text.lower()
if not context:
context = action_lower
def sentence(base: str) -> str:
details = ""
if ";" in action_text:
details = pov_clean_detail(action_text.split(";", 1)[1], f"{context} {base}", detail_density)
return f"{base}; {details}" if details else base
def outercourse_sentence(base: str) -> str:
return _clean(base).rstrip(".")
if (
"face-sitting" in context
or "face sitting" in context
or ("straddles" in context and "face" in context and "pussy" in context)
):
return outercourse_sentence(
"The woman is above the camera in a close first-person underview, straddling the viewer's face with her thighs on both sides of his head; "
"her pussy is directly over the viewer's mouth in the lower foreground, tongue contact visible from below"
)
if is_outercourse_text(context, action_lower):
if any(term in context for term in ("boobjob", "titjob", "breast sex", "breast-sex")):
return outercourse_sentence(
"The woman kneels between the viewer's open thighs with her torso bent forward over his pelvis and shoulders low; "
"both hands lift and press her breasts tightly around the viewer's penis shaft in the lower foreground, with the glans just below her lips"
)
if any(term in context for term in ("testicle", "balls licking", "balls-licking", "balls and mouth")):
return outercourse_sentence(
"The woman kneels very low between the viewer's open thighs with her torso bent forward and shoulders between his knees; "
"her head is tucked under the penis shaft at the base of the penis, mouth and tongue licking the viewer's balls while his penis points upward above her face in the lower foreground"
)
if any(term in context for term in ("penis licking", "penis-licking", "tongue along", "tongue licking")):
return outercourse_sentence(
"The woman bends forward between the viewer's open thighs, head low under the viewer's penis with her face directly under the penis; "
"her tongue runs along the underside from the penis shaft to the glans while one hand steadies the base of the penis in the lower foreground"
)
if any(term in context for term in ("handjob", "hand job", "hand wrapped", "hand stroking", "manual stimulation")):
return outercourse_sentence(
"The woman kneels between the viewer's open thighs with her torso leaning forward and face visible behind the penis shaft; "
"one hand wraps around the penis shaft in the lower foreground while the other hand steadies the base of the penis as she strokes toward the glans"
)
if any(term in context for term in ("footjob", "soles", "toes curled", "feet stroking")):
return outercourse_sentence(
"The woman faces the viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera; "
"her soles wrap around the penis shaft in the lower foreground, toes curled around the penis shaft with her face visible beyond her feet"
)
return outercourse_sentence(
"The woman stays close to the viewer's pelvis, keeping the non-penetrative contact centered in the lower foreground with her face visible behind the contact"
)
penetrative_tokens = (
"penetrat",
"thrust",
"anal",
"cowgirl",
"missionary",
"doggy",
"rear-entry",
"spooning",
"side-lying",
"bent-over",
"face-down",
"ejaculat",
"semen",
"cumshot",
"climax",
)
if not any(token in context or token in action_lower for token in penetrative_tokens):
return ""
oral_only = any(token in context for token in ("oral", "blowjob", "cunnilingus", "mouth on", "penis in her mouth"))
if oral_only and not any(token in context for token in ("penetrat", "thrust", "anal", "ejaculat", "semen", "cumshot", "climax")):
return ""
contact = pov_contact_clause(action, role_graph, hard_item, axis_values, context)
if "reverse cowgirl" in context:
return sentence(
"POV reverse cowgirl position: the viewer lies on his back while the woman straddles his hips facing away; "
f"her back, ass, thighs, and the viewer's foreground legs are visible {contact}"
)
if "cowgirl" in context or "straddling a partner" in context or "squatting on top" in context:
return sentence(
"POV cowgirl position: the viewer lies on his back while the woman straddles his hips facing him; "
f"her torso, hips, and open thighs fill the frame from below {contact}"
)
if "lotus" in context or "seated in a partner's lap" in context:
return sentence(
"POV lotus position: the viewer sits upright while the woman sits in his lap facing him with her legs around his hips; "
f"her torso and hips stay close to the viewer {contact}"
)
if "kneeling straddle" in context:
return sentence(
"POV kneeling straddle position: the viewer kneels upright while the woman straddles his hips facing him; "
f"both torsos are upright and her hips press directly against him {contact}"
)
if "face-down" in context or "face down" in context:
return sentence(
"The woman is seen from behind with her ass raised toward the POV viewer, lying face-down with hips lifted; "
f"the viewer looks down at her raised ass with foreground hands on her hips {contact}"
)
if "bent-over" in context or "bent over" in context or "bent forward" in context:
return sentence(
"The woman is seen from behind with her ass raised toward the POV viewer, bent forward at the waist with hips lifted and head turned back; "
f"the viewer looks down at her raised ass from behind with foreground hands near her hips {contact}"
)
if "doggy" in context or "all fours" in context or "rear-entry" in context:
return sentence(
"The woman is seen from behind with her ass raised toward the POV viewer, on all fours directly in front of him with hips high and back arched; "
f"the viewer looks down at her raised ass with his hands on her hips in the foreground {contact}"
)
if "standing" in context:
return sentence(
"POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; "
f"the viewer stands behind her at hip level {contact}"
)
if "spooning" in context or "side-lying" in context or "lies on her side" in context:
return sentence(
"POV side-lying sex position: the woman lies on her side in front of the viewer with thighs parted; "
f"the viewer is behind her along the same body line {contact}"
)
if (
"edge-supported" in context
or "raised edge" in context
or "edge of bed" in context
or "bed edge" in context
or "kneels between her legs" in context
):
return sentence(
"POV raised-edge penetration position: the woman reclines at the raised edge with thighs open toward the viewer; "
f"the viewer kneels between her legs with his hands near her hips {contact}"
)
if "missionary" in context or ("lies on her back" in context and ("legs open" in context or "thighs open" in context)):
return sentence(
"POV missionary position: the woman lies on her back with legs open around the viewer's hips; "
f"the viewer is above her with foreground arms braced beside her body {contact}"
)
return sentence(
"POV penetrative sex position: the woman is directly in front of the viewer with legs open around his hips; "
f"the viewer's foreground hands and body position define the first-person angle {contact}"
)
def pov_action_phrase(
action: Any,
pov_labels: list[str],
role_graph: Any = "",
hard_item: Any = "",
composition: Any = "",
axis_values: Any = None,
detail_density: str = "balanced",
) -> str:
rendered = _clean(action)
if not rendered or not pov_labels:
return rendered
if "Man A" in pov_labels:
pov_sentence = pov_hardcore_pose_sentence(
rendered,
role_graph,
hard_item,
composition,
axis_values,
detail_density,
)
if pov_sentence:
return pov_sentence
for label in sorted(pov_labels, key=len, reverse=True):
escaped = re.escape(label)
rendered = re.sub(rf"\b{escaped}'s\b", "the viewer's", rendered)
rendered = re.sub(rf"\b{escaped}\b", "the viewer", rendered)
if "Man A" in pov_labels:
rendered = re.sub(r"\bthe man's\b", "the viewer's", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bthe man\b", "the viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhe\b", "the viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhim\b", "the viewer", rendered, flags=re.IGNORECASE)
rendered = re.sub(r"\bhis\b", "the viewer's", rendered, flags=re.IGNORECASE)
rendered = re.sub(
r"\bthe viewer lies on the viewer's back under her\b",
"the viewer reclines underneath her",
rendered,
flags=re.IGNORECASE,
)
rendered = re.sub(
r"\bthe viewer lies on the viewer's back\b",
"the viewer reclines",
rendered,
flags=re.IGNORECASE,
)
rendered = re.sub(r"\bthe viewer is positioned\b", "the POV camera is positioned", rendered, flags=re.IGNORECASE)
return rendered