Extract interaction role graph wording

This commit is contained in:
2026-06-26 17:45:28 +02:00
parent 3ebbb09d63
commit dcddfe5d61
5 changed files with 274 additions and 111 deletions
@@ -102,6 +102,9 @@ Already isolated:
- hardcore configured-cast role graph generation lives in - hardcore configured-cast role graph generation lives in
`hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and `hardcore_role_graphs.py`; `prompt_builder.py` selects item/axis metadata and
then asks that module for the source role graph. then asks that module for the source role graph.
- interaction-style role graph wording lives in `hardcore_role_interaction.py`,
covering foreplay, manual stimulation, body worship, clothing transitions,
dominant guidance, camera performance, aftercare, and group coordination.
- outercourse-specific role graph wording has started moving into action-family - outercourse-specific role graph wording has started moving into action-family
modules; `hardcore_role_outercourse.py` owns boobjob, testicle-sucking, modules; `hardcore_role_outercourse.py` owns boobjob, testicle-sucking,
penis-licking, handjob, and footjob body geometry. penis-licking, handjob, and footjob body geometry.
@@ -318,6 +321,8 @@ Near-term:
Krea routes so selected position geometry stays synchronized with metadata. Krea routes so selected position geometry stays synchronized with metadata.
- Cover generated climax routes through Krea, SDXL, and natural caption outputs - Cover generated climax routes through Krea, SDXL, and natural caption outputs
so source aftermath placement and formatter details cannot drift apart. so source aftermath placement and formatter details cannot drift apart.
- Cover generated interaction routes through Krea, SDXL, and natural caption
outputs so source contact/guidance/presentation wording stays metadata-driven.
Medium-term: Medium-term:
+4
View File
@@ -65,6 +65,7 @@ Core helper ownership:
| Python module | What it owns | | Python module | What it owns |
| --- | --- | | --- | --- |
| `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. | | `hardcore_role_graphs.py` | Source role graph construction for hardcore configured-cast rows, including POV-aware interaction geometry. |
| `hardcore_role_interaction.py` | Foreplay, manual stimulation, body worship, clothing transition, dominant guidance, camera performance, aftercare, and group coordination role graph wording. |
| `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral geometry. | | `hardcore_role_oral.py` | Oral-sex role graph wording for kneeling, face-sitting, sixty-nine, edge-supported, side-lying, chair, standing, and reclining oral geometry. |
| `hardcore_role_outercourse.py` | Outercourse role graph wording for boobjob, testicle-sucking, penis-licking, handjob, and footjob geometry. | | `hardcore_role_outercourse.py` | Outercourse role graph wording for boobjob, testicle-sucking, penis-licking, handjob, and footjob geometry. |
| `hardcore_role_penetration.py` | Penetrative-sex role graph wording for missionary, cowgirl, reverse-cowgirl, doggy, standing, side-lying, raised-edge, kneeling-straddle, and lotus geometry. | | `hardcore_role_penetration.py` | Penetrative-sex role graph wording for missionary, cowgirl, reverse-cowgirl, doggy, standing, side-lying, raised-edge, kneeling-straddle, and lotus geometry. |
@@ -752,6 +753,9 @@ pair metadata through the core Python APIs, then verifies:
- climax routes keep source body position, Krea aftermath wording, SDXL family - climax routes keep source body position, Krea aftermath wording, SDXL family
tags, and training captions synchronized for face-down, side-lying, lap, tags, and training captions synchronized for face-down, side-lying, lap,
open-thigh, and front/back placements; open-thigh, and front/back placements;
- interaction routes keep source role graphs, Krea prose, SDXL tags, and
training captions synchronized for manual, clothing transition, body worship,
camera-performance, aftercare, and group-coordination rows;
- 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
+29 -111
View File
@@ -6,12 +6,24 @@ from typing import Any
try: try:
from .hardcore_role_anal import build_anal_or_double_role_graph from .hardcore_role_anal import build_anal_or_double_role_graph
from .hardcore_role_climax import build_climax_role_graph from .hardcore_role_climax import build_climax_role_graph
from .hardcore_role_interaction import (
build_foreplay_role_graph,
build_group_coordination_role_graph,
build_interaction_role_graph,
build_manual_role_graph,
)
from .hardcore_role_oral import build_oral_role_graph from .hardcore_role_oral import build_oral_role_graph
from .hardcore_role_outercourse import build_outercourse_role_graph from .hardcore_role_outercourse import build_outercourse_role_graph
from .hardcore_role_penetration import build_penetration_role_graph from .hardcore_role_penetration import build_penetration_role_graph
except ImportError: # Allows local smoke tests with `python -c`. except ImportError: # Allows local smoke tests with `python -c`.
from hardcore_role_anal import build_anal_or_double_role_graph from hardcore_role_anal import build_anal_or_double_role_graph
from hardcore_role_climax import build_climax_role_graph from hardcore_role_climax import build_climax_role_graph
from hardcore_role_interaction import (
build_foreplay_role_graph,
build_group_coordination_role_graph,
build_interaction_role_graph,
build_manual_role_graph,
)
from hardcore_role_oral import build_oral_role_graph from hardcore_role_oral import build_oral_role_graph
from hardcore_role_outercourse import build_outercourse_role_graph from hardcore_role_outercourse import build_outercourse_role_graph
from hardcore_role_penetration import build_penetration_role_graph from hardcore_role_penetration import build_penetration_role_graph
@@ -91,112 +103,11 @@ def build_hardcore_role_graph(
] ]
return f" {extra} {rng.choice(actions)}." return f" {extra} {rng.choice(actions)}."
def foreplay_position_graph(primary: str, partner: str) -> str:
text = " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
if any(term in text for term in ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning")):
return (
f"{primary} and {partner} stand close while {partner}'s hands pull clothing aside from {primary}'s body; "
f"{primary}'s exposed skin and the clothing being removed stay clearly visible."
)
if any(term in text for term in ("breast", "breasts", "nipple", "cupping breasts", "touching breasts")):
return (
f"{primary} and {partner} press their bodies close while {partner}'s hand cups {primary}'s breast; "
f"their faces stay close and the breast-touching gesture is clear."
)
if any(term in text for term in ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin")):
return (
f"{primary} and {partner} stand face-to-face at close range while one hand holds {primary}'s cheek and jaw; "
f"their lips are close and the face-touching gesture is clear."
)
if any(term in text for term in ("kiss", "kissing", "mouth-to-mouth", "lips pressed")):
return (
f"{primary} and {partner} press their bodies together and kiss deeply, "
f"with hands on each other's face, waist, and hips."
)
return (
f"{primary} and {partner} are pressed close in a heated foreplay setup, "
f"hands caressing skin while clothing is pulled aside."
)
def interaction_text() -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
def manual_position_graph(primary: str, partner: str = "") -> str:
text = interaction_text()
if not partner:
if "mutual" in text:
return f"{primary} faces the camera with thighs open, both hands on her body for solo mutual-style masturbation framing."
return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy."
if "mutual" in text:
return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible."
if "clit" in text or "clitoris" in text:
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers rubbing her clit as her hips tilt toward the touch."
if "toy" in text or "vibrator" in text:
return f"{primary} reclines with thighs open while {partner} holds a vibrator or toy against her clit, one hand keeping her thigh open."
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers visibly stimulating her pussy."
def interaction_position_graph(primary: str, partner: str, third: str = "") -> str:
text = interaction_text()
if "aftercare" in slug or any(term in text for term in ("aftercare", "cleanup", "wiping", "towel", "post-sex", "cuddle")):
if "cleanup" in text or "wiping" in text or "towel" in text:
return f"{primary} reclines after sex while {partner} kneels close and wipes her skin with a towel, hands and relaxed body contact visible."
return f"{primary} and {partner} lie close together after sex, bodies relaxed and hands resting on skin in a post-sex cuddle."
if "camera_performance" in slug or any(term in text for term in ("camera", "presenting", "showing", "viewer", "creator-shot")):
if third:
return f"{primary} faces the camera while {partner} and {third} hold and present her body, hands framing the exposed skin for the viewer."
return f"{primary} faces the camera and presents her body while {partner}'s hands hold her hips or thighs open for a clear creator-shot reveal."
if "body_worship" in slug or any(term in text for term in ("body worship", "nipple", "thigh", "mouth on skin", "kissing down", "ass grabbing")):
if "ass" in text:
return f"{primary} stands or kneels with hips angled back while {partner}'s hands grip her ass, fingers pressing into skin."
if "thigh" in text:
return f"{primary} reclines with thighs open while {partner} kneels close and kisses along her inner thighs, hands holding her legs in place."
if "nipple" in text or "breast" in text:
return f"{primary} arches toward {partner} while {partner}'s mouth is on her breast and one hand cups or squeezes the other breast."
return f"{primary} reclines or leans back while {partner} kisses down her body, hands tracing breasts, waist, hips, and thighs."
if "clothing_position" in slug or any(term in text for term in ("transition", "turning", "pulling onto", "lifting", "guided backward", "clothing", "garment")):
if "turn" in text or "rear-facing" in text:
return f"{partner}'s hands turn {primary} around by the hips, clothing partly moved aside as her body rotates into the next pose."
if "legs" in text or "thigh" in text:
return f"{primary} lies back while {partner} lifts and spreads her legs into position, hands and clothing movement clearly visible."
return f"{primary} and {partner} are mid-transition, with {partner}'s hands moving clothing aside and guiding {primary}'s hips toward the next pose."
if "dominant" in slug or any(term in text for term in ("hair", "wrist", "wrists", "jaw", "chin", "guided", "dominant", "control", "dirty talk", "whisper", "mouth near the ear", "verbal teasing")):
if "dirty talk" in text or "whisper" in text or "mouth near the ear" in text or "verbal teasing" in text:
return f"{partner} leans close to {primary}'s ear for dirty talk while holding her waist and keeping their bodies pressed close."
if "wrist" in text or "wrists" in text:
return f"{primary} lies back while {partner} pins her wrists above her head, both bodies close and the consensual control gesture clearly visible."
if "hair" in text:
return f"{partner} holds {primary}'s hair back while guiding her body closer, face and hair-hold gesture visible."
if "thigh" in text or "spread" in text:
return f"{primary} reclines with thighs open while {partner}'s hands spread her legs and hold the position for the camera."
return f"{partner} guides {primary}'s body with hands on her jaw, waist, and hips, keeping the consensual control gesture readable."
return foreplay_position_graph(primary, partner)
def group_coordination_graph(primary: str, partner: str, third: str) -> str:
observer = third or any_person({primary, partner})
text = interaction_text()
if "camera" in text or "hold" in text or "present" in text:
return f"{primary} is centered while {partner} and {observer} hold and present the body for the camera, each role clearly visible."
if "watch" in text or "waiting" in text:
return f"{primary} is centered while {partner} touches her body and {observer} watches close beside them, hands and faces readable."
return f"{primary} is centered while {partner} touches her body and {observer} stays close as the watching or guiding partner."
if people_count == 1: if people_count == 1:
solo = people[0] solo = people[0]
if women_count == 1: if women_count == 1:
if "manual_stimulation" in slug: if "manual_stimulation" in slug:
return manual_position_graph(solo) return build_manual_role_graph(solo, item_text=item_text, item_axis_values=item_axis_values)
if "camera_performance" in slug: if "camera_performance" in slug:
return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose." return f"{solo} faces the camera and presents her body with hands framing the exposed skin in a solo creator-shot pose."
if "cumshot" in slug or "climax" in slug: if "cumshot" in slug or "climax" in slug:
@@ -211,16 +122,16 @@ def build_hardcore_role_graph(
c = any_woman({a, b}) if len(women) >= 3 else "" c = any_woman({a, b}) if len(women) >= 3 else ""
used = {a, b} used = {a, b}
if "manual_stimulation" in slug: if "manual_stimulation" in slug:
graph = manual_position_graph(a, b) graph = build_manual_role_graph(a, b, item_text, item_axis_values)
elif "group_coordination" in slug and c: elif "group_coordination" in slug and c:
graph = group_coordination_graph(a, b, c) graph = build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values)
used.add(c) used.add(c)
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
graph = interaction_position_graph(a, b, c) graph = build_interaction_role_graph(a, b, c, slug, item_text, item_axis_values)
if c and "camera_performance" in slug: if c and "camera_performance" in slug:
used.add(c) used.add(c)
elif "foreplay" in slug: elif "foreplay" in slug:
graph = foreplay_position_graph(a, b) graph = build_foreplay_role_graph(a, b, item_text, item_axis_values)
elif "outercourse" in slug: elif "outercourse" in slug:
graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact." graph = f"{a} kneels close to {b}'s body and uses mouth, hands, breasts, or feet for explicit non-penetrative contact."
elif "oral" in slug: elif "oral" in slug:
@@ -244,7 +155,7 @@ def build_hardcore_role_graph(
if "manual_stimulation" in slug: if "manual_stimulation" in slug:
graph = f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup." graph = f"{a} and {b} sit or recline close together with hands visibly stimulating bodies in a manual sex setup."
elif "group_coordination" in slug and c: elif "group_coordination" in slug and c:
graph = group_coordination_graph(a, b, c) graph = build_group_coordination_role_graph(a, b, c, item_text=item_text, item_axis_values=item_axis_values)
used.add(c) used.add(c)
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside." graph = f"{a} and {b} press close together, kissing and caressing skin while clothing is pulled aside."
@@ -270,13 +181,20 @@ def build_hardcore_role_graph(
man = any_man() man = any_man()
third = any_person({woman, man}) if people_count >= 3 else "" third = any_person({woman, man}) if people_count >= 3 else ""
if "manual_stimulation" in slug: if "manual_stimulation" in slug:
graph = manual_position_graph(woman, man) graph = build_manual_role_graph(woman, man, item_text, item_axis_values)
elif "group_coordination" in slug: elif "group_coordination" in slug:
graph = group_coordination_graph(woman, man, third) graph = build_group_coordination_role_graph(
woman,
man,
third,
any_person({woman, man}) if not third else "",
item_text,
item_axis_values,
)
elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")): elif any(token in slug for token in ("foreplay", "body_worship", "clothing_position", "dominant_guidance", "camera_performance", "aftercare")):
graph = interaction_position_graph(woman, man, third) graph = build_interaction_role_graph(woman, man, third, slug, item_text, item_axis_values)
elif "foreplay" in slug: elif "foreplay" in slug:
graph = foreplay_position_graph(woman, man) graph = build_foreplay_role_graph(woman, man, item_text, item_axis_values)
elif "outercourse" in slug: elif "outercourse" in slug:
graph = build_outercourse_role_graph(woman, man, item_text, item_axis_values, pov_labels) graph = build_outercourse_role_graph(woman, man, item_text, item_axis_values, pov_labels)
elif "oral" in slug: elif "oral" in slug:
+127
View File
@@ -0,0 +1,127 @@
from __future__ import annotations
from typing import Any
def _context_text(item_text: str, item_axis_values: dict[str, Any] | None) -> str:
return " ".join(
str(part or "").lower()
for part in (
item_text,
*((item_axis_values or {}).values()),
)
)
def build_foreplay_role_graph(
primary: str,
partner: str,
item_text: str,
item_axis_values: dict[str, Any] | None = None,
) -> str:
text = _context_text(item_text, item_axis_values)
if any(term in text for term in ("undressing", "removing clothing", "removing clothes", "pulling clothing", "sliding straps", "unbuttoning")):
return (
f"{primary} and {partner} stand close while {partner}'s hands pull clothing aside from {primary}'s body; "
f"{primary}'s exposed skin and the clothing being removed stay clearly visible."
)
if any(term in text for term in ("breast", "breasts", "nipple", "cupping breasts", "touching breasts")):
return (
f"{primary} and {partner} press their bodies close while {partner}'s hand cups {primary}'s breast; "
f"their faces stay close and the breast-touching gesture is clear."
)
if any(term in text for term in ("face", "cheek", "jaw", "chin", "hand on the cheek", "fingers under the chin")):
return (
f"{primary} and {partner} stand face-to-face at close range while one hand holds {primary}'s cheek and jaw; "
f"their lips are close and the face-touching gesture is clear."
)
if any(term in text for term in ("kiss", "kissing", "mouth-to-mouth", "lips pressed")):
return (
f"{primary} and {partner} press their bodies together and kiss deeply, "
f"with hands on each other's face, waist, and hips."
)
return (
f"{primary} and {partner} are pressed close in a heated foreplay setup, "
f"hands caressing skin while clothing is pulled aside."
)
def build_manual_role_graph(
primary: str,
partner: str = "",
item_text: str = "",
item_axis_values: dict[str, Any] | None = None,
) -> str:
text = _context_text(item_text, item_axis_values)
if not partner:
if "mutual" in text:
return f"{primary} faces the camera with thighs open, both hands on her body for solo mutual-style masturbation framing."
return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy."
if "mutual" in text:
return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible."
if "clit" in text or "clitoris" in text:
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers rubbing her clit as her hips tilt toward the touch."
if "toy" in text or "vibrator" in text:
return f"{primary} reclines with thighs open while {partner} holds a vibrator or toy against her clit, one hand keeping her thigh open."
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers visibly stimulating her pussy."
def build_interaction_role_graph(
primary: str,
partner: str,
third: str = "",
slug: str = "",
item_text: str = "",
item_axis_values: dict[str, Any] | None = None,
) -> str:
text = _context_text(item_text, item_axis_values)
if "aftercare" in slug or any(term in text for term in ("aftercare", "cleanup", "wiping", "towel", "post-sex", "cuddle")):
if "cleanup" in text or "wiping" in text or "towel" in text:
return f"{primary} reclines after sex while {partner} kneels close and wipes her skin with a towel, hands and relaxed body contact visible."
return f"{primary} and {partner} lie close together after sex, bodies relaxed and hands resting on skin in a post-sex cuddle."
if "camera_performance" in slug or any(term in text for term in ("camera", "presenting", "showing", "viewer", "creator-shot")):
if third:
return f"{primary} faces the camera while {partner} and {third} hold and present her body, hands framing the exposed skin for the viewer."
return f"{primary} faces the camera and presents her body while {partner}'s hands hold her hips or thighs open for a clear creator-shot reveal."
if "body_worship" in slug or any(term in text for term in ("body worship", "nipple", "thigh", "mouth on skin", "kissing down", "ass grabbing")):
if "ass" in text:
return f"{primary} stands or kneels with hips angled back while {partner}'s hands grip her ass, fingers pressing into skin."
if "thigh" in text:
return f"{primary} reclines with thighs open while {partner} kneels close and kisses along her inner thighs, hands holding her legs in place."
if "nipple" in text or "breast" in text:
return f"{primary} arches toward {partner} while {partner}'s mouth is on her breast and one hand cups or squeezes the other breast."
return f"{primary} reclines or leans back while {partner} kisses down her body, hands tracing breasts, waist, hips, and thighs."
if "clothing_position" in slug or any(term in text for term in ("transition", "turning", "pulling onto", "lifting", "guided backward", "clothing", "garment")):
if "turn" in text or "rear-facing" in text:
return f"{partner}'s hands turn {primary} around by the hips, clothing partly moved aside as her body rotates into the next pose."
if "legs" in text or "thigh" in text:
return f"{primary} lies back while {partner} lifts and spreads her legs into position, hands and clothing movement clearly visible."
return f"{primary} and {partner} are mid-transition, with {partner}'s hands moving clothing aside and guiding {primary}'s hips toward the next pose."
if "dominant" in slug or any(term in text for term in ("hair", "wrist", "wrists", "jaw", "chin", "guided", "dominant", "control", "dirty talk", "whisper", "mouth near the ear", "verbal teasing")):
if "dirty talk" in text or "whisper" in text or "mouth near the ear" in text or "verbal teasing" in text:
return f"{partner} leans close to {primary}'s ear for dirty talk while holding her waist and keeping their bodies pressed close."
if "wrist" in text or "wrists" in text:
return f"{primary} lies back while {partner} pins her wrists above her head, both bodies close and the consensual control gesture clearly visible."
if "hair" in text:
return f"{partner} holds {primary}'s hair back while guiding her body closer, face and hair-hold gesture visible."
if "thigh" in text or "spread" in text:
return f"{primary} reclines with thighs open while {partner}'s hands spread her legs and hold the position for the camera."
return f"{partner} guides {primary}'s body with hands on her jaw, waist, and hips, keeping the consensual control gesture readable."
return build_foreplay_role_graph(primary, partner, item_text, item_axis_values)
def build_group_coordination_role_graph(
primary: str,
partner: str,
third: str = "",
fallback_observer: str = "",
item_text: str = "",
item_axis_values: dict[str, Any] | None = None,
) -> str:
observer = third or fallback_observer or partner
text = _context_text(item_text, item_axis_values)
if "camera" in text or "hold" in text or "present" in text:
return f"{primary} is centered while {partner} and {observer} hold and present the body for the camera, each role clearly visible."
if "watch" in text or "waiting" in text:
return f"{primary} is centered while {partner} touches her body and {observer} watches close beside them, hands and faces readable."
return f"{primary} is centered while {partner} touches her body and {observer} stays close as the watching or guiding partner."
+109
View File
@@ -1217,6 +1217,114 @@ def smoke_climax_position_routes() -> None:
_expect_formatter_outputs(row, name, target="single") _expect_formatter_outputs(row, name, target="single")
def smoke_interaction_role_graph_routes() -> None:
cases = [
(
"interaction_manual",
"Manual stimulation",
"manual_only",
"manual",
"fingering",
4301,
_character_cast(),
1,
1,
("reclines with thighs open", "fingers visibly stimulating"),
("fingers visibly stimulating", "between her legs"),
),
(
"interaction_clothing_transition",
"Clothing and position transitions",
"interaction_only",
"interaction",
"position_transition",
4302,
_character_cast(),
1,
1,
("mid-transition", "moving clothing aside"),
("mid-transition", "guiding the woman's hips"),
),
(
"interaction_body_worship",
"Body worship and touching",
"interaction_only",
"interaction",
"body_worship",
4301,
_character_cast(),
1,
1,
("kisses down her body", "hands tracing"),
("kisses down her body", "hands tracing"),
),
(
"interaction_camera_performance",
"Camera performance",
"interaction_only",
"interaction",
"camera_showing",
4301,
_character_cast(),
1,
1,
("faces the camera", "creator-shot reveal"),
("faces the camera", "creator-shot reveal"),
),
(
"interaction_aftercare",
"Aftercare and cleanup",
"interaction_only",
"interaction",
"aftercare",
4301,
_character_cast(),
1,
1,
("lie close together after sex", "post-sex cuddle"),
("lie close together after sex", "post-sex cuddle"),
),
(
"interaction_group_coordination",
"Group coordination",
"interaction_only",
"interaction",
"watching",
4301,
_character_cast_two_men(),
1,
2,
("is centered", "hold and present"),
("is centered", "each role clearly visible"),
),
]
for name, subcategory, focus, family, position_key, seed, cast, women_count, men_count, role_terms, krea_terms in cases:
row = _prompt_row(
name=name,
category="Hardcore sexual poses",
subcategory=subcategory,
seed=seed,
character_cast=cast,
women_count=women_count,
men_count=men_count,
hardcore_position_config=_position_filter(focus, family, [position_key]),
)
_expect_custom_row(row, name)
_expect(row.get("action_family") == "foreplay", f"{name} action_family should stay formatter foreplay")
_expect(row.get("position_family") == family, f"{name} position_family mismatch: {row.get('position_family')}")
_expect(position_key in (row.get("position_keys") or []), f"{name} lost position key {position_key!r}")
role_graph = _expect_text(f"{name}.source_role_graph", 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(row), target="single")
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("role graph:" not in prompt and "sexual scene:" not in prompt, f"{name} Krea leaked raw labels")
for term in krea_terms:
_expect(term in prompt, f"{name} Krea prompt missing {term!r}: {prompt}")
_expect_formatter_outputs(row, name, target="single")
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",
@@ -1354,6 +1462,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
("pov_anal_position_routes", smoke_pov_anal_position_routes), ("pov_anal_position_routes", smoke_pov_anal_position_routes),
("double_front_back_route", smoke_double_front_back_route), ("double_front_back_route", smoke_double_front_back_route),
("climax_position_routes", smoke_climax_position_routes), ("climax_position_routes", smoke_climax_position_routes),
("interaction_role_graph_routes", smoke_interaction_role_graph_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),
] ]