Pin POV outercourse position routing

This commit is contained in:
2026-06-26 17:02:17 +02:00
parent f27ba23a62
commit 0e7cf60fcb
4 changed files with 114 additions and 14 deletions
+2
View File
@@ -730,6 +730,8 @@ pair metadata through the core Python APIs, then verifies:
- Krea POV penetration routes keep first-person position anchors, suppress - Krea POV penetration routes keep first-person position anchors, suppress
normal camera text, and preserve composition punctuation before the style normal camera text, and preserve composition punctuation before the style
suffix; suffix;
- POV outercourse routes keep constrained boobjob, testicle-sucking,
penis-licking, handjob, and footjob geometry through Krea formatting;
- expression-disabled rows do not fall back to generated expression text. - expression-disabled rows do not fall back to generated expression text.
- static formatter metadata fixtures keep source-provided action families - static formatter metadata fixtures keep source-provided action families
stable across Krea2 prose, SDXL tags, and natural captions even when raw item stable across Krea2 prose, SDXL tags, and natural captions even when raw item
+10 -10
View File
@@ -333,16 +333,6 @@ def build_hardcore_role_graph(
f"{woman} bends forward between {man}'s open thighs, head low under {man}'s penis with her face directly under the penis, " f"{woman} bends forward between {man}'s open thighs, head low under {man}'s penis with her face directly under the penis, "
f"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis." f"tongue running along the underside from the penis shaft to the glans while one hand steadies the base of the penis."
) )
if "handjob" in position_text or "handjob" in text or "hand job" in text or "hand wrapped" in text:
if man_is_pov:
return (
f"{woman} kneels between the POV viewer's open thighs with her torso leaning forward and face visible behind the penis shaft, "
"one hand wrapped around the POV viewer's penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
)
return (
f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind the penis shaft, "
f"one hand wrapped around {man}'s penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
)
if "footjob" in text or "soles" in text or "toes curled" in text or "feet stroking" in text: if "footjob" in text or "soles" in text or "toes curled" in text or "feet stroking" in text:
if man_is_pov: if man_is_pov:
return ( return (
@@ -353,6 +343,16 @@ def build_hardcore_role_graph(
f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, " f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, "
f"wrapping both soles around {man}'s penis shaft while the contact stays centered." f"wrapping both soles around {man}'s penis shaft while the contact stays centered."
) )
if "handjob" in position_text or "handjob" in text or "hand job" in text or "hand wrapped" in text:
if man_is_pov:
return (
f"{woman} kneels between the POV viewer's open thighs with her torso leaning forward and face visible behind the penis shaft, "
"one hand wrapped around the POV viewer's penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
)
return (
f"{woman} kneels between {man}'s open thighs with her torso leaning forward and face visible behind the penis shaft, "
f"one hand wrapped around {man}'s penis shaft while the other hand steadies the base of the penis as she strokes toward the glans."
)
if man_is_pov: if man_is_pov:
return ( return (
f"{woman} kneels close to the POV viewer's hips and keeps the POV viewer's penis centered in clear non-penetrative contact, " f"{woman} kneels close to the POV viewer's hips and keeps the POV viewer's penis centered in clear non-penetrative contact, "
+10 -2
View File
@@ -1246,13 +1246,21 @@ def _outercourse_axis_values_for_position(values: list[Any], position: str, axis
return filtered(by_axis.get(axis_name, ("hand", "penis", "shaft"))) return filtered(by_axis.get(axis_name, ("hand", "penis", "shaft")))
if "footjob" in position_text: if "footjob" in position_text:
by_axis = { by_axis = {
"contact_detail": ("soles", "toes", "shaft"), "contact_detail": ("soles", "toes"),
"hand_detail": ("ankles", "thighs"), "hand_detail": ("ankles", "thighs"),
"texture_detail": ("toes", "soles", "pressure"), "texture_detail": ("toes", "soles", "pressure"),
"visibility": ("feet", "soles"), "visibility": ("feet", "soles"),
"body_contact": ("legs", "knees", "body angled"), "body_contact": ("legs", "knees", "body angled"),
} }
return filtered(by_axis.get(axis_name, ("feet", "soles", "toes"))) excluded_by_axis = {
"contact_detail": ("hand", "finger", "palm", "balls", "tongue", "breast"),
"texture_detail": ("fingers", "tongue", "breast"),
"visibility": ("hand", "balls", "breast"),
}
return filtered(
by_axis.get(axis_name, ("feet", "soles", "toes")),
excluded_by_axis.get(axis_name, ()),
)
return values return values
+92 -2
View File
@@ -163,7 +163,7 @@ def _character_cast(*, pov_man: bool = False) -> str:
)["character_cast"] )["character_cast"]
def _action_filter(focus: str) -> str: def _action_filter(focus: str, hardcore_position_config: str | dict[str, Any] | None = "") -> str:
kwargs = { kwargs = {
"allow_toys": False, "allow_toys": False,
"allow_double": False, "allow_double": False,
@@ -176,7 +176,20 @@ def _action_filter(focus: str) -> str:
"allow_anal": focus in ("anal_only", "keep_pool"), "allow_anal": focus in ("anal_only", "keep_pool"),
"allow_climax": focus in ("climax_only", "keep_pool"), "allow_climax": focus in ("climax_only", "keep_pool"),
} }
return pb.build_hardcore_action_filter_json(focus=focus, **kwargs) return pb.build_hardcore_action_filter_json(
hardcore_position_config=hardcore_position_config,
focus=focus,
**kwargs,
)
def _position_filter(focus: str, family: str, positions: list[str] | tuple[str, ...] | str) -> str:
position_config = pb.build_hardcore_position_pool_json(
combine_mode="replace",
family=family,
selected_positions=positions,
)
return _action_filter(focus, position_config)
def _coworking_location_config() -> str: def _coworking_location_config() -> str:
@@ -713,6 +726,82 @@ def smoke_krea_pov_penetration_route() -> None:
_expect("composition. explicit" in lower, "POV penetration composition sentence should keep punctuation before style suffix") _expect("composition. explicit" in lower, "POV penetration composition sentence should keep punctuation before style suffix")
def smoke_pov_outercourse_position_routes() -> None:
cases = [
(
"pov_outercourse_boobjob",
"boobjob",
("breasts tightly around", "glans sits just below"),
("press her breasts tightly around", "glans just below", "penis shaft"),
),
(
"pov_outercourse_testicle",
"testicle_sucking",
("mouth and tongue on the pov viewer's balls", "penis points upward"),
("mouth and tongue licking", "balls", "penis points upward"),
),
(
"pov_outercourse_penis_licking",
"penis_licking",
("head low under the pov viewer's penis", "tongue running along"),
("tongue runs along", "penis shaft", "glans"),
),
(
"pov_outercourse_handjob",
"handjob",
("one hand wrapped around the pov viewer's penis", "strokes toward the glans"),
("one hand wraps around", "penis shaft", "strokes toward the glans"),
),
(
"pov_outercourse_footjob",
"footjob",
("both soles wrapped around the pov viewer's penis", "lower foreground"),
("soles wrap around", "penis shaft", "lower foreground"),
),
]
for offset, (name, position_key, role_terms, krea_terms) in enumerate(cases, start=3601):
pair = pb.build_insta_of_pair(
row_number=1,
start_index=1,
seed=offset,
ethnicity="any",
figure="random",
no_plus_women=False,
no_black=False,
trigger=Trigger,
prepend_trigger_to_prompt=True,
options_json=_insta_options(
softcore_camera_mode="from_camera_config",
hardcore_camera_mode="from_camera_config",
camera_detail="compact",
),
character_cast=_character_cast(pov_man=True),
hardcore_position_config=_position_filter("outercourse_only", "outercourse", [position_key]),
location_config=_coworking_location_config(),
hardcore_camera_config=_orbit_camera(
horizontal_angle=45,
vertical_angle=0,
zoom=7.5,
subject_focus="action",
),
)
_expect_pair(pair, name)
hard_row = pair.get("hardcore_row") or {}
_expect(hard_row.get("action_family") == "outercourse", f"{name} action_family should be outercourse")
_expect(hard_row.get("position_family") == "outercourse", f"{name} position_family should be outercourse")
_expect(position_key in (hard_row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
role_graph = _expect_text(f"{name}.source_role_graph", hard_row.get("source_role_graph"), 40).lower()
for term in role_terms:
_expect(term in role_graph, f"{name} role graph missing {term!r}: {role_graph}")
krea = krea_formatter.format_krea2_prompt("", metadata_json=_json(pair), target="hardcore")
prompt = _expect_text(f"{name}.krea_prompt", krea.get("krea_prompt"), 60).lower()
_expect("metadata" in krea.get("method", ""), f"{name}.krea did not use metadata")
_expect("viewer" in prompt and "first-person" in prompt, f"{name} Krea prompt lost POV wording")
_expect("camera:" not in krea.get("krea_prompt", ""), f"{name} Krea prompt emitted normal third-person camera directive")
for term in krea_terms:
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
def smoke_no_expression_fallback() -> None: def smoke_no_expression_fallback() -> None:
cast = pb.build_character_slot_json( cast = pb.build_character_slot_json(
subject_type="woman", subject_type="woman",
@@ -844,6 +933,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("insta_pair_camera_split", smoke_insta_pair_camera_split), ("insta_pair_camera_split", smoke_insta_pair_camera_split),
("pov_camera_scene", smoke_pov_camera_scene), ("pov_camera_scene", smoke_pov_camera_scene),
("krea_pov_penetration_route", smoke_krea_pov_penetration_route), ("krea_pov_penetration_route", smoke_krea_pov_penetration_route),
("pov_outercourse_position_routes", smoke_pov_outercourse_position_routes),
("expression_disabled", smoke_no_expression_fallback), ("expression_disabled", smoke_no_expression_fallback),
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures), ("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
] ]