Compare commits

...

39 Commits

Author SHA1 Message Date
Ethanfel f5ba07e340 Add Krea2 POV routing and eval tooling 2026-06-30 19:28:10 +02:00
Ethanfel 284c6279e6 Add Krea2 ballsucking route scaffold 2026-06-29 10:20:58 +02:00
Ethanfel 364c42103b Preserve semen wording in Krea2 climax prompts 2026-06-29 09:55:59 +02:00
Ethanfel 49d130467b Show eval template commands in Krea2 report 2026-06-29 09:32:36 +02:00
Ethanfel 6a37c807bc Add Krea2 eval entry templates 2026-06-29 09:20:20 +02:00
Ethanfel 2aafab03bd Add validated Krea2 eval recorder 2026-06-29 09:09:55 +02:00
Ethanfel 1e9794eed0 Mark sixty-nine as low-priority control route 2026-06-29 08:58:26 +02:00
Ethanfel 3467acbd6a Show latest Krea2 evidence in tuning report 2026-06-29 08:45:49 +02:00
Ethanfel b8e15289ca Map Krea2 sixty-nine and refine ready aftermath 2026-06-29 08:34:12 +02:00
Ethanfel 03907439a4 Add Krea2 ready aftermath candidate 2026-06-29 08:01:23 +02:00
Ethanfel e028419e6d Add Krea2 wand atlas candidate 2026-06-29 07:44:03 +02:00
Ethanfel 05f14cecc7 Add Krea2 reverse cowgirl alt candidate 2026-06-29 07:20:41 +02:00
Ethanfel 43a71c2353 Add Krea2 reverse cowgirl candidate 2026-06-29 07:05:44 +02:00
Ethanfel f937d3c109 Add Krea2 cowgirl alt candidate 2026-06-29 06:54:23 +02:00
Ethanfel b41d140927 Add Krea2 cowgirl candidate 2026-06-29 06:38:18 +02:00
Ethanfel f73eb72d68 Add Krea2 folded missionary candidate 2026-06-29 06:20:11 +02:00
Ethanfel f855c7b022 Add Krea2 missionary candidate 2026-06-29 06:04:08 +02:00
Ethanfel 2a29fcdfbb Add Krea2 blowjob sitting candidate 2026-06-29 05:48:59 +02:00
Ethanfel 607c612196 Add Krea2 blowjob laying candidate 2026-06-29 05:36:49 +02:00
Ethanfel 8ff02a181b Add Krea2 blowjob side candidate 2026-06-29 05:24:56 +02:00
Ethanfel 00e371e4b6 Add Krea2 blowjob top-view candidate 2026-06-29 05:09:40 +02:00
Ethanfel 858fbe8d46 Add Krea2 spread pose candidate 2026-06-29 04:52:01 +02:00
Ethanfel d77e7631da Add Krea2 fingering pose candidate 2026-06-29 04:37:27 +02:00
Ethanfel e96b9e9aae Add Krea2 footjob pose candidate 2026-06-29 04:24:52 +02:00
Ethanfel 5a5d5dd6fe Add Krea2 atlas gap plans 2026-06-29 04:12:54 +02:00
Ethanfel 06525c42a3 Add Krea2 atlas coverage report 2026-06-29 04:04:32 +02:00
Ethanfel 3a09210f71 Add Krea2 next test plans 2026-06-29 03:55:17 +02:00
Ethanfel 333f4752f6 Add Krea2 tuning coverage report 2026-06-29 03:46:42 +02:00
Ethanfel fae5423513 Add Krea2 variant evidence node 2026-06-29 03:27:32 +02:00
Ethanfel d384cb8a46 Add Krea2 pose variant selector node 2026-06-29 03:07:57 +02:00
Ethanfel 742281f48f Add Krea2 fixed-seed eval log 2026-06-29 02:49:01 +02:00
Ethanfel 40ee843baf Add Krea2 pose variant catalog loader 2026-06-29 02:31:03 +02:00
Ethanfel 484fb40638 Add Krea2 POV pose variant catalog 2026-06-29 02:10:48 +02:00
Ethanfel a484783515 Tune Krea2 POV handjob wording 2026-06-29 01:55:54 +02:00
Ethanfel 11b7c2acf9 Tune Krea2 POV boobjob wording 2026-06-29 01:35:07 +02:00
Ethanfel bb53967df4 Tune Krea2 POV doggy prompts 2026-06-29 01:05:36 +02:00
Ethanfel ef3b983712 Document seed-controlled Krea2 evals 2026-06-28 22:56:50 +02:00
Ethanfel 0328e5ca3a Add Krea2 evaluation loop 2026-06-28 20:07:31 +02:00
Ethanfel 54617e4702 Add POV foreground clothing cues 2026-06-28 10:31:01 +02:00
38 changed files with 9652 additions and 92 deletions
+1
View File
@@ -2,3 +2,4 @@ __pycache__/
*.py[cod]
.pytest_cache/
.ruff_cache/
.sxcp_eval/
+798
View File
@@ -0,0 +1,798 @@
{
"version": 1,
"atlas_root": "/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2",
"purpose": "Machine-readable Krea2 POV pose-geometry catalog for fixed-seed SxCP prompt tuning.",
"status_values": {
"proven": "A route has atlas support and repeated or structural Krea2 evidence strong enough for generator defaults.",
"candidate": "A route has atlas support but needs more fixed-seed Krea2 tests before changing generator defaults.",
"unstable": "A route has known text-only limits and should prefer control images or a narrower variant."
},
"variants": [
{
"key": "pov_doggy_top_down_rear_entry",
"family": "doggy",
"status": "proven",
"atlas_folders": ["doggy", "doggy_alt"],
"action_family": "penetration",
"position_keys": ["doggy", "rear_entry", "on_all_fours"],
"canonical_geometry": "Top-down first-person rear-entry view from behind: viewer body cues at the bottom, hands near the woman's hips, woman on all fours with chest low, forearms folded, cheek turned sideways far ahead, back arched, and hips raised toward the camera.",
"prompt_cues": [
"top-down POV doggy position from behind",
"camera looks down over the viewer's hands onto the woman's raised hips",
"woman is on all fours with chest low, forearms folded, cheek turned sideways",
"back arched, hips raised high toward the camera",
"natural lower-body POV cues in the foreground"
],
"avoid_cues": [
"visible shoes or lower legs as the standing cue",
"viewer torso and thighs outside frame",
"face or mouth as the fluid target for rear-entry climax"
],
"reference_images": [
"doggy/65_doggy.png",
"doggy_alt/100_doggy_alt.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["doggy", "all fours", "rear-entry"]
},
"evidence": {
"fixed_seed_tests": ["65", "52", "5202"],
"guide_section": "docs/krea2-prompt-guide.md#pov-doggy--rear-entry",
"notes": "Visible viewer thighs, torso, or pelvis can be correct; shoes/lower-leg wording caused oral drift."
}
},
{
"key": "pov_boobjob_upright_cleavage",
"family": "boobjob",
"status": "proven",
"atlas_folders": ["boobjob"],
"action_family": "outercourse",
"position_keys": ["boobjob", "titjob", "breast_sex"],
"canonical_geometry": "Frontal upright first-person view: viewer reclines with thighs open while the woman faces him between his legs, breasts pressed together around a vertical shaft, glans above the cleavage near her mouth.",
"prompt_cues": [
"POV boobjob position",
"woman kneels upright between his legs facing him",
"penis rises vertically in the lower foreground",
"squeezed between her pressed-together breasts",
"woman's own fingers and nails cup her breasts from the outside",
"glans emerging above the cleavage directly below her mouth"
],
"avoid_cues": [
"torso bent forward over his pelvis",
"both hands push her breasts without naming whose hands",
"only foreground hands when the woman's hands are the intended hands"
],
"reference_images": [
"boobjob/100_boobjob.png",
"boobjob/18_boobjob.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["boobjob", "titjob", "breast sex"]
},
"evidence": {
"fixed_seed_tests": ["7301", "7302"],
"guide_section": "docs/krea2-prompt-guide.md#boobjob--titjob",
"notes": "Same-seed A/B showed upright cleavage-sleeve wording improves contact pressure; hand ownership must be explicit."
}
},
{
"key": "pov_handjob_upright_centered",
"family": "handjob",
"status": "proven",
"atlas_folders": ["handjob"],
"action_family": "outercourse",
"position_keys": ["handjob"],
"canonical_geometry": "Centered first-person view: viewer reclines with thighs open, the woman faces him between his legs, and the woman's hand is the main contact anchor on the shaft with her face and torso behind it.",
"prompt_cues": [
"POV handjob position",
"woman kneels between his legs facing him",
"the woman's right hand wraps around the viewer's penis",
"her left hand steadies the base",
"viewer thighs and pelvis frame the lower edges",
"without his hands covering the action"
],
"avoid_cues": [
"generic one hand grips when hand ownership matters",
"foreground hands competing with the woman's active hand"
],
"reference_images": [
"handjob/18_handjob.png",
"handjob/92_handjob.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["handjob", "hand job", "hand stroking"]
},
"evidence": {
"fixed_seed_tests": ["7401"],
"guide_section": "docs/krea2-prompt-guide.md#handjob",
"notes": "Same-seed A/B showed explicit woman-hand ownership removed viewer-hand ambiguity."
}
},
{
"key": "pov_ballsucking_low_head",
"family": "ballsucking",
"status": "candidate",
"atlas_folders": ["ballsucking"],
"action_family": "outercourse",
"position_keys": ["testicle_sucking", "ballsucking"],
"canonical_geometry": "Low first-person pelvis view: the woman stays low beside or between the viewer's open thighs, with cheek/thigh proximity, the scrotum as the mouth surface, scrotal skin as the nearest mouth surface, and testicles resting across her open lips while both testicles rest against her tongue from below as the accepted partial target.",
"prompt_cues": [
"woman bends forward and kneels very low between the viewer's open thighs",
"chest low over the viewer's pelvis",
"low side-pelvis POV",
"face is the closest visible partner part",
"cheek against the viewer's inner thigh",
"scrotum is the mouth surface",
"scrotal skin is the nearest mouth surface",
"testicles resting across her open lips while her tongue cups them from below",
"both testicles rest against her tongue from below",
"viewer abdomen and inner thighs frame the close foreground"
],
"avoid_cues": [
"head tucked under the penis shaft without testicle-height wording",
"repeating shaft/hand-on-shaft wording before scrotum/testicle contact is established",
"viewer first as the main subject",
"mid-height head placement"
],
"reference_images": [
"ballsucking/101_ballsucking.png",
"ballsucking/4_ballsucking.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["testicle_sucking", "balls licking", "testicle"]
},
"evidence": {
"fixed_seed_tests": ["238365845574312", "1212121212", "5757575757", "6262626262", "9797979797", "9898989898", "5959595959", "6060606060", "6161616161", "7171717171", "7272727272"],
"guide_section": "docs/krea2-prompt-guide.md#ballsucking--testicle-sucking",
"notes": "Fifty-probe threshold search accepted tongue/lips on testicles as a partial improvement over baseline shaft/glans collapse; generator carried the side-low partial axis provisionally. Fresh seed 6262626262 then showed open-lips scrotum-surface wording on turns 252 and 258 improved target contact over the generated-route controls 250 and 256. Fresh seed 9797979797 repeated the scrotal-skin target-object branch on turns 288 and 293, with scrotal skin as the nearest mouth surface and both testicles resting against tongue from below. Fresh seed 9898989898 validated the patched generated route on turns 296 and 297, preserving side-low cheek/thigh geometry while keeping scrotum/testicles at the tongue/lip contact. Fresh seed 5959595959 tested lip-oval, sideways mouth pocket, and chin-pelvis upward seal wording across three women; all branches kept some low-pelvis geometry but collapsed back toward shaft/glans contact, so record it as a weak case. Fresh seed 6060606060 tested foreground occlusion, under-scrotum tongue shelf, and hand-guided scrotum wording; every branch still became shaft-centered or hand/shaft-dominant, so keep the route candidate and do not patch those axes. Fresh seed 6161616161 tested exact mouth-sucking, single-testicle, hanging-balls-below-shaft, side-mouth-wrap, and chin-pelvis lower-mouth wording across three women; generated-route controls stayed the best repeated partials on turns 331 and 337, side-mouth and chin-pelvis branches produced isolated useful partials on turns 335 and 348, and the rest collapsed back to shaft/glans contact. Fresh seed 7171717171 tested flat pelvis-valley, thigh-tunnel, pubic-hair mouth-line, low-cushion chin-anchor, and pelvis-edge target-first wording across three women; flat pelvis-valley repeated a better viewer-flat body plane on turns 350, 356, and 362 but stayed shaft-centered, while the cushion and pelvis-edge branches drifted into wrong open-thigh/presentation geometry. Fresh seed 7272727272 tested hybrid flat-valley scrotal-skin, valley-floor open-lips, upper-frame shaft lower-scrotum, cropped upper-shaft valley-mouth, and side-low flat-valley wording; the flat-valley branch repeated the body plane on turns 368, 374, and 380 but stayed shaft-centered, and side-low flat-valley gave only look hints. Stop text-only expansion for now: do not patch those hybrid axes. The provisional generator route uses scrotum-as-mouth-surface, testicles resting across open lips, and scrotal-skin nearest-surface wording while staying candidate."
}
},
{
"key": "pov_footjob_frontal_sole_stroke",
"family": "footjob",
"status": "proven",
"atlas_folders": ["footjob"],
"action_family": "outercourse",
"position_keys": ["footjob"],
"canonical_geometry": "Frontal first-person footjob view: viewer reclines with thighs framing the lower foreground while the woman sits opposite with two large overlapping soles dominating the lower center foreground, inner arches pressing inward around the upright shaft, toes curled around both edges, a narrow visible strip of shaft and glans rising between the compressed feet, and her body and face behind the feet.",
"prompt_cues": [
"POV footjob position",
"viewer reclines with thighs framing the lower foreground",
"woman sits opposite facing him with legs open toward the camera",
"two large overlapping soles dominate the lower center foreground",
"inner arches press inward from both sides around the upright shaft",
"toes curl around both edges",
"narrow visible strip of shaft and glans rises between the compressed feet",
"woman's face and torso stay visible behind the large foreground feet"
],
"avoid_cues": [
"generic foot contact without both soles around the shaft",
"hands replacing the feet as the main contact",
"mouth or hand action competing with the footjob",
"feet off to the side without centered penis contact"
],
"reference_images": [
"footjob/59_footjob.png",
"footjob/86_footjob.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["footjob", "foot job", "feet stroking"]
},
"evidence": {
"fixed_seed_tests": ["238365845574312", "3434343434", "6868686868", "7373737373"],
"guide_section": "docs/krea2-prompt-guide.md#footjob",
"notes": "Same-seed two-woman expansions repeated the two-sole clamp as a provisional generator improvement over valid baselines; seed 6868686868 showed overlapping soles plus a narrow visible shaft/glans strip is more reliable than generic large-sole wording. Fresh seed 7373737373 then repeated the generated overlapping-sole/narrow-strip route across two women on turns 264 and 267, with tight center-gap repeats on turns 265 and 268. Promote the default route to proven and keep cross-foot side press as an alternate branch."
}
},
{
"key": "pov_fingering_reclined_open_thighs",
"family": "fingering",
"status": "candidate",
"atlas_folders": ["fingering"],
"action_family": "manual",
"position_keys": ["fingering", "open_thighs"],
"canonical_geometry": "First-person manual-contact view: the woman reclines or sits back with thighs spread wide toward the camera, face and torso visible behind the open legs, and the viewer hand enters from the foreground to make the visible contact between her legs.",
"prompt_cues": [
"POV fingering position",
"woman reclines with thighs spread wide toward the camera",
"viewer hand enters from the foreground",
"fingers make the central contact between her open thighs",
"her face and torso remain visible behind the open-leg frame",
"thighs and knees form the main framing around the action"
],
"avoid_cues": [
"generic hand near the body without visible manual contact",
"the woman's own hand replacing the POV hand",
"mouth, foot, or penetration action competing with the foreground hand",
"closed legs hiding the contact point"
],
"reference_images": [
"fingering/103_fingering.png",
"fingering/69_fingering.png",
"fingering/80_fingering.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["fingering", "finger", "manual stimulation"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows a repeated open-thigh manual-contact POV layout; needs fixed-seed Krea2 tests before promotion to proven."
}
},
{
"key": "pov_wand_foreground_tool_contact",
"family": "wand",
"status": "proven",
"atlas_folders": ["wand"],
"action_family": "toy",
"position_keys": ["wand", "toy_contact", "open_thighs"],
"canonical_geometry": "First-person toy-contact view: the woman reclines or sits back with thighs spread toward the camera, face and torso visible behind the open-leg frame, and the viewer hand holds a single continuous teal wand-style massager from the foreground with the rounded bulb head pressed flat to the central contact point.",
"prompt_cues": [
"POV wand toy position",
"woman reclines with thighs spread wide toward the camera",
"single continuous teal wand-style massager is the largest lower-frame object",
"viewer hand holds a wand-style toy from the foreground",
"rounded bulb head presses flat to her vulva and clit as the central contact point",
"her face and torso remain visible behind the open-leg frame",
"thighs and knees form the main frame around the foreground tool"
],
"avoid_cues": [
"generic toy nearby without contact",
"the woman holding the toy when the foreground viewer hand is intended",
"mouth, foot, or penetration action competing with the toy contact",
"closed legs hiding the contact point",
"toy floating without a visible hand or handle"
],
"reference_images": [
"wand/106_wand_.png",
"wand/107_wand_.png",
"wand/108_wand_.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["wand", "toy", "vibrator"]
},
"evidence": {
"fixed_seed_tests": ["246813579", "8642086420", "7979797979"],
"guide_section": "docs/krea2-prompt-guide.md#wand-toy-contact",
"notes": "The teal lower-right single-continuous-wand axis repeated across two women on sampler seeds 8642086420 and 7979797979 and validated through generated-route turns 197, 234, and 238. The pale upper-left wand remains a useful alternate branch; oversized bulb wording can hide contact."
}
},
{
"key": "pov_ejaculation_aftermath_open_thigh_candidate",
"family": "ready",
"status": "candidate",
"atlas_folders": ["ready"],
"action_family": "climax",
"position_keys": ["open_thighs", "camera_showing"],
"canonical_geometry": "First-person post-ejaculation open-thigh display: the woman reclines or sits back facing the viewer with thighs spread open, face and torso readable behind the open-leg frame, viewer body cue or recently withdrawn foreground cue near the lower edge, and thick semen or fluid visibly coating or dripping around the exposed pussy or anal opening.",
"prompt_cues": [
"POV post-ejaculation open-thigh display pose",
"woman reclines or sits back facing the viewer with thighs spread open",
"thick semen or fluid is visible around the exposed pussy or anal opening",
"the body stays still after ejaculation",
"viewer body cue or recently withdrawn foreground cue stays near the lower edge",
"her face and torso remain visible behind the open-leg frame",
"thighs and knees frame the wet aftermath detail without hiding it"
],
"avoid_cues": [
"generic ready/setup pose before sex",
"active thrusting or penetration-in-progress wording",
"turning the setup into oral, toy, or manual contact",
"generic wetness without thick visible fluid around the exposed opening",
"closed thighs hiding the aftermath detail",
"cropping out the face and torso behind the open-leg frame"
],
"reference_images": [
"ready/105_ready_.png",
"ready/106_ready_.png",
"ready/107_ready_.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["post-ejaculation open-thigh display", "thick visible semen or fluid", "open thighs"]
},
"evidence": {
"fixed_seed_tests": ["1123581321"],
"guide_section": "docs/krea2-prompt-guide.md#ready--post-ejaculation-open-thigh-display",
"notes": "The ready folder is a post-ejaculation open-thigh display pose with thick visible fluid around the exposed opening, not a neutral ready/setup pose. First fixed-seed evidence on source 52 was mirrored into the generator as a provisional improvement; repeat before promotion to proven."
}
},
{
"key": "pov_spread_open_thigh_presentation",
"family": "spread",
"status": "candidate",
"atlas_folders": ["spread"],
"action_family": "interaction",
"position_keys": ["open_thighs", "camera_showing"],
"canonical_geometry": "Frontal open-thigh presentation view: the woman faces the camera with legs raised or knees held wide, thighs forming a wide V-frame toward the viewer, face and torso visible behind the open-leg pose, and no required partner contact.",
"prompt_cues": [
"POV open-thigh presentation position",
"woman faces the camera with legs raised or knees held wide",
"thighs form a wide V-frame toward the viewer",
"face and torso remain visible behind the open-leg pose",
"hands may hold the knees or thighs open",
"no partner contact is required for this setup pose"
],
"avoid_cues": [
"adding penetration or manual contact by default",
"closed thighs hiding the open-leg geometry",
"cropping out the face and torso behind the leg frame",
"turning the pose into doggy or side-lying geometry"
],
"reference_images": [
"spread/100_spread_.png",
"spread/4_spread_.png",
"spread/69_spread_.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["spread", "open thighs", "legs spread"]
},
"evidence": {
"fixed_seed_tests": ["3141592653"],
"guide_section": "docs/krea2-prompt-guide.md#spread--open-thigh-presentation",
"notes": "Same-seed A/B on source 50 and 47 showed raised-knee V-frame and hand-on-knee hierarchy improves over generic spread wording. Mirrored into the generator as a provisional improvement; repeat on another seed before promotion to proven."
}
},
{
"key": "pov_sixty_nine_close_reversed_oral",
"family": "sixty_nine",
"status": "unstable",
"difficulty": "hardest",
"priority": "low",
"control_requirement": "pose_or_image_guidance_first",
"atlas_folders": ["69"],
"action_family": "oral",
"position_keys": ["sixty_nine"],
"canonical_geometry": "Close first-person sixty-nine view: the visible partner is reversed over the viewer with hips closest to the camera, head and torso receding away into the upper frame, viewer face or mouth anchoring the lower foreground, and hands optionally holding the hips to keep the reversed body arrangement readable.",
"prompt_cues": [
"POV close sixty-nine position",
"visible partner is reversed over the viewer with hips closest to the camera",
"the partner's head and torso recede away into the upper frame",
"viewer face or mouth anchors the lower foreground under the partner's hips",
"viewer hands may hold the partner's hips without changing the reversed-over-viewer body arrangement",
"keep the mutual oral geometry readable as one continuous first-person frame"
],
"avoid_cues": [
"side-by-side sixty-nine layout",
"upright oral pose with the partner facing the viewer",
"generic oral contact without the reversed-over-viewer body arrangement",
"cropping away the head-and-torso direction that proves the sixty-nine setup",
"text-only prompting when exact geometry matters; prefer pose control or image guidance"
],
"reference_images": [
"69/105_sixtynine.png",
"69/106_sixtynine.png",
"69/50_sixtynine.png",
"69/80_sixtynine.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["sixty-nine", "reversed over viewer", "mutual oral"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Lowest-priority atlas route for now: geometry is consistent but visually fragile for text-only Krea2 prompting. Treat it as a pose/control-image or image-guidance-first case, not a normal prompt-only fixed-seed candidate."
}
},
{
"key": "pov_blowjob_top_down_vertical_shaft",
"family": "blowjob_top_view",
"status": "candidate",
"atlas_folders": ["blowjob_top_view"],
"action_family": "oral",
"position_keys": ["kneeling", "top_down_oral"],
"canonical_geometry": "Nadir-angle standing male POV top-view oral view: the viewer looks almost straight down from his torso toward the floor, nearby floor plane dominates the image, the viewer abdomen, shorts, thighs, and feet frame the lower foreground, the shaft is a short centered vertical column, and one woman kneels directly below between his feet with hair crown, forehead, shoulders, hands, knees, mouth, and shaft alignment visible from above.",
"prompt_cues": [
"nadir-angle standing male POV top-view oral position",
"viewer looks almost straight down from his torso toward the floor",
"nearby carpet/floor plane dominates the image",
"viewer abdomen, shorts, thighs, and feet frame the lower foreground",
"shaft is a short centered vertical column",
"one woman kneels directly below the viewer between his feet",
"hair crown, forehead, shoulders, hands, and knees are visible from above",
"desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors"
],
"avoid_cues": [
"side-view oral framing",
"woman standing level with the viewer",
"shaft angled sideways or cropped away from the mouth",
"hands replacing the mouth as the main oral contact",
"camera placed behind the woman instead of above the viewer",
"literal plumb-line or map wording that renders as drawn graphics"
],
"reference_images": [
"blowjob_top_view/102_blowjob_top_view.png",
"blowjob_top_view/2_blowjob_top_view.png",
"blowjob_top_view/85_blowjob_top_view.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["kneeling oral", "top-down oral", "oral"]
},
"evidence": {
"fixed_seed_tests": ["4242424242"],
"guide_section": "docs/krea2-prompt-guide.md#blowjob-top-view--overhead-vertical-shaft",
"notes": "Same-sampler source 46/47 A/B showed that top-down oral hierarchy tightens hand-at-base support and centered shaft-to-mouth alignment over generic kneeling oral. A follow-up axis loop on the same seed showed that generic steep-overhead wording can still feel horizontal, while nadir-angle standing male POV plus a dominating nearby floor plane, one woman directly between the viewer's feet, top-down office anchors, and a short centered vertical shaft column gives the strongest atlas-like verticality. Avoid plumb-line/map wording because Krea2 can literalize it as drawn graphics. Keep candidate until another source or seed repeats the nadir-angle axis."
}
},
{
"key": "pov_blowjob_side_profile_oral",
"family": "blowjob_side",
"status": "proven",
"atlas_folders": ["blowjob_side"],
"action_family": "oral",
"position_keys": ["side_lying", "reclining_oral", "penis_licking"],
"canonical_geometry": "Side-profile first-person oral body-line view: the male viewer's abdomen, navel, pelvis, and near thigh create the broad lower-frame foreground surface, the adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground, the woman enters laterally from the left edge beside his hip, and her side-facing mouth plus hand contact align to the shaft at the male abdomen line.",
"prompt_cues": [
"POV side-profile oral body-line position",
"male viewer's abdomen, navel, pelvis, and near thigh create a broad horizontal body surface",
"adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground",
"navel, abdomen hair, pelvis, and near thigh mark the camera owner's body",
"woman enters laterally from the left edge beside his hip",
"cheek and jaw stay in profile",
"mouth on the shaft at the male abdomen line",
"lips touching the shaft at the male abdomen line",
"mouth-to-shaft contact is the nearest facial detail",
"hand around the base under her lips",
"shoulder and torso trail sideways along the edge"
],
"avoid_cues": [
"top-down oral framing",
"front-facing centered face instead of side profile",
"woman standing level with the viewer",
"camera behind the woman",
"hands replacing the mouth as the main oral contact",
"pure male-body-axis wording that exposes the male as a photographed subject",
"transferring the central body surface to the woman"
],
"reference_images": [
"blowjob_side/103_blowjob_side.png",
"blowjob_side/105_blowjob_side.png",
"blowjob_side/29_blowjob_side.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["side_lying", "side-lying oral", "blowjob_side", "side-profile oral", "oral"]
},
"evidence": {
"fixed_seed_tests": ["5656565656", "9753197531", "9595959595", "9696969696", "5858585858"],
"guide_section": "docs/krea2-prompt-guide.md#blowjob-side-profile--side-phone-weak-case",
"notes": "Seed 5656565656 first produced attractive side-phone / external side-camera oral compositions across source 46 and 47, but not valid POV evidence. A later source-46 candidate with explicit adult-male foreground ownership recovered a more atlas-like first-person body-line view, while a related source-47 body-axis candidate failed by transferring the central body surface to the woman. Seed 9753197531 then repeated the lateral-edge body-line wording across two women. Generated-route turn 207 showed the route also needs lips-touching and mouth-to-shaft-contact priority to keep the mouth from floating above the shaft. Seed 9595959595 repeated the lower-right torso anchor on turns 279 and 283 across two visible women, improving camera-owner torso ownership over a control that could expose the male as a photographed side subject. Seed 9696969696 generated-route validation repeated the patched route on turns 284 and 285, keeping lower-right own-body foreground, profile mouth contact, and office depth across two visible women. Seed 5858585858 added a three-woman generated-route repeat on turns 298, 301, and 304; all controls preserved the patched camera-owner lower-right body plane, lateral profile entry, mouth contact at the abdomen line, and office depth. Promote the generated side-profile POV hierarchy to proven while keeping side-camera-style self-body crop wording as a look branch rather than the default."
}
},
{
"key": "pov_blowjob_laying_frontal_oral",
"family": "blowjob_laying",
"status": "candidate",
"atlas_folders": ["blowjob_laying"],
"action_family": "oral",
"position_keys": ["reclining_oral", "penis_licking"],
"canonical_geometry": "Frontal prone first-person oral view: the viewer reclines with open thighs framing the lower foreground, the woman lies belly-down between the viewer's open thighs, and her front-facing mouth and hands align to a shaft rising from the lower center of the frame.",
"prompt_cues": [
"POV prone frontal oral position",
"viewer reclines with open thighs framing the lower foreground",
"woman lies belly-down between the viewer's open thighs",
"her chest and shoulders stay low over the viewer's pelvis",
"shaft rises from the lower center toward her front-facing mouth",
"her hands support or steady the shaft while mouth contact remains the main action"
],
"avoid_cues": [
"side-profile oral framing",
"top-down oral framing from above the viewer",
"woman kneeling upright instead of lying forward",
"woman standing level with the viewer",
"hands replacing the mouth as the main oral contact"
],
"reference_images": [
"blowjob_laying/101_blowjob_laying.png",
"blowjob_laying/103_blowjob_laying.png",
"blowjob_laying/60_blowjob_laying.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["blowjob_laying", "prone frontal oral", "oral"]
},
"evidence": {
"fixed_seed_tests": ["6767676767"],
"guide_section": "docs/krea2-prompt-guide.md#blowjob-laying-frontal--wide-v-frame",
"notes": "Seed 6767676767 improved source 46 and 50 with a wide symmetrical V-frame, lower abdomen near-edge anchor, torso stretched low and horizontal between the viewer's thighs, hands at the base, and centered mouth-to-shaft contact. Baselines were already strong but read more raised-hips or all-fours than prone belly-down, so keep the route candidate until another seed repeats the low-horizontal body improvement."
}
},
{
"key": "pov_blowjob_sitting_upright_oral",
"family": "blowjob_sitting",
"status": "candidate",
"atlas_folders": ["blowjob_sitting"],
"action_family": "oral",
"position_keys": ["reclining_oral", "penis_licking", "blowjob_sitting"],
"canonical_geometry": "Upright seated first-person oral view: the viewer reclines with open thighs framing the lower foreground, the woman sits upright between the viewer's open thighs, and her close front-facing mouth aligns to a vertical shaft centered between the viewer's legs.",
"prompt_cues": [
"POV upright sitting oral position",
"viewer reclines with open thighs framing the lower foreground",
"woman sits low between the viewer's open thighs with torso upright behind the action",
"her face lowers close to the exact center contact point",
"vertical shaft centered between the viewer's legs",
"her open mouth covers the centered tip with hands wrapped low at the base"
],
"avoid_cues": [
"prone belly-down oral framing",
"side-profile oral framing",
"top-down oral framing from above the viewer",
"woman standing level with the viewer",
"cropping away the viewer's open-thigh frame"
],
"reference_images": [
"blowjob_sitting/100_blowjob_sitting.png",
"blowjob_sitting/24_blowjob_sitting.png",
"blowjob_sitting/58_blowjob_sitting.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["blowjob_sitting", "upright sitting oral", "oral"]
},
"evidence": {
"fixed_seed_tests": ["7878787878"],
"guide_section": "docs/krea2-prompt-guide.md#blowjob-sitting-upright--low-mouth-contact",
"notes": "Seed 7878787878 improved source 46 and 50 with low-mouth seated hierarchy: viewer thigh V-frame, lower abdomen near edge, woman sitting low between the thighs with torso upright behind the action, face lowered to the exact center contact point, open mouth covering the centered shaft tip, and both hands wrapped at the base. Source 50 had some outfit looseness/drift, so keep the route candidate and provisional until another seed repeats it."
}
},
{
"key": "pov_missionary_open_leg_penetration",
"family": "missionary",
"status": "candidate",
"atlas_folders": ["missionary"],
"action_family": "penetration",
"position_keys": ["missionary", "open_thighs", "front_entry"],
"canonical_geometry": "First-person missionary view from above the viewer's pelvis: the woman reclines on her back facing the viewer, knees open toward the viewer, thighs frame the central contact line, and the viewer's lower body or hands anchor the lower foreground.",
"prompt_cues": [
"POV missionary open-leg penetration position",
"woman reclines on her back with knees open toward the viewer",
"her face, torso, and open thighs remain visible in one frame",
"viewer is positioned between her legs from the lower foreground",
"thighs frame the central penetration line",
"viewer hands may hold her thighs without blocking the contact geometry",
"for flat elevated-support examples, viewer stands or braces at the foot edge with feet, shins, or side-dropping legs below the support edge"
],
"avoid_cues": [
"folded-leg or knees-to-chest geometry",
"rear-entry or doggy geometry",
"side-profile framing",
"cropping away the woman's face and torso",
"viewer standing far back instead of positioned between her legs"
],
"reference_images": [
"missionary/101_missionary.png",
"missionary/102_missionary.png",
"missionary/1_missionary.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["missionary", "open-leg penetration", "front-entry"]
},
"evidence": {
"fixed_seed_tests": ["8989898989"],
"guide_section": "missionary-open-leg--seated-lounge-drift",
"notes": "Same-seed batches on 8989898989 show two valid subcases. Generic/angled missionary can preserve open thighs, viewer hands, and centered contact, while the flatter atlas examples need elevated-support edge placement: woman flat across a table/platform, viewer standing or braced at the foot edge, and viewer feet/shins or side-dropping legs below the support. The accepted turn84 axis is mirrored only into the raised-edge/edge-supported route as a provisional patch; keep generic missionary available and keep the catalog candidate until another seed repeats it."
}
},
{
"key": "pov_missionary_folded_high_leg_penetration",
"family": "missionary_folded",
"status": "candidate",
"atlas_folders": ["missionary_folded"],
"action_family": "penetration",
"position_keys": ["missionary_folded", "front_entry"],
"canonical_geometry": "First-person folded missionary view from above the viewer's pelvis: the woman reclines on her back facing the viewer, knees folded high toward her chest, feet or ankles close to the camera, and the viewer's hands hold her calves or ankles while the central contact line stays below the raised legs.",
"prompt_cues": [
"POV folded missionary high-leg penetration position",
"woman reclines on her back with knees folded high toward her chest",
"viewer lower abdomen and a large centered shaft anchor the lower center first",
"compact folded-knee block sits above the contact point",
"feet or ankles sit close to the camera above the contact line",
"viewer hands hold her calves or ankles in the foreground",
"her face and torso remain visible between or behind the raised legs",
"central penetration line stays below the folded knees"
],
"avoid_cues": [
"normal open-leg missionary with knees spread low",
"rear-entry or doggy geometry",
"side-profile framing",
"legs cropped away or relaxed flat on the bed",
"feet replacing the penetration geometry as the main action"
],
"reference_images": [
"missionary_folded/16_missionary_folded.png",
"missionary_folded/50_missionary_folded.png",
"missionary_folded/80_missionary_folded.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["missionary_folded", "folded missionary", "knees-to-chest"]
},
"evidence": {
"fixed_seed_tests": ["8989898989"],
"guide_section": "missionary-folded--contact-first-knee-block",
"notes": "Same-seed turns 85-92 show that subject-first knees-to-chest wording can produce folded high-leg geometry, but Krea2 drops readable shaft/contact when the knees and feet dominate first. The accepted turn89 axis puts the viewer lower abdomen and large centered shaft/contact before the compact folded-knee block, then holds calves/ankles and keeps the face/torso behind the raised knees. Mirrored into the folded-missionary route as a provisional patch; keep catalog candidate until another seed or subject repeats it."
}
},
{
"key": "pov_cowgirl_frontal_straddle_penetration",
"family": "cowgirl",
"status": "proven",
"atlas_folders": ["5.cowgirl"],
"action_family": "penetration",
"position_keys": ["cowgirl", "frontal_straddle", "woman_on_top"],
"canonical_geometry": "First-person frontal cowgirl view: the viewer reclines below while the woman straddles the viewer facing him, knees open to either side, torso upright above the contact line, and the viewer's thighs, pelvis, or hands anchor the lower foreground.",
"prompt_cues": [
"POV frontal cowgirl straddle penetration position",
"woman straddles the viewer facing him",
"her torso stays upright above the viewer",
"viewer lower abdomen and pelvis anchor the bottom edge",
"wide horizontal thigh bridge spans left edge to right edge",
"her knees are open to either side of the viewer's hips",
"viewer reclines below with thighs or pelvis in the lower foreground",
"viewer hands may hold her thighs or hips without blocking the centered contact line"
],
"avoid_cues": [
"missionary with the woman lying on her back",
"reverse cowgirl with the woman facing away",
"folded-leg knees-to-chest geometry",
"rear-entry or doggy geometry",
"cropping away the upright torso and straddling knees"
],
"reference_images": [
"5.cowgirl/100_cowgirl.png",
"5.cowgirl/101_cowgirl.png",
"5.cowgirl/1_cowgirl.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["cowgirl", "frontal cowgirl", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": ["8989898989", "2828282828", "9191919191"],
"guide_section": "cowgirl-frontal--wide-thigh-bridge",
"notes": "Same-seed turns 93-96 show the generic baseline already validly hits frontal cowgirl on seed 8989898989. The best atlas-like improvement was turn95: wide horizontal thigh bridge from left edge to right edge, viewer lower abdomen/pelvis at the bottom edge, upright torso above the contact, and hands gripping the thigh sides. Seed 2828282828 then repeated the wide-thigh bridge hierarchy across two visible women on turns 209 and 213, and generated-route turn 216 validated the patched normal cowgirl route. Fresh seed 9191919191 repeated the generated route and three branch wordings across turns 242-249, with turns 242, 243, 244, and 248 giving the clearest atlas-like wide-thigh bridge. Promote the normal cowgirl route to proven while keeping cowgirl-alt and reverse-cowgirl families separate."
}
},
{
"key": "pov_cowgirl_alt_low_squat_penetration",
"family": "cowgirl_alt",
"status": "candidate",
"atlas_folders": ["5.cowgirl_alt"],
"action_family": "penetration",
"position_keys": ["cowgirl_alt", "woman_on_top"],
"canonical_geometry": "Close first-person cowgirl-alt view: the viewer lies flat on his back underneath while the woman faces him in a low seated squat over the viewer's pelvis, knees bent wide near the camera, torso close above the contact line, and ceiling or upper-wall background cues confirm the low upward viewpoint.",
"prompt_cues": [
"POV low cowgirl seated-squat penetration position",
"viewer lies flat on his back underneath her",
"lens sits low at the viewer's abdomen looking upward from his pelvis",
"woman faces the viewer in a low seated squat over the viewer's pelvis",
"her knees are bent wide and close to the camera on either side of the viewer's hips",
"her torso stays close above the centered contact line",
"ceiling lights, upper walls, or high partition lines appear behind her upper body",
"viewer reclines below with thighs, pelvis, or lower torso anchoring the foreground",
"viewer hands may hold the underside of her thighs or hips without blocking the centered contact line"
],
"avoid_cues": [
"upright distant cowgirl with the torso far from the viewer",
"missionary with the woman lying on her back",
"reverse cowgirl with the woman facing away",
"folded-leg knees-to-chest geometry",
"rear-entry or doggy geometry",
"cropping away the wide bent knees and close seated position"
],
"reference_images": [
"5.cowgirl_alt/101_cowgirl_alt.png",
"5.cowgirl_alt/102_cowgirl_alt.png",
"5.cowgirl_alt/103_cowgirl_alt.png",
"5.cowgirl_alt/16_cowgirl_alt.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["cowgirl_alt", "low cowgirl", "seated-squat cowgirl", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": ["8989898989"],
"guide_section": "cowgirl-alt--flat-supine-low-angle",
"notes": "Same-seed turns 97-104 show that low-squat/contact wording can still miss the atlas orientation by reading as a platform/high-camera setup. The accepted turn104 axis uses flat-supine viewer wording plus ceiling and upper glass/room background cues: viewer abdomen and chest lie flat at the bottom, lens looks upward from his abdomen/pelvis, woman is mounted over him with wide bent knees, and centered contact remains readable. Mirrored into the cowgirl-alt route as a provisional patch; keep catalog candidate until another seed or subject repeats it."
}
},
{
"key": "pov_reverse_cowgirl_back_facing_penetration",
"family": "reverse_cowgirl",
"status": "candidate",
"atlas_folders": ["cowgirl_reverse"],
"action_family": "penetration",
"position_keys": ["reverse_cowgirl", "back_facing_straddle", "woman_on_top"],
"canonical_geometry": "First-person reverse cowgirl view: the viewer reclines below while the woman straddles the viewer facing away, her back and hips dominate the frame, her knees or thighs sit to either side of the viewer's hips, and the viewer's thighs, pelvis, hands, or lower torso anchor the lower foreground.",
"prompt_cues": [
"POV reverse cowgirl back-facing penetration position",
"woman faces away from the viewer in a back-facing straddle",
"her back, hips, and ass are the nearest largest shapes to the camera",
"her back, hips, and ass are closest to the camera while her face may turn over one shoulder",
"her knees or thighs are planted to either side of the viewer's hips",
"viewer reclines underneath with thighs, pelvis, or lower torso anchoring the foreground",
"viewer thighs frame the lower corners",
"centered contact sits directly between her thighs below her ass",
"viewer hands may hold her hips or thighs without changing the woman-on-top geometry"
],
"avoid_cues": [
"frontal cowgirl with the woman facing the viewer",
"missionary with the woman lying on her back",
"rear-entry or doggy geometry with the viewer behind her",
"woman on all fours",
"side-profile penetration without the back-facing straddle",
"cropping away the back, hips, and viewer-underneath foreground cues"
],
"reference_images": [
"cowgirl_reverse/101_cowgirl_reverse.png",
"cowgirl_reverse/104_cowgirl_reverse.png",
"cowgirl_reverse/106_cowgirl_reverse.png",
"cowgirl_reverse/1_cowgirl_reverse.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["cowgirl_reverse", "reverse cowgirl", "back-facing straddle", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": ["8989898989"],
"guide_section": "reverse-cowgirl--close-back-hip-dominant",
"notes": "Same-seed turns 105-108 show that generic facing-away reverse-cowgirl wording can collapse into frontal cowgirl. The accepted turn106 axis makes the back/hips/ass the nearest largest shapes, puts the viewer underneath with thighs framing the lower corners, and keeps centered contact directly between her thighs below her ass. Turns 107 and 108 are useful secondary evidence for viewer-leg V-frame and over-shoulder glance variants. Mirrored into the reverse-cowgirl route as a provisional patch; keep candidate until another seed or subject repeats it, and keep reverse-cowgirl-alt separate for the more upright seated atlas family."
}
},
{
"key": "pov_reverse_cowgirl_alt_upright_back_facing_penetration",
"family": "reverse_cowgirl_alt",
"status": "candidate",
"atlas_folders": ["cowgirl_reversere_alt"],
"action_family": "penetration",
"position_keys": ["reverse_cowgirl_alt", "reverse_cowgirl", "back_facing_straddle", "woman_on_top", "upright_seated"],
"canonical_geometry": "Upright first-person reverse cowgirl alt view: the viewer reclines below while the woman sits upright facing away in a back-facing straddle, her back and ass stay centered above the viewer's pelvis, her knees or thighs frame the viewer's hips, and viewer hands may hold her hips, thighs, wrists, or hands.",
"prompt_cues": [
"POV upright reverse cowgirl back-facing penetration position",
"woman sits upright facing away from the viewer in a back-facing straddle",
"her back stays vertical and readable above her hips",
"her ass is centered above the viewer's pelvis while her knees or thighs frame the viewer's hips",
"viewer hands hold her hips",
"viewer thighs frame the lower corners",
"centered contact remains visible below her ass",
"viewer reclines underneath with thighs, pelvis, or lower torso anchoring the foreground",
"viewer hands may hold her hips, thighs, wrists, or hands without changing the upright woman-on-top posture"
],
"avoid_cues": [
"close hip-only reverse cowgirl crop without the upright back",
"frontal cowgirl with the woman facing the viewer",
"missionary with the woman lying on her back",
"rear-entry or doggy geometry with the viewer behind her",
"woman on all fours",
"cropping away the vertical back and seated woman-on-top posture"
],
"reference_images": [
"cowgirl_reversere_alt/100_cowgirl_reversere_alt.png",
"cowgirl_reversere_alt/101_cowgirl_reversere_alt.png",
"cowgirl_reversere_alt/102_cowgirl_reversere_alt.png",
"cowgirl_reversere_alt/18_cowgirl_reversere_alt.png"
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["cowgirl_reversere_alt", "reverse cowgirl alt", "upright back-facing straddle", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": ["8989898989"],
"guide_section": "reverse-cowgirl-alt--upright-seated-back-facing",
"notes": "Same-seed turns 109-112 show that the upright seated reverse-cowgirl-alt family is distinct from the close normal reverse-cowgirl route. Turn 109's generic upright baseline was already valid, while turn110's vertical-back plus hands-on-hips wording best matched the alt atlas: back and shoulders stay upright/readable, ass centered over the viewer's pelvis, viewer hands hold both hips, viewer thighs frame the lower corners, and centered contact remains below her ass. Mirrored into a separate reverse-cowgirl-alt route as a provisional patch; keep candidate until another seed or subject repeats it."
}
}
]
}
+5 -1
View File
@@ -7,7 +7,7 @@
"weight": 1.0,
"subject_type": "configured_cast",
"item_label": "Sexual pose",
"style": "explicit consensual adult hardcore sex scene, anatomically clear body positioning, adults only",
"style": "explicit consensual adult hardcore sex scene, anatomically clear body positioning, all participants are adults",
"positive_suffix": "Use clear adult anatomy, visible sexual contact, readable limb placement, precise body orientation, coherent spatial depth, and intense body language.",
"negative_prompt": "minors, childlike appearance, teen, schoolgirl, incest, bestiality, non-consensual, coercion, rape, violence, injury, blood, gore, watermark",
"scene_pools": ["hardcore_private_scenes"],
@@ -1110,8 +1110,11 @@
],
"position": [
"missionary position",
"folded missionary position",
"cowgirl position",
"low cowgirl seated-squat position",
"reverse cowgirl position",
"upright reverse cowgirl position",
"doggy style position",
"standing sex position",
"spooning sex position",
@@ -2105,6 +2108,7 @@
"standing with cum on the body",
"straddling a partner's hips in cowgirl position",
"reverse cowgirl over a partner's hips",
"upright reverse cowgirl over a partner's hips",
"on all fours with hips raised",
"face-down ass-up on the mattress",
"side-lying with thighs parted",
+461
View File
@@ -0,0 +1,461 @@
# Krea2 A/B Methodology Memory
This file is the persistent memory for SxCP Krea2 prompt A/B methodology.
Update it whenever the testing method improves.
## Current Method
Version: `2026-06-30-generated-route-validation-positive-channel-cleanup`
1. Pull or construct the baseline from an actual SxCP/CodexMCPTest source case.
2. Keep the sampler seed fixed across the baseline and candidate.
3. Keep subject, location family, camera family, and target pose fixed unless
the experiment explicitly tests one of those axes.
4. Change one prompt variable at a time when possible, usually the visual
hierarchy for the target contact or pose.
5. Keep `sxcp_eval_out` positive-only. Do not place negative-conditioning
phrases in the visible prompt.
6. Use location-compatible anchors only. For coworking/office scenes, use chair
edge, desk edge, laptop table, glass partitions, repeated desk rows, plants,
and window depth instead of bedroom or bedding anchors.
7. Treat a manual prompt win as proof that Krea2 responds to the wording, not
proof that the SxCP generator already emits it.
8. Mirror a prompt win into the generator as a provisional improvement when
leaving a category if same-seed evidence shows it improves over baseline and
the wording is generator-safe. Keep the route `candidate` until the broader
generator-patch evidence matrix proves it.
9. When a subject-first batch preserves appearance but repeatedly misses the
atlas body plane, record it as weak-case evidence and consider stronger
control before adding more generator text.
10. Score spatial orientation against the atlas before accepting evidence,
and treat a contradictory room/background read as a rejection even when
contact or limb placement is clear. Use background cues to decide whether
the viewer or partner is high, low, standing, seated, supine, or on a
support before grading pose/contact quality.
11. For hard text-only pose families, set an exploration budget before calling
the route weak or deciding it needs stronger control. Eight prompt probes
are only an early signal. Use batched wording-axis probes and aim for about
fifty positive-only tries across meaningful axes before concluding that
prompt text cannot reliably express the pose.
12. Do not require a perfect atlas hit before carrying progress forward. After
the exploration budget, a repeatable partial that beats the baseline failure
mode can become an accepted provisional generator improvement while the
remaining miss stays documented for later seed/source expansion.
13. After patching generator wording, render one prompt produced by the actual
code path before closing the category. Manual prompt-axis wins are not
enough; the generated route can still drop the key contact hierarchy or add
limiting positive-channel wording.
## Promotion Gates
- One clean fixed-seed A/B can be recorded as evidence for that source case.
- A prompt-guide rule needs repeated evidence across distinct subjects,
locations, or seeds, unless the generated prompt is structurally wrong before
rendering.
- A catalog variant remains candidate until the rule repeats under controlled
conditions.
- A provisional generator patch is allowed when leaving a category if the best
tested wording improves over baseline on a fixed seed. It should preserve the
selected subject, outfit, location, and camera semantics, and it must not patch
in a scene workaround that only solved one render.
- A proven/default generator patch still needs the broader evidence matrix below,
unless the generated prompt is structurally wrong before rendering.
## Generator Mirroring
After a manual A/B prompt win, do not assume the SxCP generator mirrors the
wording. Add a failing regression against the final formatter output first, then
patch the narrow route boundary that owns the wording. The regression should
assert the accepted hierarchy terms and reject the failure mode that caused the
bad render, such as scene-incompatible anchors or negative-conditioning text in
the positive prompt.
After the route patch, run a generated-route probe through `sxcp_eval_out` with
the same sampler seed when feasible. Use the actual formatter output, not a
hand-normalized prompt. If the generated route regresses compared with the
manual prompt-axis winner, record the failed generated-route image as the
baseline, tighten the route wording, and validate again before logging the
candidate as generated-route evidence.
For location-specific wins, split the implementation:
- the action or role graph owns the pose/contact hierarchy;
- the final Krea formatter owns scene-compatible anchor expansion because it can
see the selected scene, camera, and composition;
- existing route phrases that downstream tests rely on should be preserved
inside the stronger wording when they do not conflict with the A/B evidence.
## MCP Command Memory
Use the checked helper instead of ad hoc Python snippets for bridge calls. The
approved command prefix is:
```bash
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py
```
Common calls:
```bash
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py list-tools
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_pull --arguments-json '{"channel":"sxcp_eval_in"}'
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_push --arguments-json '{"channel":"sxcp_eval_out","seed":5656565656,"text":"PROMPT_ONLY_POSITIVE_CONDITIONING"}'
```
For batched prompt-axis search, prepare a JSON batch and use the offline command
renderer before touching the bridge manually:
```bash
python tools/sxcp_prompt_batch.py validate --batch-json /tmp/sxcp-batch.json
python tools/sxcp_prompt_batch.py print-push-commands --batch-json /tmp/sxcp-batch.json
python tools/sxcp_prompt_batch.py print-result-template --batch-json /tmp/sxcp-batch.json
python tools/sxcp_prompt_batch.py run-batch --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --previous-turn 80 --run
python tools/sxcp_prompt_batch.py validate-results --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json
python tools/sxcp_prompt_batch.py print-eval-entry-draft --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --variant-key pov_example_variant --baseline-image /absolute/baseline.png --candidate-id controlled_subject_first
```
Use `run-batch --run` for normal batch execution. It pushes one positive prompt,
polls `sxcp_eval_in` until the turn advances and an absolute PNG appears with
the fixed sampler seed, writes the filled result JSON, then sends the next
prompt. Omit `--run` for a dry-run command preview. Run `validate-results` after
the batch and before drafting evidence. It checks that every probe returned a
new ordered turn, an absolute PNG image path, and the same sampler seed as the
batch. This keeps batched prompt search as image-presence collection first and
bulk analysis second.
Before drafting evidence, compare atlas references and generated images for
spatial orientation, not only limb/contact similarity. First decide the
atlas's surface and camera-height relationship, then check whether the
generated background supports the same read. Use the background as a
camera-height witness: ceiling, upper walls, and high partition lines usually
support a low viewer looking upward; floor, carpet, table tops, platform edges,
or furniture behind the body can reveal a higher camera, seated support, or a
different surface. If the atlas target has the viewer flat on his back or the
partner mounted over him, do not accept a candidate only because contact is
clear; the room geometry must also support that flat/low read. Reject the
candidate before generator mirroring when the background says the bodies are on
a different surface or at a different height than the atlas.
`print-eval-entry-draft` rejects `geometry_only` candidates by default. Use
`--allow-geometry-only` only when the entry is explicitly labeled as
non-controlled prompt-axis evidence rather than subject/look-controlled A/B
evidence.
Keep `sxcp_eval_out` prompt-only and positive-only. Do not use
`sxcp_eval_negative_out` for Krea2 tuning.
## Generator-Patch Evidence Matrix
Do prompt and image exploration before editing production generator wording. A
normal pose-wording generator patch needs all of this evidence first:
- at least three distinct source cases with different visible subjects;
- at least two sampler seeds, unless the source prompt is structurally wrong
before rendering;
- location-family coverage when the proposed wording changes scene anchors;
- one baseline and one candidate per source case, with subject, location family,
camera family, and sampler seed fixed inside each pair;
- positive-only candidate prompts, with no negative-conditioning phrases in the
positive prompt.
A generated-route probe that works before the full matrix is useful evidence.
If it is the best tested improvement when leaving the category, it can become a
`provisional_generator_patch` with final prompt regression coverage. It should
not become a proven `generator_patch` decision until the matrix repeats and the
final generated prompt is regression-tested.
## Hard-Pose Exploration Budget
Use this budget for atlas poses where early prompt-only results repeatedly miss
the core spatial read.
- Define the failure threshold before the run. The default threshold is about
fifty positive-only prompt tries across distinct wording axes before declaring
the pose text-insufficient or moving it to a stronger-control bucket.
- Run the search in batches, usually six to twelve prompts at a time. Send each
prompt through `sxcp_eval_out`, wait for the image path, then analyze the
batch together instead of overreacting to one render.
- Keep a short axis ledger for each batch: intended wording axis, seed, source
subject, best image, repeated failure mode, and words that literalized or
harmed the result.
- Treat a small failed batch as direction, not a conclusion. If a batch shows a
repeated failure such as head height, camera height, viewer/partner elevation,
or background-plane mismatch, the next batch should vary that axis directly.
- Stop early only for a strong positive result that is worth repeating on a
second source or seed, or for a hard technical blocker. A weak but improving
result should feed the next wording batch rather than ending the category.
- If the threshold run finds a repeatable partial that is materially better
than baseline, accept the partial target explicitly and mirror only that
generator-safe improvement. Keep the route candidate and mark the evidence as
needing expansion when the full atlas target is still unsolved.
## Current Fingering Test Pattern
The prior bedding-based fingering prompt is invalid as a general rule because
it solved a lower-foreground artifact by adding bedroom context to an office
scene. The corrected test pattern keeps the coworking location intact:
- baseline: generic POV fingering/manual-contact wording from the same source
case;
- candidate: foreground hand first, open-thigh geometry second, visible woman
face/torso third, office chair and coworking depth fourth;
- anchors: black office chair seat/arms, desk edge, laptop table corners, glass
partitions, repeated desk rows, plants, tall-window depth;
- rejection trigger: any result that fixes contact by changing the scene family
instead of improving the pose hierarchy.
## Improvement Log
- `2026-06-30`: Added side-camera/result-label separation after ballsucking
seed `5757575757` produced attractive low side-camera oral views while still
collapsing the requested contact object onto the shaft/glans. Future scoring
should record that as side-view oral evidence and keep target-contact evidence
separate.
- `2026-06-30`: Added generated-route validation discipline after footjob turn
`183` kept large foreground soles but hid the shaft/contact that manual probes
had preserved. Future provisional generator patches should render the exact
final Krea prompt once after the code change; if shared route wording adds
limiting positive-channel language, clean it before sending the validation
prompt.
- `2026-06-30`: Added a hard-pose exploration budget after ballsucking wording
tests produced only eight early probes before the first weak-case note. Future
hard text-only poses should use batched wording-axis search and aim for about
fifty positive-only tries before concluding the pose needs stronger control.
- `2026-06-30`: Added partial-acceptance discipline after ballsucking produced
repeatable tongue/lips-on-testicles results that beat the shaft/glans
baseline but did not fully solve mouth-wrapped contact. Future hard-pose exits
should preserve repeatable progress as a provisional generator patch while
keeping the remaining miss in the expansion queue.
- `2026-06-30`: Added ballsucking target-object refinement after sampler seed
`9797979797` repeated the `scrotal skin is the nearest mouth surface` branch
on turns `288` and `293`. Score target-object ownership separately from the
side-low camera family: a route can preserve face/thigh geometry while still
drifting to shaft/base contact. Avoid promoting balls-first center-object
wording when it creates multi-subject or body-layout artifacts.
- `2026-06-30`: Added ballsucking generated-route validation after sampler seed
`9898989898` repeated the patched scrotal-skin route on turns `296` and
`297`. Validation can accept a provisional target-object improvement while
still keeping the pose queued when the remaining miss is full mouth-wrapped
testicle contact.
- `2026-06-30`: Added ballsucking fresh weak-case evidence after sampler seed
`5959595959` tested lip-oval, sideways mouth pocket, and chin-pelvis upward
seal wording across three women. The batch preserved low-pelvis/cheek-thigh
geometry in places, but every branch returned to shaft/glans collapse or
generic oral contact. Do not retry those axes as generator defaults; the next
search should change the target-object control strategy rather than adding
more mouth-shape synonyms.
- `2026-06-30`: Added ballsucking occlusion weak-case evidence after sampler
seed `6060606060` tested foreground occlusion, under-scrotum tongue shelf,
and hand-guided scrotum wording across three women. The generated route
remained the best partial while those axes became shaft-centered or
hand/shaft-dominant. Do not retry occlusion or hand-support synonyms as
generator defaults; the next useful move is a different target-object strategy
or stronger control.
- `2026-06-30`: Added ballsucking mouth-axis mixed-case evidence after sampler
seed `6161616161` tested exact mouth-sucking, single-testicle, hanging balls
below shaft, side-mouth wrap, and chin-pelvis lower-mouth wording across
three women. The generated-route controls stayed the best repeated partials
on two subjects, side-mouth and chin-pelvis variants produced isolated useful
partials, and the rest drifted back to shaft/glans contact. Record isolated
partials as axis hints, but do not patch generator wording unless a branch
repeats across subjects or beats the generated-route controls.
- `2026-06-30`: Added ballsucking pelvis-valley weak-case evidence after
sampler seed `7171717171` tested flat pelvis-valley, thigh tunnel,
pubic-hair mouth-line, low-cushion chin-anchor, and pelvis-edge target-first
wording across three women. The flat pelvis-valley branch repeated a strong
body-plane correction on three subjects, matching the atlas viewer-flat
thigh-wall read better, but it stayed shaft-centered. Score body-plane
orientation and target-object contact separately; do not patch a route when
it improves orientation while regressing the target.
- `2026-06-30`: Stopped the ballsucking text-only loop after sampler seed
`7272727272` combined `flat-valley scrotal-skin` target wording with the
prior side-low route across three women. The hybrid repeated the body-plane
hint on turns `368`, `374`, and `380`, but the target stayed shaft-centered,
while side-low flat-valley variants only gave look hints. Preserve the
current side-low scrotal-skin partial, do not patch the hybrid axes, and move
future full-target work toward stronger pose/control evidence rather than
more positive-prompt synonyms.
- `2026-06-30`: Promoted blowjob side-profile POV after sampler seed
`5858585858` produced a three-woman generated-route repeat on turns `298`,
`301`, and `304`. When the current generated route repeats across multiple
subjects on a fresh seed and alternate branches do not beat it cleanly, mark
the route proven instead of continuing to queue it. Keep attractive
side-camera-style self-body crop results as a separate look branch when they
risk drifting toward external side framing.
- `2026-06-29`: Added the multisource/generator-safe method after an overfit
single-character coworking test produced a visually usable but invalid
bedding foreground. Future A/B runs must test at least two source cases before
promoting wording that is meant to become a durable guide or generator rule.
- `2026-06-29`: Added generator mirroring discipline after the accepted
fingering wording proved Krea2 behavior but not generator output. Future
mirroring changes need a red-green regression at final Krea formatter output,
not just a guide entry.
- `2026-06-29`: Tightened generator-patch promotion after the fingering
generated-route probe looked good but had too little image coverage. Future
pose-wording generator edits need a broader seed, subject, and location matrix
before production route code changes.
- `2026-06-29`: Added semantic-axis discipline after source 52 fingering tests.
If a candidate succeeds by changing ownership, viewpoint, location family, or
role semantics, record it as a weak-case or prompt note unless that semantic
change is the intended generator behavior. Do not count it as direct evidence
for the original route even when the image is visually cleaner.
- `2026-06-29`: Added provisional generator-patch discipline after the user
clarified that leaving a category should still carry forward same-seed progress
over baseline. Future category exits should patch the generator with the best
generator-safe improvement, record it as `provisional_generator_patch`, and
keep the catalog route as `candidate` until repeated evidence proves it.
- `2026-06-29`: Applied the category-exit rule to spread/open-thigh presentation
after two source subjects improved on the same sampler seed. For setup poses
that are not structurally broken before rendering, prefer at least two source
subjects before mirroring a provisional generator patch, and keep the
observation explicit about remaining weak points such as insufficient V-frame
width or outfit closure.
- `2026-06-29`: Applied the same category-exit rule to blowjob top-view after
two source subjects improved on sampler seed `4242424242`. When the baseline is already usable,
record the improvement narrowly: name the axis that got better, keep the route
candidate, and avoid overstating the finding as proven until another seed
repeats it.
- `2026-06-29`: Corrected blowjob top-view criteria after atlas review and a
same-seed source-`46` probe showed that vertical shaft alignment alone can
still render as frontal/eye-height oral. Future top-view evidence must show
steep overhead camera geometry: viewer abdomen at the lower edge, camera
looking down from above the viewer chest/abdomen, and the woman's hair crown,
shoulders, and hands visible from above.
- `2026-06-29`: Refined blowjob top-view prompt-axis search after the user
rejected horizontally biased probes. Run several prompt-only probes before
editing the generator, wait for `sxcp_eval_in` to advance to the new turn, and
compare each image against the atlas verticality criteria. The useful axis is
`nadir-angle` or `bird's-eye` plus standing male POV, nearby floor plane
dominating the image, one woman directly below between the viewer's feet, and
top-down office anchors. Avoid `plumb-line` and `map` in generator prompts
because Krea2 can literalize them as drawn graphics.
- `2026-06-29`: For quick wording-axis search, prefer a batched prompt-probe
loop before analysis-heavy iteration. Prepare several positive-only alternate
prompts that isolate likely wording axes, send them one at a time through
`sxcp_eval_out` with the same sampler seed, pull only until each new
`sxcp_eval_in` turn and image path exists, then inspect the returned images as
a batch. Use the bulk comparison to pick the best axis, identify literalized
or harmful words, and only then update the generator, guide, catalog, or eval
log.
- `2026-06-29`: Preserve prompt-order controls when testing anything beyond
rough pose-axis discovery. Prompts that start with pose geometry and omit or
move the subject/look block can reduce female-look adherence, so treat those
runs as geometry-only probes. Durable A/B prompts should keep the original
subject/look description first, then the pose hierarchy, then location and
style/background anchors, unless the test is explicitly about prompt-order
sensitivity.
- `2026-06-29`: Added result-validation discipline to the batched prompt helper.
After sending a batch, fill the result template from `sxcp_eval_in`, run
`validate-results`, and only then draft evidence. The validation step proves
each probe returned an ordered turn, an absolute PNG artifact, and the fixed
sampler seed before bulk analysis or log-entry drafting.
- `2026-06-29`: Added `run-batch` automation to the batched prompt helper. It
removes manual push/pull copy-paste from normal A/B runs while keeping the same
gates: positive-only prompts, fixed sampler seed, turn advancement, absolute
PNG image path, and `validate-results` before evidence drafting.
- `2026-06-29`: Split missionary subcases after turns `77`-`84`. Turns `76` and
`80` are valid angled/cushion missionary results, not failures. The flatter
atlas examples need a different positive axis: woman flat across an elevated
table/platform, viewer standing or braced at the foot edge, and viewer feet,
shins, or side-dropping legs placed below the support edge. Patch this only
into the raised-edge/edge-supported route; keep generic missionary available
for angled valid views.
- `2026-06-29`: Folded-missionary tuning on seed `8989898989` used two
subject-first batches before code changes. Turns `85`-`88` showed that
compact knee-block and vertical-thigh-column wording can produce the folded
high-leg geometry, but the shaft/contact disappears when knees and feet lead
the hierarchy. Turns `89`-`92` then tested contact-first variants; turn `89`
was accepted because it placed the viewer lower abdomen and large centered
shaft/contact before the compact folded-knee block. This confirms the
method: use the first batch to identify the failed axis, run a targeted
second batch, then mirror only the accepted generator-safe hierarchy as a
provisional patch.
- `2026-06-29`: Frontal cowgirl on seed `8989898989` used a baseline-plus-
variants batch instead of comparing against a previous category. Turn `93`
was a valid generic cowgirl baseline, so turn `95`'s wide horizontal thigh
bridge improvement became a prompt-guide rule rather than a generator patch.
When the baseline already hits the pose, record the useful atlas refinement
and leave the generator unchanged unless repeated evidence shows a systemic
weakness.
- `2026-06-29`: Cowgirl-alt on seed `8989898989` exposed a spatial-orientation
blind spot. Turns `97`-`100` had readable contact and squat-like knees, but
the background still read as a platform/high-camera setup. After rechecking
the atlas, turns `101`-`104` tested flat-supine viewer wording with ceiling
and upper-room cues; turn `104` was accepted. Future pose analysis must
compare atlas and generated room geometry before accepting an image.
- `2026-06-29`: Reverse cowgirl on seed `8989898989` showed that a correct
semantic label such as `facing away` can be ignored when the visual hierarchy
still resembles frontal cowgirl. Future back-facing straddle tests should
score facing direction before contact quality and should name the back, hips,
and ass as the nearest largest shapes before viewer-leg and contact details.
Treat over-shoulder glances as secondary refinements only after the
back-facing straddle is already locked.
- `2026-06-29`: Reverse-cowgirl-alt on seed `8989898989` confirmed that atlas
sibling folders can need separate generator routes even when the baseline is
already valid. Normal reverse cowgirl is close back/hip dominant; reverse-alt
is upright seated with vertical back/shoulders and viewer hands or thighs
forming the lower frame. Keep those prompt hierarchies separate instead of
merging all back-facing woman-on-top evidence into one route.
- `2026-06-29`: Added non-target-viewpoint discipline after blowjob side-profile
oral produced an attractive side-camera result on seed `5656565656`. If a
render is visually useful but reads as a different camera family, record it as
a weak case for a future route and do not mirror it into the current POV
generator path.
- `2026-06-29`: Added MCP command memory after repeated context loss around the
bridge workflow. Future A/B calls should use the checked helper command
`/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py ...`, with
`comfy_push` to `sxcp_eval_out` for prompt-only positive conditioning and
`comfy_pull` from `sxcp_eval_in` for returned prompt/image/seed payloads.
- `2026-06-29`: Added side-profile oral ownership discipline after source `46`
improved with explicit adult-male foreground ownership while source `47`
rejected a related `body-axis` cue by transferring the body surface to the
woman. Future side-profile tests should name the foreground owner repeatedly
and verify that the woman's body stays lateral before considering any
generator mirroring.
- `2026-06-30`: Promoted the side-profile oral lateral-edge body-line axis
after sampler seed `9753197531` repeated it across two visible women. Pure
male-body-axis wording can expose the male as a photographed subject or let
Krea2 transfer the central body surface away from the intended first-person
view. Future generator patches should combine adult-male foreground ownership
with explicit lateral entry from the left edge, mouth at the male abdomen
line, and hand under the lips; keep the route provisional until another
seed/source expansion repeats it.
- `2026-06-30`: Added side-profile oral generated-route contact validation
after turn `206` kept the male body-line geometry but let the mouth float
above the shaft while the hand became the contact anchor. Turn `207` improved
after adding lips-touching and mouth-to-shaft-contact priority. Future
generated-route validation for oral side-profile should score both viewpoint
ownership and which body part actually anchors the contact.
- `2026-06-30`: Added the side-profile oral lower-right torso anchor after
sampler seed `9595959595` repeated it on turns `279` and `283` across two
visible women. The useful wording makes the adult male viewer's own torso
start at the lower edge and run diagonally into the lower-right foreground,
with navel, abdomen hair, pelvis, and near thigh marking the camera owner's
body. Prefer this over generic body-axis wording, which can expose the male
as a photographed side subject or transfer the axis onto the woman.
- `2026-06-30`: Added side-profile oral generated-route validation after
sampler seed `9696969696` repeated the patched route on turns `284` and
`285`. Count generated-route validation separately from prompt-axis search:
it proves the formatter can carry the new wording, while promotion still
requires broader source/seed evidence.
- `2026-06-30`: Promoted normal frontal cowgirl from guide-only to provisional
generator patch after seed `2828282828` repeated the wide-thigh bridge axis
across two visible women. When the baseline is already valid, a generator
patch is still appropriate if a later seed repeats a narrow atlas refinement
that improves geometry without harming subject/look, contact, or setting.
Generated-route turn `216` validated the patched formatter route with viewer
hands on outer thighs, wide foreground thigh bridge, upright torso, centered
contact, and coworking depth. Keep the route candidate until another
source/seed repeats the refinement.
- `2026-06-29`: Applied the category-exit rule to blowjob laying frontal after
source `46` and source `50` improved on sampler seed `6767676767`. When
baselines are already strong, preserve the exact improved axis: wide V-frame and low-horizontal torso hierarchy, while noting residual high-hip posture and
keeping the generator patch provisional until another seed repeats it.
- `2026-06-29`: Applied the category-exit rule to blowjob sitting upright after
source `46` and source `50` improved on sampler seed `7878787878`. When a
baseline preserves the seated pose but floats the face above the contact
point, prefer low-mouth seated hierarchy over generic `mouth aligned` wording:
face lowered to the exact center contact point, open mouth covering the
centered tip, and hands directly at the base. Record outfit looseness/drift as
residual risk and keep the generator patch provisional until another seed
repeats it.
File diff suppressed because it is too large Load Diff
+262
View File
@@ -0,0 +1,262 @@
# Krea2 POV Pose Atlas
Local reference root:
`/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2`
Use this dataset as the pose-geometry reference for POV prompt tuning. The pose
folders contain rendered POV examples; matching `_control` folders contain the
solo/control image for the same pose family. Ignore `bg` and `*_bg` folders for
pose analysis; they are background plates without people.
Machine-readable pose variants live in
`categories/krea2_pov_pose_variants.json`. That catalog is intentionally smaller
than the full atlas: it only contains variants that are proven or useful
candidates for fixed-seed Krea2 tuning. Add a variant there when it has a compact
geometry summary, cue phrases, avoid phrases, references, and a known generator
hook. Code should read it through `krea2_pose_variant_catalog.py` instead of
parsing the JSON directly.
In ComfyUI, use the `SxCP Krea2 Pose Variant` node when you want a workflow to
select one catalog variant and emit a compatible `hardcore_position_config` for
the existing Position Pool / Action Filter / Insta-OF chain. Pair it with
`SxCP Krea2 Variant Evidence` to display the fixed-seed eval entry, image paths,
and generator decision behind that variant.
For command-line planning, `python tools/krea2_tuning_report.py` shows which
catalog variants are proven or pending and which atlas pose folders are still
unmapped by the catalog. Unmapped folders include sample pose/control image
paths and a suggested candidate key to start the next catalog entry.
The `ready` folder name is misleading for prompt planning: it is mapped as
`pov_ejaculation_aftermath_open_thigh_candidate`, a post-ejaculation
open-thigh display family with thick visible fluid around the exposed opening,
not as a neutral setup pose.
## Inventory
| Family | Pose images | Control images | First sample |
| --- | ---: | ---: | --- |
| cowgirl | 63 | 63 | `5.cowgirl/100_cowgirl.png` |
| cowgirl alt | 62 | 62 | `5.cowgirl_alt/101_cowgirl_alt.png` |
| reverse cowgirl | 58 | 58 | `cowgirl_reverse/101_cowgirl_reverse.png` |
| reverse cowgirl alt | 50 | 50 | `cowgirl_reversere_alt/100_cowgirl_reversere_alt.png` |
| doggy | 57 | 57 | `doggy/101_doggy.png` |
| doggy alt | 45 | 45 | `doggy_alt/100_doggy_alt.png` |
| missionary | 74 | 74 | `missionary/101_missionary.png` |
| missionary folded | 12 | 12 | `missionary_folded/16_missionary_folded.png` |
| sixty-nine | 29 | 29 | `69/105_sixtynine.png` |
| ballsucking | 25 | 25 | `ballsucking/101_ballsucking.png` |
| blowjob laying | 42 | 42 | `blowjob_laying/101_blowjob_laying.png` |
| blowjob side | 17 | 17 | `blowjob_side/103_blowjob_side.png` |
| blowjob sitting | 27 | 27 | `blowjob_sitting/100_blowjob_sitting.png` |
| blowjob top view | 17 | 17 | `blowjob_top_view/102_blowjob_top_view.png` |
| boobjob | 11 | 11 | `boobjob/100_boobjob.png` |
| handjob | 24 | 24 | `handjob/18_handjob.png` |
| footjob | 2 | 2 | `footjob/59_footjob.png` |
| fingering | 10 | 10 | `fingering/103_fingering.png` |
| spread | 55 | 55 | `spread/100_spread_.png` |
| ready | 19 | 19 | `ready/105_ready_.png` |
| wand | 7 | 7 | `wand/106_wand_.png` |
## Tuning Method
For each pose family:
1. Sample 5-10 pose images and 2-3 control images.
2. Write a compact geometry summary using only repeated visual facts.
3. Test one prompt variant with a fixed seed.
4. Test the same wording on a second seed or character.
5. Patch generator defaults only when the wording improvement repeats or the
generated prompt is structurally wrong before rendering.
6. Record the evidence in `docs/krea2-prompt-guide.md`.
## Confirmed Notes
### Doggy / Rear-Entry
Dataset references show that visible POV thighs, lower torso, or pelvis can be
correct. They should be treated as natural foreground cues, not automatic
failures.
Better Krea2 wording:
- `top-down POV doggy position from behind`
- `camera looks down over the viewer's hands onto the woman's raised hips`
- `woman is on all fours with chest low, forearms folded, cheek turned sideways`
- `back arched, hips raised high toward the camera`
- `viewer hands hold her hips with natural lower-body POV cues in the foreground`
Avoid using visible shoes or lower legs as the standing cue. In seed `65`, that
wording pulled Krea2 toward oral contact and weakened rear-entry geometry.
### Boobjob / Titjob
The boobjob folder shows a repeated upright, frontal geometry rather than a
forward-bent one: the woman faces the viewer between his thighs, breasts pressed
together around a vertical shaft, with the glans above the cleavage near her
mouth. For Krea2, name hand ownership when hands matter. In POV prompts, generic
`hands` can become the viewer's hands.
### Handjob
The handjob folder repeats a centered first-person layout: the viewer's thighs
frame the lower edges, the woman faces the viewer between his legs, and her hand
is the contact anchor on the shaft. Prompt the woman's hand ownership directly;
viewer hands should not cover the action unless that is the intended variant.
## Candidate Notes
### Footjob
The footjob folder is small but visually consistent: the viewer reclines with
thighs framing the lower foreground, the penis is upright near the center, and
the woman's soles/toes are the contact anchor while her body and face remain
behind the feet. Treat `pov_footjob_frontal_sole_stroke` as a candidate until it
has fixed-seed Krea2 evidence.
### Fingering
The fingering folder repeats a first-person manual-contact layout: the woman is
reclined or sitting back with thighs spread wide toward camera, her face and
torso visible behind the open-leg frame, and the viewer hand entering from the
foreground as the contact anchor. Treat `pov_fingering_reclined_open_thighs` as
a candidate until it has fixed-seed Krea2 evidence.
### Wand / Toy Contact
The wand folder repeats a close first-person tool-contact layout: the woman is
reclined or sitting back with thighs spread toward camera, face and torso visible
behind the open-leg frame, and the viewer hand holding a wand-style toy from the
foreground with the rounded head pressed to the central contact point. Treat
`pov_wand_foreground_tool_contact` as a candidate until it has fixed-seed Krea2
evidence. Keep the visible hand/handle in the wording; otherwise Krea2 may float
the toy or transfer ownership to the visible partner.
### Ready / Post-Ejaculation Open-Thigh Display
The ready folder is not a neutral setup family. It repeats a first-person
post-ejaculation display pose: the woman reclines or sits back facing the viewer
with thighs spread open, face and torso readable behind the open-leg frame, a
viewer body cue or recently withdrawn foreground cue near the lower edge, and
thick semen or fluid visible around the exposed pussy or anal opening. Treat
`pov_ejaculation_aftermath_open_thigh_candidate` as a candidate until it has
fixed-seed Krea2 evidence. Avoid active thrusting wording here; the key state is
post-ejaculation fluid visibility, not penetration-in-progress.
### Spread / Open-Thigh Presentation
The spread folder is a setup/presentation family rather than a required contact
action: the woman faces the camera with legs raised or knees held wide, thighs
forming a wide V-frame, and her face and torso visible behind the open-leg pose.
Treat `pov_spread_open_thigh_presentation` as a candidate until it has
fixed-seed Krea2 evidence.
### Sixty-Nine / Close Reversed POV
The `69` folder repeats a close first-person mutual-oral layout rather than a
wide side-by-side pose: the visible partner is reversed over the viewer, hips
closest to camera, head and torso receding away into the upper frame, and the
viewer face or mouth anchoring the lower foreground. Treat
`pov_sixty_nine_close_reversed_oral` as the hardest and lowest-priority route in
the atlas for now. Do not queue it as a normal prompt-only fixed-seed candidate.
When exact geometry matters, prefer a pose/control image or a narrower
image-guided route; text alone can collapse this into generic oral contact or
lose the reversed-over-viewer body arrangement.
### Blowjob Top View
The `blowjob_top_view` folder repeats a top-down first-person oral layout: the
viewer looks down from chest or pelvis height, viewer torso or thighs sit at the
lower edge, the shaft is vertical and centered, and the woman kneels below
looking upward with mouth and hand aligned to it. Treat
`pov_blowjob_top_down_vertical_shaft` as a candidate until it has fixed-seed
Krea2 evidence.
### Blowjob Side
The `blowjob_side` folder repeats a side-profile first-person oral layout: the
viewer reclines with torso or thighs visible, the woman leans beside the
viewer's pelvis from the side, and her side-facing mouth aligns to the shaft
near the lower center of the frame. Treat `pov_blowjob_side_profile_oral` as a
candidate until it has fixed-seed Krea2 evidence.
### Blowjob Laying
The `blowjob_laying` folder repeats a frontal prone first-person oral layout:
the viewer reclines with open thighs framing the lower foreground, the woman
lies belly-down between the viewer's thighs, and her front-facing mouth and
hands align to a shaft rising from the lower center of the frame. Treat
`pov_blowjob_laying_frontal_oral` as a candidate until it has fixed-seed Krea2
evidence.
### Blowjob Sitting
The `blowjob_sitting` folder includes a few top-view outliers, but the named
sitting files repeat an upright seated first-person oral layout: the viewer
reclines with open thighs framing the lower foreground, the woman sits upright
between the viewer's thighs, and her close front-facing mouth aligns to a
vertical centered shaft. Treat `pov_blowjob_sitting_upright_oral` as a candidate
until it has fixed-seed Krea2 evidence.
### Missionary / Open-Leg Penetration
The `missionary` folder repeats a front-facing first-person penetration layout:
the woman reclines on her back facing the viewer, her knees open toward the
viewer, her face and torso stay visible behind the open-thigh frame, and the
viewer is positioned between her legs from the lower foreground. Treat
`pov_missionary_open_leg_penetration` as a candidate until it has fixed-seed
Krea2 evidence. Keep this separate from `missionary_folded`, where the legs are
pressed much higher and need different wording.
### Missionary Folded / High-Leg Penetration
The `missionary_folded` folder repeats a high-leg first-person penetration
layout: the woman reclines on her back facing the viewer, her knees are folded
high toward her chest, feet or ankles sit close to the camera, and the viewer's
hands often hold her calves or ankles while the contact line stays below the
raised legs. Treat `pov_missionary_folded_high_leg_penetration` as a candidate
until it has fixed-seed Krea2 evidence.
### Cowgirl / Frontal Straddle Penetration
The `5.cowgirl` folder repeats a frontal woman-on-top first-person layout: the
viewer reclines below, the woman straddles the viewer facing him, her torso
stays upright above the contact line, and her knees open to either side of the
viewer. Treat `pov_cowgirl_frontal_straddle_penetration` as a candidate until it
has fixed-seed Krea2 evidence. Keep this separate from the alt and reverse
cowgirl folders, which need their own geometry wording.
### Cowgirl Alt / Low Seated-Squat Penetration
The `5.cowgirl_alt` folder is still frontal woman-on-top, not reverse cowgirl,
but the repeated pose is lower and closer than the main cowgirl folder: the
woman faces the viewer in a low seated squat over the viewer's pelvis, knees
bent wide close to the camera, with viewer hands often anchoring the underside
of her thighs or hips. Treat `pov_cowgirl_alt_low_squat_penetration` as a
candidate until it has fixed-seed Krea2 evidence. Keep this separate from the
main cowgirl route so Krea2 can choose between upright straddle wording and
closer seated-squat wording.
### Reverse Cowgirl / Back-Facing Straddle Penetration
The `cowgirl_reverse` folder repeats a woman-on-top first-person layout where
the viewer reclines underneath and the woman faces away from the viewer. Her
back, hips, and ass are the closest readable body anchors, with her knees or
thighs planted to either side of the viewer's hips; her face may turn back over
one shoulder. Treat `pov_reverse_cowgirl_back_facing_penetration` as a
candidate until it has fixed-seed Krea2 evidence. Keep it separate from doggy:
the viewer is underneath her in a back-facing straddle, not kneeling behind her
while she is on all fours.
### Reverse Cowgirl Alt / Upright Back-Facing Straddle
The `cowgirl_reversere_alt` folder repeats an upright seated reverse-cowgirl
layout. The viewer reclines underneath, while the woman sits upright facing
away in a back-facing straddle; her back remains vertical and readable above
her hips, with viewer hands often holding her hips, thighs, wrists, or hands.
Treat `pov_reverse_cowgirl_alt_upright_back_facing_penetration` as a candidate
until it has fixed-seed Krea2 evidence. Keep it separate from
`pov_reverse_cowgirl_back_facing_penetration`, which can be closer and more
hip-cropped; this alt needs wording that preserves the vertical torso and
seated woman-on-top posture.
File diff suppressed because it is too large Load Diff
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+266
View File
@@ -0,0 +1,266 @@
# SxCP Eval Loop
This loop is for tuning the SxCP generator toward stronger Krea2 images.
ComfyUI sends a generated prompt, image, and seed to Codex, Codex analyzes the
result, then sends back exactly one edited prompt for the next A/B test.
Confirmed findings become either generator changes or durable prompt rules in
[`krea2-prompt-guide.md`](krea2-prompt-guide.md).
The active A/B testing method is recorded in
[`krea2-ab-methodology.md`](krea2-ab-methodology.md); update that memory when
the method improves.
## Channels
- `sxcp_eval_in`: ComfyUI to Codex. Contains the prompt text, image path, and
seed.
- `sxcp_eval_out`: Codex to ComfyUI. Prompt-only text plus the same seed through
the MCP signal when supported. Do not put analysis here.
- `sxcp_eval_log`: optional analysis/log channel.
## MCP Helper Command
Use the checked helper for bridge calls instead of ad hoc Python snippets. The
approved command prefix is:
```bash
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py
```
Common calls:
```bash
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py list-tools
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_pull --arguments-json '{"channel":"sxcp_eval_in"}'
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_push --arguments-json '{"channel":"sxcp_eval_out","seed":5656565656,"text":"PROMPT_ONLY_POSITIVE_CONDITIONING"}'
```
## Batch Prompt Helper
For prompt-axis batches, prepare a local JSON file and use the offline helper to
render the approved MCP push/pull commands and an image-presence checklist:
```bash
python tools/sxcp_prompt_batch.py validate --batch-json /tmp/sxcp-batch.json
python tools/sxcp_prompt_batch.py print-push-commands --batch-json /tmp/sxcp-batch.json
python tools/sxcp_prompt_batch.py print-result-template --batch-json /tmp/sxcp-batch.json
python tools/sxcp_prompt_batch.py run-batch --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --previous-turn 80 --run
python tools/sxcp_prompt_batch.py validate-results --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json
python tools/sxcp_prompt_batch.py print-eval-entry-draft --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --variant-key pov_example_variant --baseline-image /absolute/baseline.png --candidate-id controlled_subject_first
```
Batch files use the fixed sampler seed and one positive prompt per probe:
```json
{
"seed": 8989898989,
"channel_out": "sxcp_eval_out",
"channel_in": "sxcp_eval_in",
"probes": [
{
"id": "controlled_subject_first",
"prompt_order": "subject_first",
"text": "SUBJECT_LOOK_FIRST. POSE_HIERARCHY. LOCATION_ANCHORS."
},
{
"id": "rough_geometry_axis",
"prompt_order": "geometry_only",
"text": "POSE_AXIS_ONLY_FOR_DISCOVERY."
}
]
}
```
`geometry_only` probes are for rough pose-axis discovery and are not durable
subject/look-controlled A/B evidence. The helper rejects
`sxcp_eval_negative_out`; keep batch prompts positive-only.
Use `run-batch --run` to push one positive prompt, poll `sxcp_eval_in` until a
new turn and absolute PNG image path appear with the fixed sampler seed, write
the filled result JSON, then send the next probe. Omit `--run` for a dry-run
command preview. After a live run, run `validate-results`; it requires the
result probe ids to match the batch order, each turn to advance in batch order,
every image path to be an absolute PNG artifact, and every returned seed to
match the fixed sampler seed. Then use `print-eval-entry-draft` to create a
valid `krea2-eval-log.json` entry draft. Replace the generated summaries and
observation with the real visual comparison before recording it with
`tools/krea2_record_eval.py`. By default the draft command rejects
`geometry_only` candidates; pass `--allow-geometry-only` only when deliberately
recording non-controlled prompt-axis evidence.
## Manual Loop
Start the helper after sending a test prompt:
```bash
tools/sxcp_eval_loop.sh 3
```
Every three minutes it prints a structured request asking Codex to:
1. Pull `sxcp_eval_in`.
2. Record the emitted seed.
3. Inspect the image.
4. Compare it to the prompt and previous edit.
5. Push one prompt-only edit to `sxcp_eval_out`, preserving the same seed through
the MCP signal when available.
6. Classify the finding as prompt-only, prompt-guide rule, provisional generator
improvement, or proven generator fix.
7. When leaving a category after same-seed progress over baseline, mirror the
best generator-safe wording into the responsible generator path as
`provisional_generator_patch`.
8. Promote a generator change to proven only when the issue is systemic,
repeated, or structurally wrong before rendering.
9. Record the finding and update the Krea2 prompt guide when a rule is confirmed.
Runtime logs are written under `.sxcp_eval/` and ignored by git.
Durable fixed-seed findings that justify a guide rule, generator patch, or pose
variant promotion are recorded in [`krea2-eval-log.json`](krea2-eval-log.json).
Method changes belong in [`krea2-ab-methodology.md`](krea2-ab-methodology.md).
Use runtime logs for scratch notes; use the JSON log only for evidence that
should remain tied to a catalog variant. Image paths in that log point at
external ComfyUI artifacts and may be cleaned; the durable evidence is the fixed
sampler seed, optional generator seed, prompt summaries, observation, decision,
and commit.
Record durable findings with the checked helper instead of hand-editing the log:
```bash
python tools/krea2_record_eval.py --print-template --variant-key pov_footjob_frontal_sole_stroke --seed 1234 --generator-seed 5678 > /tmp/krea2-entry.json
python tools/krea2_record_eval.py --entry-json /tmp/krea2-entry.json --dry-run
python tools/krea2_record_eval.py --entry-json /tmp/krea2-entry.json
```
Entry template:
```json
{
"id": "variant-seed-short-finding",
"date": "2026-06-29",
"variant_key": "pov_example_variant",
"seed": 1234,
"generator_seed": 5678,
"source": "sxcp_eval_mcp",
"result": "accepted",
"decision": "generator_patch",
"baseline_prompt_summary": "What the generated prompt did before the edit.",
"candidate_prompt_summary": "What the edited prompt changed for the same seed.",
"observation": "What the image comparison proved and why it matters for the generator or guide.",
"baseline_image": "/absolute/path/to/baseline.png",
"candidate_image": "/absolute/path/to/candidate.png",
"commit": "pending"
}
```
To see catalog coverage and the next variants that still need controlled
testing, run:
```bash
python tools/krea2_tuning_report.py
```
The report includes atlas references plus prompt cues and avoid cues for the
next fixed-seed test candidate. It also shows the latest durable evidence for
variants that already have fixed-seed results, including the evidence id, seed,
decision, candidate prompt summary, and observation. For each normal next-test
candidate, it prints a `krea2_record_eval.py --print-template` command; replace
`<fixed_seed>` with the seed from the run you are recording.
## Optional Command Hook
If you have a one-shot Codex command you want to run automatically, set:
```bash
SXCP_EVAL_CODEX_CMD="codex exec" tools/sxcp_eval_loop.sh 3
```
The request is sent on stdin. The command also receives:
- `SXCP_EVAL_IN_CHANNEL`
- `SXCP_EVAL_OUT_CHANNEL`
- `SXCP_EVAL_LOG_CHANNEL`
- `SXCP_EVAL_GUIDE_FILE`
- `SXCP_EVAL_REQUEST_FILE`
- `SXCP_EVAL_CYCLE_DIR`
- `SXCP_EVAL_CYCLE`
## Evaluation Axes
- Identity consistency
- Outfit continuity
- Pose/action accuracy
- Camera compliance
- Location coherence
- Crop/framing
- Prompt noise/repetition
- Model confusion tokens
- Seed control/reproducibility
- Overall Krea2 image usefulness
## POV Pose Atlas
Use `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2` as the local
reference atlas for POV pose geometry. The top-level pose folders contain real
POV examples, and matching `_control` folders contain solo/control versions.
Ignore `bg` and `*_bg` folders for pose rules; they are background plates
without people. Treat the pose image folders as the primary source for body
geometry; captions are optional and are not present for every folder.
Suggested workflow:
1. Choose one pose family, for example `doggy`, `doggy_alt`, `cowgirl`, or
`missionary`.
2. Sample 5-10 real pose images and their control images.
3. Write the repeated geometry as a compact prompt rule.
4. Run one fixed-seed Krea2 prompt using that rule.
5. Repeat on a second seed or character before changing generator defaults.
6. If the prompt itself is structurally contradictory before rendering, patch
immediately and add a regression test.
For POV doggy, the atlas shows that visible viewer thighs, lower torso, or
pelvis can be correct. Do not treat them as automatic failures.
## Seed Contract
The sampler seed is transport metadata, not prompt text. When the graph emits a
sampler seed, an A/B wording test should reuse that exact seed so the image
difference mostly comes from wording, not sampling randomness. If the SxCP
generator/control seed differs from the sampler seed, record it as
`generator_seed` in the eval entry. If a payload has no sampler seed, mark that
cycle as uncontrolled and avoid turning the result into a durable generator rule
without another controlled run.
## Positive-Only Conditioning
`sxcp_eval_out` is positive conditioning only. Never send negative-conditioning
phrases such as `no shaft`, `no hands`, `without clothing`, or `avoid X` inside
the positive prompt; distilled Krea2 can reinforce or hallucinate the unwanted
object from that wording.
This loop has no active negative-output channel. A same-positive, same-seed
probe on seed `424242` compared empty negative conditioning against strong
negative text targeting visible prompt attributes, and the rendered image stayed
visually unchanged. Do not rely on negative conditioning for Krea2 pose tuning;
keep prompt fixes positive-only.
## Generator Fix Rule
Use two levels of generator change:
- `provisional_generator_patch`: apply the best generator-safe wording when
leaving a category after fixed-seed progress over baseline. Keep the catalog
variant as `candidate`.
- `generator_patch`: promote as a proven/default generator rule when the issue
is repeated, systemic, or structurally wrong before rendering.
Examples of proven generator fixes:
- Selfie wording overrides orbit camera.
- Clothing continuity loses the selected softcore outfit.
- POV wording makes the off-camera participant the visual subject.
- Location camera layout inserts foreground anchors in the wrong place.
For one-off model drift inside an active category, send a cleaner prompt to
`sxcp_eval_out` and keep collecting evidence. When exiting a category, carry
forward same-seed improvements over baseline as provisional generator changes
and add the rule or weak case to `docs/krea2-prompt-guide.md`.
+33 -5
View File
@@ -39,8 +39,11 @@ HARDCORE_POSITION_FOCUS_CHOICES = [
]
HARDCORE_POSITION_KEY_CHOICES = [
"missionary",
"missionary_folded",
"cowgirl",
"cowgirl_alt",
"reverse_cowgirl",
"reverse_cowgirl_alt",
"doggy",
"bent_over",
"face_down_ass_up",
@@ -123,8 +126,11 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
}
HARDCORE_POSITION_KEY_MATCHES = {
"missionary": ("missionary", "above her", "under her"),
"missionary_folded": ("folded missionary", "knees-to-chest", "knees to chest", "folded legs", "folded high"),
"cowgirl": ("cowgirl", "straddling", "straddles", "on top", "squatting on top"),
"cowgirl_alt": ("cowgirl-alt", "low cowgirl", "seated-squat cowgirl", "low seated squat"),
"reverse_cowgirl": ("reverse cowgirl", "facing away"),
"reverse_cowgirl_alt": ("reverse cowgirl alt", "upright reverse cowgirl", "upright back-facing straddle"),
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
"bent_over": ("bent-over", "bent over", "hips raised"),
"face_down_ass_up": ("face-down", "ass-up"),
@@ -168,6 +174,28 @@ HARDCORE_POSITION_KEY_MATCHES = {
"open_thighs": ("thighs open", "legs spread", "open thighs", "legs open", "reclining with thighs open"),
"front_back": ("front-and-back", "front and back", "one behind and one in front", "between two partners"),
}
def _text_matches_position_key(text: str, position: str) -> bool:
terms = HARDCORE_POSITION_KEY_MATCHES.get(position, ())
if not any(term in text for term in terms):
return False
if position == "missionary" and any(term in text for term in HARDCORE_POSITION_KEY_MATCHES["missionary_folded"]):
return False
if position == "cowgirl" and any(
term in text
for term in (
HARDCORE_POSITION_KEY_MATCHES["cowgirl_alt"]
+ HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl"]
+ HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl_alt"]
)
):
return False
if position == "reverse_cowgirl" and any(term in text for term in HARDCORE_POSITION_KEY_MATCHES["reverse_cowgirl_alt"]):
return False
return True
HARDCORE_POSITION_AXIS_KEYS = {
"position",
"body_position",
@@ -733,7 +761,7 @@ def hardcore_position_entry_matches(entry: Any, config: dict[str, Any]) -> bool:
return bool(set(metadata_keys) & set(positions))
text = _entry_text(entry).lower()
for position in positions:
if any(term in text for term in HARDCORE_POSITION_KEY_MATCHES.get(position, ())):
if _text_matches_position_key(text, position):
return True
return False
@@ -749,8 +777,8 @@ def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> boo
text = _entry_text(entry).lower()
matched = {
position
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
if any(term in text for term in terms)
for position in HARDCORE_POSITION_KEY_MATCHES
if _text_matches_position_key(text, position)
}
return bool(matched) and not bool(matched & selected)
@@ -861,8 +889,8 @@ def hardcore_position_keys(*parts: Any, axis_values: dict[str, Any] | None = Non
if not text:
return []
keys: list[str] = []
for key, tokens in HARDCORE_POSITION_KEY_MATCHES.items():
if any(token in text for token in tokens):
for key in HARDCORE_POSITION_KEY_MATCHES:
if _text_matches_position_key(text, key):
keys.append(key)
return keys
+5 -2
View File
@@ -43,7 +43,10 @@ def build_climax_role_graph(
if "lying at the bed edge with thighs open" in context:
return f"{woman} lies at the bed edge with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs."
if "reclining with thighs open" in context or "lying on the back with legs spread" in context:
return f"{woman} lies on her back with thighs open while {man} kneels between her legs and ejaculates semen across her pussy and thighs."
return (
f"{woman} lies on her back with thighs open after ejaculation; thick semen and clear fluid cover her exposed pussy "
f"and inner thighs as the exact-center aftermath detail, her body stays still, and her face and torso remain visible behind the open thighs."
)
if "on all fours with hips raised" in context:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and ejaculates semen across her ass, thighs, and lower back."
if "face-down ass-up" in context:
@@ -53,7 +56,7 @@ def build_climax_role_graph(
if "kneeling with mouth open" in context:
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
if "kneeling in front of a standing partner" in context:
return f"{woman} kneels in front of {man} at hip height while {man} stands over her for visible ejaculation."
return f"{woman} kneels in front of {man} at hip height while {man} stands over her and ejaculates semen across her body."
if "standing with cum on the body" in context:
return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body."
if "squatting on top of a partner" in context:
+24 -4
View File
@@ -58,11 +58,25 @@ def build_manual_role_graph(
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 "toy" in text or "vibrator" in text or "wand" in text:
return (
f"{primary} reclines in a close first-person toy-contact view with thighs spread wide toward the camera; "
"a single continuous teal wand-style massager is the largest lower-frame object, "
"the rounded bulb head presses flat to her vulva and clit as the central contact point, "
f"and the smooth handle angles in from the bottom right inside {partner}'s visible hand. "
f"{primary}'s open thighs and knees form a V around the foreground wand while her face and torso remain visible behind the leg frame."
)
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."
return (
f"{primary} reclines with thighs open while {partner}'s foreground hand is the largest lower-frame object; "
f"her open thighs form a V around the hand, the wrist enters from the bottom center, "
f"and two fingers press at her vulva and clit as the clear contact point while her face and torso remain visible behind the open thighs."
)
return (
f"{primary} reclines with thighs open while {partner}'s foreground hand is the largest lower-frame object; "
f"her open thighs form a V around the hand, the wrist enters from the bottom center, "
f"and two fingers at her vulva and clit make the central manual-contact point while her face and torso remain visible behind the open thighs."
)
def build_interaction_role_graph(
@@ -79,6 +93,12 @@ def build_interaction_role_graph(
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 any(term in text for term in ("spread open", "open thighs", "thighs open", "legs spread", "knees held wide")):
return (
f"{primary} sits back facing the camera with knees raised and held wide; "
f"her thighs form a broad V-frame around her centered exposed vulva, "
f"her hands hold her knees and upper thighs, and her face and torso remain visible behind the open-thigh frame."
)
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."
+8 -1
View File
@@ -83,7 +83,14 @@ def build_oral_role_graph(
if "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
if woman_gives and not man_gives:
if man_is_pov:
return f"The viewer lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes the viewer's penis in her mouth."
return (
f"The adult male viewer reclines in a first-person side-profile body-line view with the adult male viewer's abdomen, "
"navel, pelvis, and near thigh creating a broad horizontal body surface; the adult male viewer's own torso starts at the lower edge "
f"and runs diagonally into the lower-right foreground, with navel, abdomen hair, pelvis, and near thigh marking the camera owner's body; {woman} enters laterally from the left edge beside his hip, "
"cheek and jaw in profile, mouth on the shaft at the male abdomen line, lips touching the shaft at the male abdomen line, "
"mouth-to-shaft contact is the nearest facial detail, hand around the base under her lips, "
"shoulder and torso trailing sideways along the edge."
)
return f"{man} lies on his side with hips angled toward {woman} while {woman} lies beside his thighs and takes his penis in her mouth."
if man_is_pov:
return f"{woman} lies on her side with her top thigh lifted while the viewer lies beside her hips with his mouth pressed to her pussy."
+30 -8
View File
@@ -42,15 +42,34 @@ def build_outercourse_role_graph(
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
if man_is_pov:
return (
f"{woman} bends forward and kneels very low between the POV viewer's open thighs with her shoulders between his knees, "
"her face below the POV viewer's penis at testicle height, mouth and tongue on the POV viewer's balls, "
"while his penis points upward in the lower foreground above her forehead."
f"{woman} lies low in a side-pelvis POV beside the POV viewer's open thighs, "
"her face is the closest visible partner part, her cheek against the POV viewer's inner thigh and her head low under his pelvis, "
"with the POV viewer's scrotum at her mouth; scrotum is the mouth surface, testicles resting across her open lips while her tongue cups them from below, "
"scrotal skin is the nearest mouth surface and both testicles rest against her tongue from below, "
"and his abdomen and inner thighs frame the close foreground."
)
return (
f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, "
f"{woman}'s face below {man}'s penis at testicle height, mouth and tongue on his balls, while {man}'s penis points upward above her forehead."
f"{man} reclines with legs apart while {woman} lies low beside his inner thigh, "
f"her face as the closest visible partner part, her cheek against his thigh and her head low under his pelvis, "
f"with {man}'s scrotum at her mouth; scrotum is the mouth surface, scrotal skin is the nearest mouth surface, and testicles resting across her open lips while both testicles rest against her tongue from below."
)
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
prone_laying = any(
term in f"{position_text} {text}"
for term in ("reclining", "prone", "belly-down", "belly down", "lying")
)
if prone_laying:
if man_is_pov:
return (
f"{woman} lies belly-down between the POV viewer's open thighs while his thighs form a wide V-frame in the foreground, "
"her torso stretched low and horizontal between his knees, her front-facing mouth and tongue aligned to the POV viewer's penis, "
"and her hands wrap the base of the POV viewer's penis."
)
return (
f"{woman} lies belly-down between {man}'s open thighs while his thighs form a wide V-frame in the foreground, "
f"her torso stretched low and horizontal between his knees, her front-facing mouth and tongue aligned to {man}'s penis, "
f"and her hands wrap the base of {man}'s penis."
)
if man_is_pov:
return (
f"{woman} bends forward between the POV viewer's open thighs with her head low under the POV viewer's penis, "
@@ -65,12 +84,15 @@ def build_outercourse_role_graph(
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
if man_is_pov:
return (
f"{woman} faces the POV viewer with her hips back, torso visible behind her raised legs, and both knees bent open toward the camera, "
"both soles wrapped around the POV viewer's penis shaft in the lower foreground."
f"{woman} faces the POV viewer with hips back, torso visible behind her raised legs, and both knees bent open toward the camera, "
"while two large overlapping soles dominate the POV viewer's lower center foreground and clamp the POV viewer's upright shaft between them. "
"Her inner arches press inward from both sides, toes curl around both edges, a narrow visible strip of shaft and glans rises between the compressed feet, "
"and her face and torso stay visible behind the large foreground feet."
)
return (
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"two large overlapping soles dominating the lower center foreground as her inner arches press inward around {man}'s upright shaft, "
"her toes curl around both edges, and a narrow visible strip of shaft and glans rises between the compressed feet."
)
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
if man_is_pov:
+20 -1
View File
@@ -19,15 +19,34 @@ def build_penetration_role_graph(
item_axis_values: dict[str, Any] | None = None,
) -> str:
text = _context_text(item_text, item_axis_values)
if "folded missionary" in text or "knees-to-chest" in text or "knees to chest" in text:
return (
f"{woman} lies on her back facing {man} with knees folded high toward her chest while {man} is above her between her thighs; "
f"{man}'s hands hold her calves and {man}'s penis thrusts into her pussy below the raised knees."
)
if "missionary" in text:
return (
f"{woman} lies on her back with legs open around {man}'s hips while {man} is above her between her thighs; "
f"{man}'s hips press close and {man}'s penis thrusts into her pussy."
)
if "cowgirl-alt" in text or "low cowgirl" in text or "seated-squat cowgirl" in text or "low seated squat" in text:
return (
f"{woman} faces {man} in a low seated squat over {man}'s pelvis while {man} lies flat on his back under her; "
f"{man} supports the underside of her thighs and {man}'s penis thrusts into her pussy."
)
if "reverse cowgirl alt" in text or "upright reverse cowgirl" in text or "upright back-facing straddle" in text:
return (
f"{woman} sits upright facing away in a back-facing straddle over {man}'s pelvis while {man} lies under her; "
f"{man}'s hands hold her hips and {man}'s penis thrusts into her pussy."
)
if "reverse cowgirl" in text:
return f"{woman} straddles {man}'s hips facing away while {man} lies under her and {man}'s penis thrusts into her pussy."
if "cowgirl" in text or "straddling" in text:
return f"{woman} straddles {man}'s hips facing him while {man} lies under her and {man}'s penis thrusts into her pussy."
return (
f"{woman} straddles {man}'s hips facing him while {man} lies under her; "
f"{man}'s lower abdomen and pelvis anchor the bottom edge, {woman}'s thighs form a wide horizontal thigh bridge from left edge to right edge, "
f"her knees plant outside {man}'s hips, {man}'s hands grip the sides of her thighs, and centered contact remains below her belly as {man}'s penis thrusts into her pussy."
)
if "doggy" in text or "rear-entry" in text or "bent-over" in text or "bent over" in text:
return f"{woman} is on all fours with hips raised while {man} is positioned behind her and {man}'s penis thrusts into her pussy."
if "standing" in text:
+235
View File
@@ -0,0 +1,235 @@
from __future__ import annotations
import copy
import json
from functools import lru_cache
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
DEFAULT_EVAL_LOG_PATH = ROOT / "docs" / "krea2-eval-log.json"
VALID_RESULTS = {"accepted", "rejected", "inconclusive"}
VALID_DECISIONS = {
"generator_patch",
"provisional_generator_patch",
"prompt_guide_rule",
"prompt_only_retry",
"needs_more_tests",
}
def _path_key(path: str | Path | None = None) -> str:
return str(Path(path or DEFAULT_EVAL_LOG_PATH).resolve())
@lru_cache(maxsize=8)
def _load_raw_eval_log(path_key: str) -> dict[str, Any]:
with Path(path_key).open("r", encoding="utf-8") as handle:
data = json.load(handle)
return data if isinstance(data, dict) else {}
def clear_cache() -> None:
_load_raw_eval_log.cache_clear()
def load_eval_log(path: str | Path | None = None) -> dict[str, Any]:
return copy.deepcopy(_load_raw_eval_log(_path_key(path)))
def _text(value: Any) -> str:
return value if isinstance(value, str) else ""
def _require_text(errors: list[str], entry: dict[str, Any], key: str, min_len: int) -> None:
value = _text(entry.get(key)).strip()
if len(value) < min_len:
errors.append(f"{key} must be at least {min_len} characters")
def _entry_id_slug(variant_key: str) -> str:
value = variant_key.removeprefix("pov_")
chars = [char.lower() if char.isalnum() else "-" for char in value]
slug = "".join(chars).strip("-")
while "--" in slug:
slug = slug.replace("--", "-")
return slug or "krea2-eval"
def entry_template(
variant_key: str,
*,
seed: int,
generator_seed: int | None = None,
source: str = "sxcp_eval_mcp",
date: str = "",
result: str = "inconclusive",
decision: str = "needs_more_tests",
commit: str = "pending",
) -> dict[str, Any]:
if not isinstance(seed, int) or isinstance(seed, bool):
raise ValueError("seed must be an integer")
if generator_seed is not None and (not isinstance(generator_seed, int) or isinstance(generator_seed, bool)):
raise ValueError("generator_seed must be an integer")
variant = _text(variant_key).strip()
if not variant:
raise ValueError("variant_key is required")
entry = {
"id": f"{_entry_id_slug(variant)}-{seed}-eval",
"date": date,
"variant_key": variant,
"seed": seed,
"source": source,
"result": result,
"decision": decision,
"baseline_prompt_summary": f"Replace this with what the generated {variant} prompt did before the edit.",
"candidate_prompt_summary": f"Replace this with what the same-seed candidate prompt changed for {variant}.",
"observation": f"Replace this with the fixed-seed Krea2 image comparison observation for {variant}.",
"baseline_image": "",
"candidate_image": "",
"commit": commit,
}
if generator_seed is not None:
entry["generator_seed"] = generator_seed
return entry
def validate_entry(
entry: dict[str, Any],
*,
existing_entries: list[dict[str, Any]] | None = None,
catalog_keys: set[str] | None = None,
) -> list[str]:
errors: list[str] = []
if not isinstance(entry, dict):
return ["entry must be an object"]
_require_text(errors, entry, "id", 6)
entry_id = _text(entry.get("id")).strip()
if entry_id and existing_entries:
existing_ids = {_text(row.get("id")).strip() for row in existing_entries if isinstance(row, dict)}
if entry_id in existing_ids:
errors.append(f"duplicate id {entry_id!r}")
_require_text(errors, entry, "variant_key", 8)
variant_key = _text(entry.get("variant_key")).strip()
if variant_key and catalog_keys is not None and variant_key not in catalog_keys:
errors.append(f"unknown variant {variant_key!r}")
seed = entry.get("seed")
if not isinstance(seed, int) or isinstance(seed, bool):
errors.append("seed must be an integer")
generator_seed = entry.get("generator_seed")
if generator_seed is not None and (not isinstance(generator_seed, int) or isinstance(generator_seed, bool)):
errors.append("generator_seed must be an integer")
result = entry.get("result")
if result not in VALID_RESULTS:
errors.append(f"result must be one of {sorted(VALID_RESULTS)}")
decision = entry.get("decision")
if decision not in VALID_DECISIONS:
errors.append(f"decision must be one of {sorted(VALID_DECISIONS)}")
_require_text(errors, entry, "baseline_prompt_summary", 20)
_require_text(errors, entry, "candidate_prompt_summary", 20)
_require_text(errors, entry, "observation", 30)
for image_key in ("baseline_image", "candidate_image"):
image_path = _text(entry.get(image_key)).strip()
if not image_path:
continue
path = Path(image_path)
if not path.is_absolute():
errors.append(f"{image_key} must be absolute when present")
if path.suffix.lower() != ".png":
errors.append(f"{image_key} must reference a PNG artifact")
return errors
def save_eval_log(log: dict[str, Any], *, path: str | Path | None = None) -> None:
target = Path(path or DEFAULT_EVAL_LOG_PATH)
target.write_text(json.dumps(log, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
clear_cache()
def append_entry(
entry: dict[str, Any],
*,
path: str | Path | None = None,
catalog_path: str | Path | None = None,
dry_run: bool = False,
) -> dict[str, Any]:
try:
from . import krea2_pose_variant_catalog
except ImportError: # Allows local smoke tests from the repository root.
import krea2_pose_variant_catalog
log = load_eval_log(path)
rows = log.get("entries")
if not isinstance(rows, list):
rows = []
log["entries"] = rows
new_entry = copy.deepcopy(entry)
errors = validate_entry(
new_entry,
existing_entries=[row for row in rows if isinstance(row, dict)],
catalog_keys=set(krea2_pose_variant_catalog.variant_keys(path=catalog_path)),
)
if errors:
raise ValueError("; ".join(errors))
rows.append(new_entry)
if not dry_run:
save_eval_log(log, path=path)
return copy.deepcopy(log)
def entries(
*,
variant_key: str | None = None,
result: str | None = None,
decision: str | None = None,
path: str | Path | None = None,
) -> list[dict[str, Any]]:
log = load_eval_log(path)
rows = log.get("entries") or []
if not isinstance(rows, list):
return []
filtered: list[dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
if variant_key is not None and row.get("variant_key") != variant_key:
continue
if result is not None and row.get("result") != result:
continue
if decision is not None and row.get("decision") != decision:
continue
filtered.append(row)
return filtered
def entries_for_variant(
variant_key: str,
*,
result: str | None = None,
decision: str | None = None,
path: str | Path | None = None,
) -> list[dict[str, Any]]:
return entries(variant_key=variant_key, result=result, decision=decision, path=path)
def variant_keys(
*,
result: str | None = None,
decision: str | None = None,
path: str | Path | None = None,
) -> list[str]:
keys: list[str] = []
for row in entries(result=result, decision=decision, path=path):
key = row.get("variant_key")
if key and key not in keys:
keys.append(str(key))
return keys
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
import copy
import json
from functools import lru_cache
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parent
DEFAULT_CATALOG_PATH = ROOT / "categories" / "krea2_pov_pose_variants.json"
def _path_key(path: str | Path | None = None) -> str:
return str(Path(path or DEFAULT_CATALOG_PATH).resolve())
@lru_cache(maxsize=8)
def _load_raw_catalog(path_key: str) -> dict[str, Any]:
with Path(path_key).open("r", encoding="utf-8") as handle:
data = json.load(handle)
return data if isinstance(data, dict) else {}
def clear_cache() -> None:
_load_raw_catalog.cache_clear()
def load_catalog(path: str | Path | None = None) -> dict[str, Any]:
return copy.deepcopy(_load_raw_catalog(_path_key(path)))
def variants(
*,
status: str | None = None,
family: str | None = None,
action_family: str | None = None,
path: str | Path | None = None,
) -> list[dict[str, Any]]:
catalog = load_catalog(path)
rows = catalog.get("variants") or []
if not isinstance(rows, list):
return []
filtered: list[dict[str, Any]] = []
for row in rows:
if not isinstance(row, dict):
continue
if status is not None and row.get("status") != status:
continue
if family is not None and row.get("family") != family:
continue
if action_family is not None and row.get("action_family") != action_family:
continue
filtered.append(row)
return filtered
def variant_keys(
*,
status: str | None = None,
family: str | None = None,
action_family: str | None = None,
path: str | Path | None = None,
) -> list[str]:
return [
str(row.get("key"))
for row in variants(status=status, family=family, action_family=action_family, path=path)
if row.get("key")
]
def get_variant(key: str, *, path: str | Path | None = None) -> dict[str, Any]:
for row in variants(path=path):
if row.get("key") == key:
return row
return {}
def reference_paths(key: str, *, path: str | Path | None = None) -> list[Path]:
catalog = load_catalog(path)
atlas_root = Path(str(catalog.get("atlas_root") or ""))
variant = get_variant(key, path=path)
refs = variant.get("reference_images") or []
if not isinstance(refs, list):
return []
paths: list[Path] = []
for ref in refs:
ref_path = Path(str(ref))
if ".." in ref_path.parts:
continue
paths.append(atlas_root / ref_path)
return paths
+426
View File
@@ -0,0 +1,426 @@
from __future__ import annotations
from collections import Counter
from pathlib import Path
import sys
from typing import Any
try:
from . import krea2_eval_log, krea2_pose_variant_catalog
except ImportError: # Allows local smoke tests from the repository root.
import krea2_eval_log
import krea2_pose_variant_catalog
def _coverage_state(status: str, accepted_count: int) -> str:
if status == "proven" and accepted_count > 0:
return "proven_with_evidence"
if status == "proven":
return "proven_missing_evidence"
if status == "candidate" and accepted_count == 0:
return "needs_fixed_seed_tests"
if status == "unstable":
return "needs_stronger_control"
return "tracked"
def _latest_evidence(entries: list[dict[str, Any]], *, result: str | None = None) -> dict[str, Any]:
filtered = [entry for entry in entries if result is None or entry.get("result") == result]
if not filtered:
return {}
entry = filtered[-1]
return {
"id": entry.get("id") or "",
"seed": entry.get("seed"),
"generator_seed": entry.get("generator_seed"),
"result": entry.get("result") or "",
"decision": entry.get("decision") or "",
"baseline_prompt_summary": entry.get("baseline_prompt_summary") or "",
"candidate_prompt_summary": entry.get("candidate_prompt_summary") or "",
"observation": entry.get("observation") or "",
"needs_expansion": bool(entry.get("needs_expansion")),
"commit": entry.get("commit") or "",
}
def coverage_rows() -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for variant in krea2_pose_variant_catalog.variants():
key = str(variant.get("key") or "")
evidence = krea2_eval_log.entries_for_variant(key)
accepted = [entry for entry in evidence if entry.get("result") == "accepted"]
status = str(variant.get("status") or "")
rows.append(
{
"key": key,
"family": variant.get("family") or "",
"action_family": variant.get("action_family") or "",
"status": status,
"difficulty": variant.get("difficulty") or "",
"priority": variant.get("priority") or "",
"control_requirement": variant.get("control_requirement") or "",
"coverage_state": _coverage_state(status, len(accepted)),
"accepted_evidence_count": len(accepted),
"total_evidence_count": len(evidence),
"latest_evidence": _latest_evidence(evidence),
"latest_accepted_evidence": _latest_evidence(evidence, result="accepted"),
"reference_count": len(variant.get("reference_images") or []),
"guide_section": (variant.get("evidence") or {}).get("guide_section", ""),
}
)
return rows
def coverage_summary() -> dict[str, Any]:
rows = coverage_rows()
status_counts = Counter(row.get("status") for row in rows)
state_counts = Counter(row.get("coverage_state") for row in rows)
return {
"variant_count": len(rows),
"status_counts": dict(status_counts),
"coverage_state_counts": dict(state_counts),
"variants_without_accepted_evidence": [
str(row.get("key"))
for row in rows
if int(row.get("accepted_evidence_count") or 0) == 0
],
"next_test_candidates": [
str(row.get("key"))
for row in rows
if row.get("coverage_state") in {"needs_fixed_seed_tests", "proven_missing_evidence"}
],
"stronger_control_cases": [
str(row.get("key"))
for row in rows
if row.get("coverage_state") == "needs_stronger_control"
],
}
def _catalog_atlas_root() -> Path:
catalog = krea2_pose_variant_catalog.load_catalog()
return Path(str(catalog.get("atlas_root") or ""))
def _mapped_atlas_folders() -> dict[str, list[str]]:
mapped: dict[str, list[str]] = {}
for variant in krea2_pose_variant_catalog.variants():
key = str(variant.get("key") or "")
for folder in variant.get("atlas_folders") or []:
folder_name = str(folder)
if not folder_name:
continue
mapped.setdefault(folder_name, []).append(key)
return mapped
def _is_background_or_control_folder(folder_name: str) -> bool:
lower = folder_name.lower()
return (
lower == "bg"
or lower == "woman"
or lower.endswith("_control")
or lower.endswith("_bg")
or lower.endswith("_control_bg")
)
def _sample_pngs(folder: Path, limit: int) -> list[str]:
if not folder.is_dir() or limit <= 0:
return []
return [str(path) for path in sorted(folder.glob("*.png"), key=lambda path: path.name.lower())[:limit]]
def atlas_folder_rows(atlas_root: str | Path | None = None) -> list[dict[str, Any]]:
root = Path(atlas_root) if atlas_root is not None else _catalog_atlas_root()
if not root.is_dir():
return []
mapped = _mapped_atlas_folders()
rows: list[dict[str, Any]] = []
for folder in sorted(root.iterdir(), key=lambda path: path.name.lower()):
if not folder.is_dir():
continue
folder_name = folder.name
if _is_background_or_control_folder(folder_name):
continue
image_count = sum(1 for _ in folder.glob("*.png"))
if image_count <= 0:
continue
control_folder = root / f"{folder_name}_control"
variant_keys = mapped.get(folder_name, [])
if not variant_keys and not control_folder.is_dir():
continue
rows.append(
{
"folder": folder_name,
"image_count": image_count,
"mapped": bool(variant_keys),
"variant_keys": list(variant_keys),
"control_folder": str(control_folder) if control_folder.is_dir() else "",
}
)
return rows
def atlas_coverage_summary(atlas_root: str | Path | None = None) -> dict[str, Any]:
rows = atlas_folder_rows(atlas_root=atlas_root)
unmapped = [str(row.get("folder")) for row in rows if not row.get("mapped")]
return {
"pose_folder_count": len(rows),
"mapped_folder_count": len(rows) - len(unmapped),
"unmapped_folder_count": len(unmapped),
"unmapped_folders": unmapped,
}
def _suggested_variant_key(folder_name: str) -> str:
if folder_name.lower() == "ready":
return "pov_ejaculation_aftermath_open_thigh_candidate"
normalized = "".join(char if char.isalnum() else "_" for char in folder_name.lower()).strip("_")
while "__" in normalized:
normalized = normalized.replace("__", "_")
return f"pov_{normalized}_candidate" if normalized else "pov_unmapped_candidate"
def atlas_gap_plans(atlas_root: str | Path | None = None, sample_limit: int = 3) -> list[dict[str, Any]]:
root = Path(atlas_root) if atlas_root is not None else _catalog_atlas_root()
plans: list[dict[str, Any]] = []
for row in atlas_folder_rows(atlas_root=root):
if row.get("mapped"):
continue
folder_name = str(row.get("folder") or "")
folder_path = root / folder_name
control_folder = Path(str(row.get("control_folder") or ""))
plans.append(
{
"folder": folder_name,
"suggested_variant_key": _suggested_variant_key(folder_name),
"image_count": int(row.get("image_count") or 0),
"sample_images": _sample_pngs(folder_path, sample_limit),
"control_images": _sample_pngs(control_folder, sample_limit),
}
)
return plans
def next_test_plans() -> list[dict[str, Any]]:
rows_by_key = {str(row.get("key")): row for row in coverage_rows()}
plans: list[dict[str, Any]] = []
for key in coverage_summary()["next_test_candidates"]:
variant = krea2_pose_variant_catalog.get_variant(key)
if not variant:
continue
row = rows_by_key.get(key, {})
evidence = variant.get("evidence") or {}
plans.append(
{
"key": key,
"family": variant.get("family") or "",
"action_family": variant.get("action_family") or "",
"status": variant.get("status") or "",
"coverage_state": row.get("coverage_state") or "",
"canonical_geometry": variant.get("canonical_geometry") or "",
"prompt_cues": list(variant.get("prompt_cues") or []),
"avoid_cues": list(variant.get("avoid_cues") or []),
"reference_paths": [str(path) for path in krea2_pose_variant_catalog.reference_paths(key)],
"generator_hook": variant.get("generator_hook") or {},
"guide_section": evidence.get("guide_section") or "",
"notes": evidence.get("notes") or "",
}
)
return plans
def guide_expansion_plans() -> list[dict[str, Any]]:
plans: list[dict[str, Any]] = []
for row in coverage_rows():
latest_accepted = row.get("latest_accepted_evidence") or {}
decision = str(latest_accepted.get("decision") or "")
if decision not in {"prompt_guide_rule", "needs_more_tests"} and not (
decision == "provisional_generator_patch" and latest_accepted.get("needs_expansion")
):
continue
key = str(row.get("key") or "")
variant = krea2_pose_variant_catalog.get_variant(key)
if not variant:
continue
evidence = variant.get("evidence") or {}
plans.append(
{
"key": key,
"family": variant.get("family") or "",
"action_family": variant.get("action_family") or "",
"status": variant.get("status") or "",
"coverage_state": row.get("coverage_state") or "",
"target": "multi_seed_multi_woman_matrix",
"latest_accepted_id": latest_accepted.get("id") or "",
"latest_accepted_seed": latest_accepted.get("seed"),
"latest_accepted_decision": decision,
"accepted_evidence_count": row.get("accepted_evidence_count") or 0,
"total_evidence_count": row.get("total_evidence_count") or 0,
"canonical_geometry": variant.get("canonical_geometry") or "",
"prompt_cues": list(variant.get("prompt_cues") or []),
"avoid_cues": list(variant.get("avoid_cues") or []),
"reference_paths": [str(path) for path in krea2_pose_variant_catalog.reference_paths(key)],
"generator_hook": variant.get("generator_hook") or {},
"guide_section": evidence.get("guide_section") or "",
"notes": evidence.get("notes") or "",
}
)
return plans
def next_eval_template_commands(*, seed_token: str = "<fixed_seed>") -> list[dict[str, str]]:
commands: list[dict[str, str]] = []
for plan in next_test_plans():
key = str(plan.get("key") or "")
if not key:
continue
commands.append(
{
"key": key,
"command": f"python tools/krea2_record_eval.py --print-template --variant-key {key} --seed {seed_token}",
}
)
return commands
def markdown_report(atlas_root: str | Path | None = None) -> str:
lines = [
"# Krea2 Pose Variant Coverage",
"",
"| Variant | Status | Evidence | State |",
"| --- | --- | ---: | --- |",
]
for row in coverage_rows():
lines.append(
f"| {row['key']} | {row['status']} | {row['accepted_evidence_count']}/{row['total_evidence_count']} | {row['coverage_state']} |"
)
evidence_rows = [row for row in coverage_rows() if row.get("latest_evidence")]
if evidence_rows:
lines.extend(["", "## Latest Evidence", ""])
for row in evidence_rows:
evidence = row.get("latest_evidence") or {}
seed = evidence.get("seed")
seed_text = f"seed {seed}" if isinstance(seed, int) else "seed unknown"
generator_seed = evidence.get("generator_seed")
generator_seed_text = f", generator seed {generator_seed}" if isinstance(generator_seed, int) else ""
commit = evidence.get("commit") or "uncommitted"
lines.append(
f"- {row['key']}: {evidence.get('id') or 'unnamed'} ({evidence.get('result') or 'unknown'}, {seed_text}{generator_seed_text}, {evidence.get('decision') or 'unknown'}, commit {commit})"
)
if evidence.get("candidate_prompt_summary"):
lines.append(f" Candidate: {evidence['candidate_prompt_summary']}")
if evidence.get("observation"):
lines.append(f" Observation: {evidence['observation']}")
accepted = row.get("latest_accepted_evidence") or {}
if accepted and accepted.get("id") != evidence.get("id"):
accepted_seed = accepted.get("seed")
accepted_seed_text = f"seed {accepted_seed}" if isinstance(accepted_seed, int) else "seed unknown"
accepted_generator_seed = accepted.get("generator_seed")
accepted_generator_seed_text = (
f", generator seed {accepted_generator_seed}" if isinstance(accepted_generator_seed, int) else ""
)
accepted_commit = accepted.get("commit") or "uncommitted"
lines.append(
f" Latest accepted: {accepted.get('id') or 'unnamed'} ({accepted.get('result') or 'unknown'}, {accepted_seed_text}{accepted_generator_seed_text}, {accepted.get('decision') or 'unknown'}, commit {accepted_commit})"
)
if accepted.get("candidate_prompt_summary"):
lines.append(f" Accepted candidate: {accepted['candidate_prompt_summary']}")
if accepted.get("observation"):
lines.append(f" Accepted observation: {accepted['observation']}")
summary = coverage_summary()
if summary["next_test_candidates"]:
lines.extend(
[
"",
"## Next Fixed-Seed Tests",
"",
*[f"- {key}" for key in summary["next_test_candidates"]],
]
)
template_commands = next_eval_template_commands()
if template_commands:
lines.extend(["", "## Eval Entry Template Commands", ""])
for command in template_commands:
lines.append(f"- {command['key']}: `{command['command']}`")
stronger_control_rows = [row for row in coverage_rows() if row.get("coverage_state") == "needs_stronger_control"]
if stronger_control_rows:
lines.extend(["", "## Stronger Control Cases", ""])
for row in stronger_control_rows:
difficulty = row.get("difficulty") or "unrated"
priority = row.get("priority") or "unprioritized"
control_requirement = row.get("control_requirement") or "control_needed"
lines.append(
f"- {row['key']}: {difficulty}, {priority} priority, {control_requirement}"
)
expansion_plans = guide_expansion_plans()
if expansion_plans:
lines.extend(["", "## Guide/Fragile Evidence Expansion", ""])
for plan in expansion_plans:
seed = plan.get("latest_accepted_seed")
seed_text = f"seed {seed}" if isinstance(seed, int) else "seed unknown"
lines.append(
f"- {plan['key']}: {plan['target']} after {plan['latest_accepted_decision']} "
f"({plan['latest_accepted_id']}, {seed_text})"
)
plans = next_test_plans()
if plans:
lines.extend(["", "## Next Test Plans"])
for plan in plans:
lines.extend(
[
"",
f"### {plan['key']}",
"",
f"- Geometry: {plan['canonical_geometry']}",
f"- References: {', '.join(plan['reference_paths']) or 'none'}",
"- Prompt cues:",
*[f" - {cue}" for cue in plan["prompt_cues"]],
"- Avoid cues:",
*[f" - {cue}" for cue in plan["avoid_cues"]],
]
)
atlas_summary = atlas_coverage_summary(atlas_root=atlas_root)
if atlas_summary["pose_folder_count"]:
unmapped = atlas_summary["unmapped_folders"]
lines.extend(
[
"",
"## Atlas Folder Coverage",
"",
f"- Pose folders: {atlas_summary['pose_folder_count']}",
f"- Mapped folders: {atlas_summary['mapped_folder_count']}",
f"- Unmapped folders: {atlas_summary['unmapped_folder_count']}",
]
)
if unmapped:
lines.extend(["", "Unmapped atlas folders:", *[f"- {folder}" for folder in unmapped]])
gap_plans = atlas_gap_plans(atlas_root=atlas_root)
if gap_plans:
lines.extend(["", "## Atlas Gap Plans"])
for plan in gap_plans:
sample_images = plan["sample_images"]
control_images = plan["control_images"]
lines.extend(
[
"",
f"### {plan['folder']}",
"",
f"- Suggested key: {plan['suggested_variant_key']}",
f"- Pose images: {plan['image_count']}",
f"- Samples: {', '.join(sample_images) or 'none'}",
f"- Controls: {', '.join(control_images) or 'none'}",
]
)
return "\n".join(lines)
def main(argv: list[str] | None = None) -> int:
_ = argv
print(markdown_report())
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))
+9 -3
View File
@@ -86,7 +86,10 @@ def climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None)
if "lying at the bed edge with thighs open" in text:
return "the woman lies at the bed edge with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs"
if "reclining with thighs open" in text or "lying on the back with legs spread" in text:
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs"
return (
"the woman lies on her back with thighs open after ejaculation; thick semen and clear fluid cover her exposed pussy "
"and inner thighs as the exact-center aftermath detail, her body stays still, and her face and torso remain visible behind the open thighs"
)
if "on all fours with hips raised" in text:
return "the woman is on all fours with hips raised while the man is positioned behind her and ejaculates semen across her ass, thighs, and lower back"
if "face-down ass-up" in text or "lies face-down" in text or "face down" in text:
@@ -96,7 +99,7 @@ def climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None)
if "kneeling with mouth open" in text:
return "the woman kneels in front of the man at hip height as he ejaculates semen onto her face, lips, and chest"
if "kneeling in front of a standing partner" in text:
return "the woman kneels in front of the man at hip height while he stands over her for visible ejaculation"
return "the woman kneels in front of the man at hip height while he stands over her and ejaculates semen across her body"
if "standing with cum on the body" in text:
return "the woman stands braced in front of the man while he stands close at hip level and ejaculates semen across her body"
if "squatting on top of a partner" in text:
@@ -116,7 +119,10 @@ def climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None)
or "arousal dripping from pussy" in text
or "open thighs" in text
):
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her pussy and thighs"
return (
"the woman lies on her back with thighs open after ejaculation; thick semen and clear fluid cover her exposed pussy "
"and inner thighs as the exact-center aftermath detail, her body stays still, and her face and torso remain visible behind the open thighs"
)
if role_graph:
return role_graph
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her body"
+11
View File
@@ -179,6 +179,10 @@ def hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "",
return "rear-entry anal pose"
if "edge-supported" in text or "raised edge" in text or "edge-of-bed" in text or "bed-edge" in text:
return "edge-supported penetrative sex pose"
if "folded missionary" in text or "knees-to-chest" in text or "knees to chest" in text:
return "folded missionary penetrative sex pose"
if "cowgirl-alt" in text or "low cowgirl" in text or "seated-squat cowgirl" in text or "low seated squat" in text:
return "low cowgirl seated-squat penetrative sex pose"
positions = (
"missionary",
"reverse cowgirl",
@@ -300,6 +304,11 @@ def hardcore_pose_arrangement(anchor: str, role_graph: str, hard_item: str, comp
"with the giver kneeling between the receiver's open thighs",
)
return "with the giver kneeling at the receiver's hips"
if "reverse cowgirl alt" in text or "upright reverse cowgirl" in text or "upright back-facing straddle" in text:
return cast_phrase(
"with the man lying on his back under the woman while she sits upright straddling his hips facing away",
"with the lower partner lying on their back while the upper partner sits upright straddling them facing away",
)
if "reverse cowgirl" in text:
return cast_phrase(
"with the man lying on his back under the woman while she straddles his hips facing away",
@@ -433,6 +442,8 @@ def action_position_phrase(action: str) -> str:
action = _clean(action).lower()
if is_close_foreplay_text(action):
return "single-frame close-body first-person position"
if "pov upright reverse cowgirl" in action or "pov reverse cowgirl alt" in action:
return "upright reverse-cowgirl first-person position"
if "pov reverse cowgirl" in action:
return "reverse-cowgirl first-person position"
if "pov cowgirl" in action:
+33 -1
View File
@@ -51,6 +51,31 @@ class KreaConfiguredCastDependencies:
paragraph: Callable[[list[str]], str]
def _coworking_action_anchor(action_family: str, scene_text: str, action: str) -> str:
action_lower = action.lower()
if "office chair seat and chair arms" in action_lower:
return ""
scene_lower = scene_text.lower()
if not any(term in scene_lower for term in ("coworking", "office", "desk", "laptop", "glass partition")):
return ""
if action_family == "climax" and "post-ejaculation open-thigh display" in action_lower:
return (
"office chair seat and chair arms frame the lower foreground around her open thighs, "
"with desk edges, laptop tables, glass partitions, plants, and tall-window depth beside and behind her body"
)
if "broad v-frame" in action_lower and "open-thigh frame" in action_lower:
return (
"office chair seat and chair arms frame the lower foreground around her hips and raised knees, "
"with desk edges, laptop tables, glass partitions, plants, and tall-window depth beside and behind her body"
)
if action_family != "manual":
return ""
return (
"office chair seat and chair arms frame the lower foreground around her hips, "
"with desk edges, laptop tables, glass partitions, plants, and tall-window depth beside and behind her body"
)
def format_configured_cast_result(
request: KreaConfiguredCastRequest,
deps: KreaConfiguredCastDependencies,
@@ -88,13 +113,14 @@ def format_configured_cast_result(
item = deps.natural_label_text(item, cast_labels)
axis_values = deps.sanitize_hardcore_axis_values(row.get("item_axis_values"))
detail_density = deps.normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
action_family = deps.row_action_family(row)
action = deps.hardcore_action_sentence(
role_graph,
item,
source_composition,
axis_values,
detail_density,
deps.row_action_family(row),
action_family,
)
action = deps.pov_action_phrase(
action,
@@ -105,9 +131,15 @@ def format_configured_cast_result(
axis_values,
detail_density,
)
scene_anchor = _coworking_action_anchor(
action_family,
" ".join(part for part in (request.scene, request.camera_scene, composition, source_composition) if part),
action,
)
output_composition = deps.pov_composition_text(composition, pov_labels)
parts = [
action,
scene_anchor,
deps.pov_camera_phrase(pov_labels),
cast_prose,
f"A consensual explicit adult scene with {subject}" if not action else "",
+1 -1
View File
@@ -28,7 +28,7 @@ def pov_camera_phrase(pov_labels: list[str], softcore: bool = False) -> str:
"Camera is the male participant's first-person creator view in one continuous frame, with him implied by perspective or foreground cues"
)
return (
"Camera is the male participant's first-person view in one continuous frame; only his foreground hands or body cues appear"
"Camera is the male participant's first-person view in one continuous frame, with his foreground hands or body cues anchoring the lower frame"
)
+173 -25
View File
@@ -36,12 +36,30 @@ def _clean(value: Any) -> str:
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")):
if any(
token in context
for token in (
"lower back",
"ass",
"rear-entry",
"rear entry",
"face-down",
"face down",
"bent-over",
"bent over",
"doggy",
"on all fours",
"hips high",
"hips raised",
"raised ass",
"behind her",
)
):
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"
if any(token in context for token in ("face", "mouth", "lips", "tongue", "chin")):
return "onto her face and chest"
return "onto her body"
@@ -71,6 +89,48 @@ def pov_contact_clause(
return contact
def _is_open_thigh_aftermath_context(context: str, action_lower: str, position_context: str) -> bool:
combined = f"{context} {action_lower} {position_context}"
has_open_thighs = any(
token in combined
for token in (
"open thighs",
"thighs open",
"legs open",
"legs spread",
"reclining with thighs open",
)
)
has_aftermath = any(
token in combined
for token in (
"post-ejaculation",
"after ejaculation",
"aftermath",
"semen",
"visible fluid",
"thick fluid",
"clear fluid",
)
)
has_rear_entry = any(
token in combined
for token in (
"rear-entry",
"rear entry",
"doggy",
"on all fours",
"face-down",
"face down",
"bent-over",
"bent over",
"behind her",
"lower back",
)
)
return has_open_thighs and has_aftermath and not has_rear_entry
def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
detail = _clean(detail).strip(" .;")
if not detail:
@@ -82,7 +142,7 @@ def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
detail = re.sub(r"\bhis\b", "the viewer's", detail, flags=re.IGNORECASE)
detail = re.sub(r"\bhim\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*",
r"^(?:folded missionary|missionary|low cowgirl seated-squat|low cowgirl|cowgirl-alt|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,
@@ -233,6 +293,8 @@ def pov_hardcore_pose_sentence(
"anal",
"cowgirl",
"missionary",
"knees-to-chest",
"knees to chest",
"doggy",
"rear-entry",
"spooning",
@@ -244,6 +306,19 @@ def pov_hardcore_pose_sentence(
"climax",
)
has_penetrative_context = any(token in context or token in action_lower for token in penetrative_tokens)
toy_contact_context = f"{context} {action_lower}"
if (
any(token in toy_contact_context for token in ("wand-style", "wand toy", "wand-toy", "vibrator", "massager"))
and any(token in toy_contact_context for token in ("clit", "vulva", "toy-contact", "toy contact"))
and not has_penetrative_context
):
return outercourse_sentence(
"Close first-person POV wand-toy contact: the woman reclines with thighs spread wide toward the camera; "
"a single continuous teal wand-style massager is the largest lower-frame object, "
"the rounded bulb head presses flat to her vulva and clit as the central contact point, "
"and the smooth handle angles in from the bottom right inside the viewer's visible hand; "
"her open thighs and knees form a V around the foreground wand while her face and torso remain visible behind the leg frame"
)
if (
"face-sitting" in context
@@ -261,16 +336,29 @@ def pov_hardcore_pose_sentence(
action_kind = outercourse_policy.infer_outercourse_action_kind(context, action_lower)
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
return outercourse_sentence(
"The woman kneels low between the viewer's open thighs with her torso bent forward over his pelvis; "
"both hands push her breasts inward around the viewer's penis in the lower foreground, the penis held between her breasts, "
"with her chin and lips directly above the glans at the tip"
"POV boobjob position: the viewer reclines with thighs open while the woman kneels upright between his legs facing him; "
"the viewer's penis rises vertically in the lower foreground and is squeezed between her pressed-together breasts; "
"the woman's own fingers and nails cup her breasts from the outside and push soft breast tissue inward around the shaft, "
"with the glans emerging above the cleavage directly below her mouth"
)
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
return outercourse_sentence(
"The woman bends forward and kneels very low between the viewer's open thighs with her shoulders between his knees; "
"her face is below the viewer's penis at testicle height, mouth and tongue licking the viewer's balls while his penis points upward in the lower foreground above her forehead"
"Low side-pelvis POV: the woman lies low beside the viewer's open thighs with her cheek against the viewer's inner thigh; "
"her face is the closest visible partner part and her head stays low under the viewer's pelvis, with the viewer's scrotum at her mouth; scrotum is the mouth surface, "
"testicles resting across her open lips while her tongue cups them from below, scrotal skin is the nearest mouth surface and both testicles rest against her tongue from below, "
"and the viewer's abdomen and inner thighs framing the close foreground"
)
if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING:
prone_laying = any(
term in position_context
for term in ("reclining", "prone", "belly-down", "belly down", "lying")
)
if prone_laying:
return outercourse_sentence(
"POV prone frontal oral position: the viewer reclines with open thighs forming a wide symmetrical V-frame from the lower corners toward the center; "
"the woman lies belly-down between his thighs with her torso stretched low and horizontal, hips and legs trailing away behind her along the center line; "
"her front-facing mouth and tongue align to the shaft rising from the exact lower center, hands wrap the base, and the centered mouth-to-shaft contact stays framed by his thighs"
)
return outercourse_sentence(
"The woman bends forward between the viewer's open thighs with her head low under the viewer's penis; "
"her face is just under the penis while her tongue touches the underside from the base toward the glans at the tip, "
@@ -278,14 +366,17 @@ def pov_hardcore_pose_sentence(
)
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
return outercourse_sentence(
"The woman kneels between the viewer's open thighs with her torso leaning forward and face visible behind the viewer's penis; "
"one hand grips and strokes the viewer's penis in the lower foreground while the other hand steadies its base, "
"thumb and fingers visible around the penis as she strokes toward the glans"
"POV handjob position: the viewer reclines with thighs open while the woman kneels between his legs facing him, "
"torso leaning forward and face visible behind the penis; "
"the woman's right hand wraps around the viewer's penis in the lower foreground and strokes along the shaft toward the glans, "
"while her left hand steadies the base with her fingers and nails visibly gripping the penis; "
"the viewer's thighs and pelvis frame the lower edges without his hands covering the action"
)
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
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"
"Frontal POV footjob close-up: the woman faces the viewer with hips back, torso behind raised legs, and knees bent open toward the camera; "
"two large overlapping soles dominate the lower center foreground and clamp the upright shaft between them, inner arches press inward from both sides, "
"toes curl around both edges, a narrow visible strip of shaft and glans rises between the compressed feet, and her face and torso stay visible behind the large foreground 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"
@@ -298,11 +389,29 @@ def pov_hardcore_pose_sentence(
"POV sixty-nine oral position: the woman lies head-to-hips over the viewer, her pelvis close to his face and her head lowered toward his hips; "
"her mouth on the viewer's penis and the viewer's mouth on the woman's pussy, with her torso, hips, mouth, and the viewer's lower-foreground body cues aligned in one first-person frame"
)
if woman_gives and not man_gives and any(
term in position_context
for term in (
"upright sitting oral",
"sitting oral",
"seated oral",
"blowjob_sitting",
)
):
return oral_sentence(
"POV upright sitting oral position: the viewer reclines with open thighs forming the lower V-frame and his lower abdomen anchoring the near edge; "
"the woman sits low between his open thighs with hips between his knees, torso upright behind the action, shoulders square to the camera, and face lowered close to the exact center contact point; "
"the vertical shaft rises from the exact lower center between the viewer thighs, her open mouth covers the tip at the centerline, lips wrapped around the glans, and mouth-to-shaft contact is the nearest facial detail; "
"both hands stay low at the base directly below her mouth, fingers wrapped around the shaft, while her eyes, face, shoulders, torso, hands, shaft, and the viewer thigh frame remain readable in one first-person seated frame"
)
if "side-lying oral" in position_context or "side lying oral" in position_context:
if woman_gives and not man_gives:
return oral_sentence(
"POV side-lying oral position: the viewer lies on his side with hips angled toward the woman while she lies beside his thighs; "
"her head stays at penis height with her mouth on the viewer's penis, shoulders and hands close to his pelvis in the lower foreground"
"POV side-profile oral body-line position: the male viewer's abdomen, navel, pelvis, and near thigh create a broad horizontal body surface across the lower frame; "
"the adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground, with navel, abdomen hair, pelvis, and near thigh marking the camera owner's body; "
"the woman enters laterally from the left edge beside his hip, cheek and jaw in profile, mouth on the shaft at the male abdomen line, "
"lips touching the shaft at the male abdomen line, mouth-to-shaft contact is the nearest facial detail, "
"hand around the base under her lips, shoulder and torso trailing sideways along the edge"
)
return oral_sentence(
"POV side-lying cunnilingus position: the woman lies on her side with her top thigh lifted while the viewer lies beside her hips; "
@@ -375,8 +484,10 @@ def pov_hardcore_pose_sentence(
"his face is at pussy height, with her knees, hips, and torso readable from the first-person angle"
)
return oral_sentence(
"POV kneeling oral position: the viewer stands over her with hips forward while the woman kneels directly in front of him; "
"her head is at penis height, mouth on the viewer's penis, shoulders below his hips and his thighs framing the lower foreground"
"Nadir-angle standing male POV top-view oral position: the viewer looks almost straight down from his torso toward the floor, with nearby carpet/floor plane dominating the image; "
"the viewer's abdomen, shorts, thighs, and feet frame the lower foreground, and the viewer's penis shaft appears as a short centered vertical column from the foreground; "
"one kneeling woman is directly below the viewer between his feet, her face tilts upward beneath the shaft, her mouth seals around it, and one hand wraps the base; "
"her hair crown, forehead, shoulders, hands, knees, and compact foreshortened torso are visible from above, with desk legs, chair wheels, carpet texture, and floor seams as top-down office anchors around her"
)
if man_gives and not woman_gives:
return oral_sentence(
@@ -395,16 +506,46 @@ def pov_hardcore_pose_sentence(
return ""
contact = pov_contact_clause(action, role_graph, hard_item, axis_values, context)
if is_climax_text(action, role_graph, hard_item, axis_values_text(axis_values)) and _is_open_thigh_aftermath_context(
context,
action_lower,
position_context,
):
return sentence(
"POV post-ejaculation open-thigh display: the woman reclines or sits back facing the viewer with thighs spread open; "
"the wet aftermath detail is the exact center, thick semen and clear fluid cover the exposed pussy and inner thighs, "
"her body stays still after ejaculation, and her face and torso remain visible behind the open-thigh frame"
)
if "reverse cowgirl alt" in position_context or "upright reverse cowgirl" in position_context or "upright back-facing straddle" in position_context:
return sentence(
"POV upright reverse cowgirl back-facing penetration position: the viewer lies on his back while the woman sits upright on his pelvis facing away; "
"her back stays vertical and readable above her hips, her ass is centered over the viewer's pelvis, "
f"viewer hands hold her hips, viewer thighs frame the lower corners, and centered contact remains visible below her ass {contact}"
)
if "reverse cowgirl" in position_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}"
"her back, hips, and ass are the nearest largest shapes to the camera; "
f"the viewer thighs frame the lower corners, and the centered contact sits directly between her thighs below her ass {contact}"
)
if "folded missionary" in position_context or "knees-to-chest" in position_context or "knees to chest" in position_context:
return sentence(
"POV folded missionary high-leg penetration position: the viewer's lower abdomen anchors the bottom edge with a large centered shaft rising from the lower center; "
"the woman lies on her back facing the viewer with both knees folded tightly toward her chest into a compact knee block above the contact; "
f"viewer hands hold her calves, her feet and ankles sit close to the camera, and her face and torso remain visible behind the raised knees {contact}"
)
if "cowgirl-alt" in position_context or "low cowgirl" in position_context or "seated-squat cowgirl" in position_context or "low seated squat" in position_context:
return sentence(
"POV low cowgirl seated-squat penetration position: the viewer lies flat on his back underneath her, and the lens sits low at the viewer's abdomen looking upward from his pelvis; "
"the woman faces the viewer in a low squat mounted over his hips with knees bent wide and close to the camera; "
f"the viewer supports the underside of her thighs, her torso stays close above the centered contact, and the high room background behind her upper body reinforces the low supine viewpoint {contact}"
)
if "cowgirl" in position_context or "straddling a partner" in position_context or "squatting on top" in position_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}"
"POV frontal cowgirl wide-thigh bridge position: the viewer reclines underneath her with lower abdomen and pelvis anchoring the bottom edge; "
"the woman straddles his hips facing him, her thighs form a wide horizontal thigh bridge from left edge to right edge, "
f"knees planted outside the viewer's hips, torso upright above the centered contact point, viewer hands grip the sides of her thighs, and centered contact remains below her belly {contact}"
)
if "lotus" in position_context or "seated in a partner's lap" in position_context:
return sentence(
@@ -428,9 +569,15 @@ def pov_hardcore_pose_sentence(
or "bed edge" in position_context
or (not position_text and "kneels between her legs" in context)
):
if "penetrates her ass" in contact:
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}"
)
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}"
"POV elevated-edge missionary position: the woman lies flat on her back across a flat elevated support with hair, shoulders, spine, and hips aligned on one horizontal surface; "
"her legs open toward the viewer at the foot edge, thighs forming a broad U-frame around the centered contact line; "
f"the viewer stands, kneels, or braces at the foot edge with hands holding her calves or outer thighs and feet, shins, or side-dropping legs placed below the support edge {contact}"
)
if "standing" in position_context:
return sentence(
@@ -444,8 +591,9 @@ def pov_hardcore_pose_sentence(
)
if "doggy" in position_context or "all fours" in position_context or "rear-entry" in position_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}"
"Top-down POV doggy position from behind: the camera looks down over the viewer's hands onto the woman's raised hips; "
f"the woman is on all fours with chest low, forearms folded, cheek turned sideways far ahead, back arched, and hips raised high toward the camera; "
f"the viewer's hands hold her hips with natural lower-body POV cues in the foreground {contact}"
)
if "kneeling" in position_context:
return sentence(
+348
View File
@@ -3,20 +3,30 @@ from __future__ import annotations
import json
try:
from . import krea2_eval_log
from . import krea2_pose_variant_catalog
from .hardcore_position_config import (
build_hardcore_action_filter_json,
build_hardcore_position_pool_json,
empty_hardcore_position_config,
hardcore_position_family_choices,
hardcore_position_focus_choices,
hardcore_position_key_choices,
hardcore_position_summary,
parse_hardcore_position_config,
)
except ImportError: # Allows local smoke tests from the repository root.
import krea2_eval_log
import krea2_pose_variant_catalog
from hardcore_position_config import (
build_hardcore_action_filter_json,
build_hardcore_position_pool_json,
empty_hardcore_position_config,
hardcore_position_family_choices,
hardcore_position_focus_choices,
hardcore_position_key_choices,
hardcore_position_summary,
parse_hardcore_position_config,
)
@@ -30,6 +40,115 @@ def _choice_input_key(prefix, choice):
return f"{prefix}_{key}"
def _variant_input_key(variant_key):
return _choice_input_key("include", str(variant_key or "").removeprefix("pov_"))
def _unique_extend(values):
selected = []
for value in values:
text = str(value or "").strip()
if text and text not in selected:
selected.append(text)
return selected
def _variant_family(value):
family = str(value or "any")
if family == "penetration":
family = "penetrative"
return family if family in hardcore_position_family_choices() else "any"
def _variant_positions(variant):
valid = set(hardcore_position_key_choices())
return [str(key) for key in variant.get("position_keys", []) if str(key) in valid]
def _variants_for_action_family(action_family):
return krea2_pose_variant_catalog.variants(action_family=action_family)
def _selected_variant_rows(action_family, kwargs):
return [
variant
for variant in _variants_for_action_family(action_family)
if bool(kwargs.get(_variant_input_key(variant.get("key")), False))
]
def _join_variant_cues(variants, cue_key):
cues = []
for variant in variants:
cues.extend(str(cue) for cue in variant.get(cue_key, []) if str(cue).strip())
return "; ".join(_unique_extend(cues))
def _selected_variant_positions(variants):
positions = []
for variant in variants:
positions.extend(_variant_positions(variant))
return _unique_extend(positions)
def _selected_variant_keys(variants):
return [str(variant.get("key")) for variant in variants if variant.get("key")]
def _merged_family_for_variant_filter(incoming_config, combine_mode, family):
family = _variant_family(family)
if combine_mode != "add":
return family
incoming = parse_hardcore_position_config(incoming_config)
incoming_family = _variant_family(incoming.get("family"))
incoming_positions = incoming.get("positions") or []
if not incoming.get("enabled") or (not incoming_positions and incoming_family == "any"):
return family
if incoming_family == family:
return family
return "any"
def _empty_or_incoming_config(incoming_config, combine_mode):
if combine_mode == "add" and incoming_config:
config = parse_hardcore_position_config(incoming_config)
else:
config = empty_hardcore_position_config()
config["summary"] = hardcore_position_summary(config)
return json.dumps(config, ensure_ascii=True, sort_keys=True)
def _merge_variant_metadata(config_json, variants):
config = json.loads(config_json)
selected_keys = _selected_variant_keys(variants)
existing_keys = config.get("krea2_variant_keys") or []
if not isinstance(existing_keys, list):
existing_keys = [existing_keys]
variant_keys = _unique_extend([*existing_keys, *selected_keys])
config["krea2_variant_keys"] = variant_keys
selected_statuses = {str(variant.get("key")): str(variant.get("status") or "") for variant in variants if variant.get("key")}
existing_statuses = config.get("krea2_variant_statuses") if isinstance(config.get("krea2_variant_statuses"), dict) else {}
config["krea2_variant_statuses"] = {**existing_statuses, **selected_statuses}
prompt_cues = _unique_extend(
[*(config.get("krea2_prompt_cues") or []), *(_join_variant_cues(variants, "prompt_cues").split("; ") if variants else [])]
)
avoid_cues = _unique_extend(
[*(config.get("krea2_avoid_cues") or []), *(_join_variant_cues(variants, "avoid_cues").split("; ") if variants else [])]
)
if prompt_cues:
config["krea2_prompt_cues"] = prompt_cues
if avoid_cues:
config["krea2_avoid_cues"] = avoid_cues
base_summary = str(config.get("summary") or hardcore_position_summary(config))
if variant_keys and "variants=" not in base_summary:
base_summary = f"{base_summary}; variants={','.join(variant_keys)}"
config["summary"] = base_summary
return json.dumps(config, ensure_ascii=True, sort_keys=True)
class SxCPHardcorePositionPool:
@classmethod
def INPUT_TYPES(cls):
@@ -66,6 +185,217 @@ class SxCPHardcorePositionPool:
return config, json.loads(config).get("summary", "")
class SxCPKrea2PoseVariant:
@classmethod
def INPUT_TYPES(cls):
keys = krea2_pose_variant_catalog.variant_keys()
return {
"required": {
"variant_key": (keys or ["missing_catalog_variant"], {"default": keys[0] if keys else "missing_catalog_variant"}),
"combine_mode": (["replace", "add"], {"default": "replace"}),
},
"optional": {
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
},
}
RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = (
"hardcore_position_config",
"variant_key",
"prompt_cues",
"avoid_cues",
"summary",
"variant_json",
)
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(self, variant_key, combine_mode="replace", hardcore_position_config=""):
variant = krea2_pose_variant_catalog.get_variant(variant_key)
if not variant:
empty = {
"key": str(variant_key or ""),
"status": "missing",
"summary": "missing Krea2 pose variant",
}
return hardcore_position_config or "", str(variant_key or ""), "", "", empty["summary"], json.dumps(empty, sort_keys=True)
positions = _variant_positions(variant)
family = _variant_family(variant.get("action_family") or variant.get("family"))
config = build_hardcore_position_pool_json(
hardcore_position_config=hardcore_position_config or "",
combine_mode=combine_mode,
family=family,
selected_positions=positions,
)
prompt_cues = "; ".join(str(cue) for cue in variant.get("prompt_cues", []) if str(cue).strip())
avoid_cues = "; ".join(str(cue) for cue in variant.get("avoid_cues", []) if str(cue).strip())
summary = (
f"variant={variant.get('key')}; status={variant.get('status')}; "
f"family={family}; positions={','.join(positions) or 'none'}"
)
return (
config,
str(variant.get("key") or variant_key),
prompt_cues,
avoid_cues,
summary,
json.dumps(variant, ensure_ascii=True, sort_keys=True),
)
class _SxCPKrea2POVVariantFilter:
ACTION_FAMILY = ""
POSITION_FAMILY = "any"
@classmethod
def INPUT_TYPES(cls):
required = {
"combine_mode": (["replace", "add"], {"default": "replace"}),
}
for variant in _variants_for_action_family(cls.ACTION_FAMILY):
required[_variant_input_key(variant.get("key"))] = ("BOOLEAN", {"default": False})
return {
"required": required,
"optional": {
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
},
}
RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING", "STRING", "STRING", "STRING", "STRING")
RETURN_NAMES = (
"hardcore_position_config",
"selected_variant_keys",
"selected_positions",
"prompt_cues",
"summary",
"variants_json",
)
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(self, combine_mode="replace", hardcore_position_config="", **kwargs):
variants = _selected_variant_rows(self.ACTION_FAMILY, kwargs)
if not variants:
config = _empty_or_incoming_config(hardcore_position_config or "", combine_mode)
return config, "", "", "", json.loads(config).get("summary", ""), "[]"
positions = _selected_variant_positions(variants)
family = _merged_family_for_variant_filter(
hardcore_position_config or "",
combine_mode,
self.POSITION_FAMILY or self.ACTION_FAMILY,
)
config = build_hardcore_position_pool_json(
hardcore_position_config=hardcore_position_config or "",
combine_mode=combine_mode,
family=family,
selected_positions=positions,
)
config = _merge_variant_metadata(config, variants)
parsed = json.loads(config)
selected_keys = parsed.get("krea2_variant_keys") or []
selected_positions = parsed.get("positions") or []
prompt_cues = _join_variant_cues(variants, "prompt_cues")
return (
config,
",".join(str(key) for key in selected_keys),
",".join(str(position) for position in selected_positions),
prompt_cues,
str(parsed.get("summary") or ""),
json.dumps(variants, ensure_ascii=True, sort_keys=True),
)
class SxCPKrea2POVPenetrationFilter(_SxCPKrea2POVVariantFilter):
ACTION_FAMILY = "penetration"
POSITION_FAMILY = "penetration"
class SxCPKrea2POVOralFilter(_SxCPKrea2POVVariantFilter):
ACTION_FAMILY = "oral"
POSITION_FAMILY = "oral"
class SxCPKrea2POVOutercourseFilter(_SxCPKrea2POVVariantFilter):
ACTION_FAMILY = "outercourse"
POSITION_FAMILY = "outercourse"
class SxCPKrea2POVManualFilter(_SxCPKrea2POVVariantFilter):
ACTION_FAMILY = "manual"
POSITION_FAMILY = "manual"
class SxCPKrea2POVToyFilter(_SxCPKrea2POVVariantFilter):
ACTION_FAMILY = "toy"
POSITION_FAMILY = "any"
class SxCPKrea2POVClimaxFilter(_SxCPKrea2POVVariantFilter):
ACTION_FAMILY = "climax"
POSITION_FAMILY = "climax"
class SxCPKrea2POVInteractionFilter(_SxCPKrea2POVVariantFilter):
ACTION_FAMILY = "interaction"
POSITION_FAMILY = "interaction"
class SxCPKrea2VariantEvidence:
@classmethod
def INPUT_TYPES(cls):
keys = krea2_pose_variant_catalog.variant_keys()
return {
"required": {
"variant_key": (keys or ["missing_catalog_variant"], {"default": keys[0] if keys else "missing_catalog_variant"}),
"result": (["accepted", "rejected", "inconclusive", "any"], {"default": "accepted"}),
},
"optional": {
"variant_key_in": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "INT", "STRING")
RETURN_NAMES = (
"summary",
"baseline_image_path",
"candidate_image_path",
"evidence_json",
"seed",
"decision",
)
FUNCTION = "build"
CATEGORY = "prompt_builder"
def build(self, variant_key, result="accepted", variant_key_in=""):
key = str(variant_key_in or variant_key or "").strip()
result_filter = None if result == "any" else result
entries = krea2_eval_log.entries_for_variant(key, result=result_filter)
if not entries:
empty = {
"variant_key": key,
"result": result,
"summary": "no Krea2 eval evidence found",
}
return empty["summary"], "", "", json.dumps(empty, ensure_ascii=True, sort_keys=True), -1, ""
entry = entries[0]
summary = (
f"evidence={entry.get('id')}; variant={entry.get('variant_key')}; "
f"seed={entry.get('seed')}; result={entry.get('result')}; decision={entry.get('decision')}"
)
seed = entry.get("seed")
return (
summary,
str(entry.get("baseline_image") or ""),
str(entry.get("candidate_image") or ""),
json.dumps(entry, ensure_ascii=True, sort_keys=True),
int(seed) if isinstance(seed, int) else -1,
str(entry.get("decision") or ""),
)
class SxCPHardcoreActionFilter:
@classmethod
def INPUT_TYPES(cls):
@@ -128,9 +458,27 @@ class SxCPHardcoreActionFilter:
NODE_CLASS_MAPPINGS = {
"SxCPHardcorePositionPool": SxCPHardcorePositionPool,
"SxCPHardcoreActionFilter": SxCPHardcoreActionFilter,
"SxCPKrea2PoseVariant": SxCPKrea2PoseVariant,
"SxCPKrea2POVPenetrationFilter": SxCPKrea2POVPenetrationFilter,
"SxCPKrea2POVOralFilter": SxCPKrea2POVOralFilter,
"SxCPKrea2POVOutercourseFilter": SxCPKrea2POVOutercourseFilter,
"SxCPKrea2POVManualFilter": SxCPKrea2POVManualFilter,
"SxCPKrea2POVToyFilter": SxCPKrea2POVToyFilter,
"SxCPKrea2POVClimaxFilter": SxCPKrea2POVClimaxFilter,
"SxCPKrea2POVInteractionFilter": SxCPKrea2POVInteractionFilter,
"SxCPKrea2VariantEvidence": SxCPKrea2VariantEvidence,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"SxCPHardcorePositionPool": "SxCP Hardcore Position Pool",
"SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter",
"SxCPKrea2PoseVariant": "SxCP Krea2 Pose Variant",
"SxCPKrea2POVPenetrationFilter": "SxCP Krea2 POV Penetration Filter",
"SxCPKrea2POVOralFilter": "SxCP Krea2 POV Oral Filter",
"SxCPKrea2POVOutercourseFilter": "SxCP Krea2 POV Outercourse Filter",
"SxCPKrea2POVManualFilter": "SxCP Krea2 POV Manual Filter",
"SxCPKrea2POVToyFilter": "SxCP Krea2 POV Toy Filter",
"SxCPKrea2POVClimaxFilter": "SxCP Krea2 POV Climax Filter",
"SxCPKrea2POVInteractionFilter": "SxCP Krea2 POV Interaction Filter",
"SxCPKrea2VariantEvidence": "SxCP Krea2 Variant Evidence",
}
+10
View File
@@ -322,6 +322,16 @@ NODE_INPUT_TOOLTIPS = {
"combine_mode": "replace discards incoming position choices; add merges these choices with the incoming config.",
"hardcore_position_config": "Optional incoming config. Usually connect previous Position Pool here only when chaining pools.",
},
"SxCPKrea2PoseVariant": {
"variant_key": "Atlas-calibrated Krea2 POV pose variant. Proven variants have fixed-seed evidence in the eval log.",
"combine_mode": "replace discards incoming position choices; add merges this variant with the incoming position config.",
"hardcore_position_config": "Optional incoming hardcore position config. Connect this when layering a variant on an existing pool.",
},
"SxCPKrea2VariantEvidence": {
"variant_key": "Catalog variant whose fixed-seed eval evidence should be shown.",
"result": "Filter eval entries by result. accepted is the evidence used for proven variants.",
"variant_key_in": "Optional connected variant key from SxCP Krea2 Pose Variant. When connected, it overrides the selector.",
},
"SxCPHardcoreActionFilter": {
"focus": "keep_pool preserves/broadens the incoming pool; *_only modes force one action family.",
"allow_toys": "Allow toy/strap-on wording in hardcore actions.",
+3
View File
@@ -247,6 +247,8 @@ def build_insta_of_pair(request: InstaPairBuildRequest, deps: InstaPairBuildDepe
rng=hard_content_rng,
continuity_map=deps.hardcore_clothing_continuity,
choose=deps.choose,
label_map=character_slot_map,
slot_hardcore_clothing=deps.slot_hardcore_clothing,
)
if clothing_route.requires_body_exposure_scene:
hard_scene = pair_clothing.body_exposure_scene_text(hard_scene)
@@ -295,6 +297,7 @@ def build_insta_of_pair(request: InstaPairBuildRequest, deps: InstaPairBuildDepe
camera_caption_text=deps.camera_caption_text,
cast_descriptors=cast_context["cast_descriptors"],
character_hardcore_clothing_entries=character_hardcore_clothing_entries,
pov_hardcore_clothing_entries=clothing_route.pov_hardcore_clothing,
default_man_hardcore_clothing_entries=clothing_route.default_man_hardcore_clothing,
hard_clothing_state=clothing_route.hardcore_clothing_state,
hard_detail_density=hard_detail_density,
+58
View File
@@ -437,10 +437,51 @@ def default_man_hardcore_clothing_entries(
return entries
def _pov_clothing_sentence(clothing: str, needs_lower_access: bool) -> str:
clothing = _clean_pair_punctuation(str(clothing or "").strip().rstrip("."))
if not clothing:
return ""
lower = clothing.lower()
if lower.startswith(("fully nude", "nude")):
if needs_lower_access:
return "POV foreground body cue: the viewer's bare hips, thighs, hands, and penis appear as first-person body cues"
return "POV foreground body cue: the viewer's bare hands, forearms, or torso edge appear as first-person body cues"
clothing = re.sub(r"^(?:wears|wearing|keeps|has|with)\s+", "", clothing, flags=re.IGNORECASE).strip()
if needs_lower_access:
return (
f"POV foreground clothing cue: {clothing}, appearing as the viewer's hands, hips, thighs, or lowered waistband"
)
return (
f"POV foreground clothing cue: {clothing}, appearing as the viewer's hands, forearms, sleeves, or torso edge"
)
def pov_hardcore_clothing_entries(
label_map: dict[str, dict[str, Any]],
pov_labels: list[str] | None,
rng: Any,
needs_lower_access: bool,
choose: Callable[[Any, list[str]], str],
slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str] | None = None,
) -> list[str]:
entries: list[str] = []
pool = INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS if needs_lower_access else INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE
for label in pov_labels or []:
slot = label_map.get(label)
clothing = slot_hardcore_clothing(slot, rng) if slot_hardcore_clothing is not None else ""
if not clothing:
clothing = choose(rng, pool)
sentence = _pov_clothing_sentence(clothing, needs_lower_access)
if sentence:
entries.append(sentence)
return entries
@dataclass(frozen=True)
class HardcorePairClothingRoute:
access_flags: dict[str, bool]
woman_access: str
pov_hardcore_clothing: list[str]
default_man_hardcore_clothing: list[str]
hardcore_clothing_state: str
hardcore_clothing_sentence: str
@@ -450,6 +491,7 @@ class HardcorePairClothingRoute:
return {
"access_flags": dict(self.access_flags),
"woman_access": self.woman_access,
"pov_hardcore_clothing": list(self.pov_hardcore_clothing),
"default_man_hardcore_clothing": list(self.default_man_hardcore_clothing),
"hardcore_clothing_state": self.hardcore_clothing_state,
"hardcore_clothing_sentence": self.hardcore_clothing_sentence,
@@ -468,9 +510,19 @@ def resolve_hardcore_pair_clothing_result(
rng: Any,
continuity_map: dict[str, str],
choose: Callable[[Any, list[str]], str],
label_map: dict[str, dict[str, Any]] | None = None,
slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str] | None = None,
) -> HardcorePairClothingRoute:
access_flags = hardcore_row_access_flags(hard_row)
woman_access = "lower" if access_flags["woman_lower"] else "upper" if access_flags["woman_upper"] else ""
pov_entries = pov_hardcore_clothing_entries(
label_map or {},
pov_labels,
rng,
access_flags["man_lower"],
choose,
slot_hardcore_clothing,
)
default_man_entries = default_man_hardcore_clothing_entries(
men_count,
pov_labels,
@@ -491,6 +543,7 @@ def resolve_hardcore_pair_clothing_result(
for part in (
fallback_state,
*character_hardcore_clothing_entries,
*pov_entries,
*default_man_entries,
)
if str(part or "").strip()
@@ -510,6 +563,7 @@ def resolve_hardcore_pair_clothing_result(
return HardcorePairClothingRoute(
access_flags=access_flags,
woman_access=woman_access,
pov_hardcore_clothing=pov_entries,
default_man_hardcore_clothing=default_man_entries,
hardcore_clothing_state=hard_clothing_state,
hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "",
@@ -531,6 +585,8 @@ def resolve_hardcore_pair_clothing(
rng: Any,
continuity_map: dict[str, str],
choose: Callable[[Any, list[str]], str],
label_map: dict[str, dict[str, Any]] | None = None,
slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str] | None = None,
) -> dict[str, Any]:
return resolve_hardcore_pair_clothing_result(
hard_row=hard_row,
@@ -542,4 +598,6 @@ def resolve_hardcore_pair_clothing(
rng=rng,
continuity_map=continuity_map,
choose=choose,
label_map=label_map,
slot_hardcore_clothing=slot_hardcore_clothing,
).as_dict()
+2
View File
@@ -67,6 +67,7 @@ def assemble_insta_pair_metadata(
camera_caption_text: Callable[[dict[str, Any]], str],
cast_descriptors: list[str],
character_hardcore_clothing_entries: list[str],
pov_hardcore_clothing_entries: list[str],
default_man_hardcore_clothing_entries: list[str],
hard_clothing_state: str,
hard_detail_density: str,
@@ -154,6 +155,7 @@ def assemble_insta_pair_metadata(
"pov_prompt_directive": pov_directive,
"softcore_partner_styling": soft_partner_styling,
"character_hardcore_clothing": character_hardcore_clothing_entries,
"pov_hardcore_clothing": pov_hardcore_clothing_entries,
"default_man_hardcore_clothing": default_man_hardcore_clothing_entries,
"hardcore_clothing_state": hard_clothing_state,
"hardcore_detail_density": hard_detail_density,
+4 -4
View File
@@ -923,12 +923,12 @@ def scene_direction_detail(
if "left side" in direction:
return f"{subject} stays readable in the first-person action while {midground} run along the viewer's left-side background toward {background}, with {detail_label} kept at the frame edges"
if "back-right" in direction or "back-left" in direction:
return f"{subject} stays close in one continuous first-person action frame; {midground} lead diagonally toward {background} at the edges, not in the lower foreground"
return f"{subject} stays close in one continuous first-person action frame; {midground} lead diagonally toward {background} along the side/background edges"
if direction == "back view":
return f"{subject} and the action stay primary while {midground} and {background} remain beyond the body, not between viewer and action; only POV body cues sit low in frame"
return f"{subject} and the action stay primary while {midground} and {background} remain beyond the body, with POV body cues sitting low in frame"
if "front-right" in direction or "front-left" in direction:
return f"{subject} fills the first-person action frame while {midground} recede diagonally behind {pronoun} toward {background}"
return f"{subject} faces the viewer in first-person view; {midground} and {background} stay behind {pronoun}, not between viewer and body"
return f"{subject} faces the viewer in first-person view; {midground} and {background} stay behind {pronoun} at background depth"
if "right side" in direction or "left side" in direction:
return f"{subject} {is_verb} held in side profile along the {foreground}; {midground} run laterally behind {pronoun}, with {background} still readable"
if "back-right" in direction or "back-left" in direction:
@@ -1045,7 +1045,7 @@ def scene_camera_directive(
subject, _pronoun = scene_subject_terms(subject_kind, pov_labels)
return (
f"{profile['layout_label']} from POV{geometry_clause}: keep {subject} and the action primary; "
f"{profile['place']} context stays beside or behind the bodies, not in the lower foreground; "
f"{profile['place']} context stays beside or behind the bodies and along the side/background edges; "
f"POV body or hand cues stay in the lower foreground."
)
return (
+67
View File
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
from datetime import date
import json
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) in sys.path:
sys.path.remove(str(ROOT))
sys.path.insert(0, str(ROOT))
import krea2_eval_log # noqa: E402
def _load_entry(path: Path) -> dict:
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, dict):
raise ValueError("entry JSON must contain one object")
return data
def main() -> int:
parser = argparse.ArgumentParser(description="Validate and append one durable Krea2 fixed-seed eval entry.")
parser.add_argument("--entry-json", help="Path to a JSON object containing one eval entry.")
parser.add_argument("--print-template", action="store_true", help="Print a valid eval entry template instead of recording.")
parser.add_argument("--variant-key", help="Catalog variant key for --print-template.")
parser.add_argument("--seed", type=int, help="Fixed seed for --print-template.")
parser.add_argument("--generator-seed", type=int, help="Optional SxCP generator/control seed for --print-template.")
parser.add_argument("--source", default="sxcp_eval_mcp", help="Source label for --print-template.")
parser.add_argument("--date", default=date.today().isoformat(), help="Date for --print-template.")
parser.add_argument("--log-path", default=str(krea2_eval_log.DEFAULT_EVAL_LOG_PATH), help="Eval log path to update.")
parser.add_argument("--dry-run", action="store_true", help="Validate without writing the log.")
args = parser.parse_args()
try:
if args.print_template:
if not args.variant_key or args.seed is None:
raise ValueError("--print-template requires --variant-key and --seed")
entry = krea2_eval_log.entry_template(
args.variant_key,
seed=args.seed,
generator_seed=args.generator_seed,
source=args.source,
date=args.date,
)
print(json.dumps(entry, ensure_ascii=True, indent=2))
return 0
if not args.entry_json:
raise ValueError("--entry-json is required unless --print-template is used")
entry = _load_entry(Path(args.entry_json))
log = krea2_eval_log.append_entry(entry, path=args.log_path, dry_run=args.dry_run)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
action = "validated" if args.dry_run else "recorded"
print(f"{action}: {entry.get('id')} ({len(log.get('entries') or [])} entries)")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env python3
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
if str(ROOT) in sys.path:
sys.path.remove(str(ROOT))
sys.path.insert(0, str(ROOT))
import krea2_tuning_report # noqa: E402
def main() -> int:
print(krea2_tuning_report.markdown_report())
return 0
if __name__ == "__main__":
raise SystemExit(main())
+2
View File
@@ -1661,6 +1661,8 @@ def run_simulation(seed: int = 3901, *, include_prompts: bool = False) -> dict[s
cases.extend(_pair_reports("insta_pair.penetration", penetration_pair, include_prompts=include_prompts))
pov_pair = _insta_pair_case(seed + 2, pov=True, position="penis_licking", focus="outercourse_only", family="outercourse")
cases.extend(_pair_reports("insta_pair.pov_outercourse", pov_pair, include_prompts=include_prompts))
ballsucking_pair = _insta_pair_case(seed + 5, pov=True, position="testicle_sucking", focus="outercourse_only", family="outercourse")
cases.extend(_pair_reports("insta_pair.pov_ballsucking", ballsucking_pair, include_prompts=include_prompts))
coverage_checks = _route_family_coverage_checks(cases)
axis_checks = _seed_axis_checks(seed + 3)
pair_seed_checks = _pair_seed_checks(seed + 4)
+2308 -36
View File
File diff suppressed because it is too large Load Diff
+232
View File
@@ -0,0 +1,232 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
tools/sxcp_eval_loop.sh [minutes] [options]
Loop protocol for Krea2 prompt-generator tuning. Start it right after sending a
prompt to sxcp_eval_out. Every N minutes it writes a structured evaluation
request, prints it, and optionally pipes it to a command. Each cycle should
produce either a prompt-only A/B edit, a generator fix, or a prompt-guide rule.
Options:
-m, --minutes N Wait N minutes between evaluation requests.
-i, --in CHANNEL Graph-to-agent channel. Default: sxcp_eval_in.
-o, --out CHANNEL Agent-to-graph prompt-only channel. Default: sxcp_eval_out.
-l, --log CHANNEL Analysis/log channel name. Default: sxcp_eval_log.
-g, --guide FILE Durable Krea2 prompt guide. Default: docs/krea2-prompt-guide.md.
-d, --dir DIR Runtime log directory. Default: .sxcp_eval.
--once Run one wait/check cycle and exit.
-h, --help Show this help.
Optional automation:
SXCP_EVAL_CODEX_CMD If set, the request is piped to this command.
Example: SXCP_EVAL_CODEX_CMD="codex exec"
The command receives the request on stdin and these environment variables:
SXCP_EVAL_IN_CHANNEL, SXCP_EVAL_OUT_CHANNEL, SXCP_EVAL_LOG_CHANNEL,
SXCP_EVAL_GUIDE_FILE, SXCP_EVAL_REQUEST_FILE, SXCP_EVAL_CYCLE_DIR,
SXCP_EVAL_CYCLE.
EOF
}
die() {
echo "sxcp_eval_loop: $*" >&2
exit 1
}
is_positive_number() {
case "${1:-}" in
''|*[!0-9.]*|.*.*|0|0.0|0.00) return 1 ;;
*) return 0 ;;
esac
}
minutes="${SXCP_EVAL_MINUTES:-}"
in_channel="${SXCP_EVAL_IN_CHANNEL:-sxcp_eval_in}"
out_channel="${SXCP_EVAL_OUT_CHANNEL:-sxcp_eval_out}"
log_channel="${SXCP_EVAL_LOG_CHANNEL:-sxcp_eval_log}"
guide_file="${SXCP_EVAL_GUIDE_FILE:-docs/krea2-prompt-guide.md}"
log_root="${SXCP_EVAL_LOG_DIR:-.sxcp_eval}"
run_once=0
if [ "${1:-}" != "" ] && [ "${1#-}" = "$1" ]; then
minutes="$1"
shift
fi
while [ "$#" -gt 0 ]; do
case "$1" in
-m|--minutes)
[ "$#" -ge 2 ] || die "$1 requires a value"
minutes="$2"
shift 2
;;
-i|--in)
[ "$#" -ge 2 ] || die "$1 requires a value"
in_channel="$2"
shift 2
;;
-o|--out)
[ "$#" -ge 2 ] || die "$1 requires a value"
out_channel="$2"
shift 2
;;
-l|--log)
[ "$#" -ge 2 ] || die "$1 requires a value"
log_channel="$2"
shift 2
;;
-g|--guide)
[ "$#" -ge 2 ] || die "$1 requires a value"
guide_file="$2"
shift 2
;;
-d|--dir)
[ "$#" -ge 2 ] || die "$1 requires a value"
log_root="$2"
shift 2
;;
--once)
run_once=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1"
;;
esac
done
minutes="${minutes:-5}"
is_positive_number "$minutes" || die "minutes must be a positive number"
mkdir -p "$log_root"
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
run_dir="$log_root/$run_id"
mkdir -p "$run_dir"
events_file="$run_dir/events.tsv"
summary_file="$run_dir/summary.md"
cat > "$summary_file" <<EOF
# SxCP Eval Loop $run_id
- Interval: ${minutes} minute(s)
- Input channel: \`$in_channel\`
- Prompt output channel: \`$out_channel\`
- Log channel: \`$log_channel\`
- Krea2 prompt guide: \`$guide_file\`
## Goal
Tune the SxCP generator so its default Krea2 prompts produce the strongest
possible images for the selected scene, camera, subject, outfit, action, and
style. Every cycle should turn visual evidence into one of:
- a prompt-only A/B edit,
- a durable rule for \`$guide_file\`,
- a generator code/data change with focused test coverage.
## Protocol
1. Pull the latest prompt/image from \`$in_channel\`.
2. Record the emitted seed. If it is missing, mark the image as uncontrolled.
3. Compare the image against the prompt and previous edited prompt.
4. Identify concrete Krea2 mismatches and likely generator path.
5. Classify the next step: prompt-only edit, guide rule, or generator patch.
6. Push only the next test prompt text to \`$out_channel\`. Preserve the same
seed through the MCP signal when available; never write the seed into the
prompt text.
7. Keep analysis in chat or \`$log_channel\`, not in \`$out_channel\`.
8. Edit generator code/data only when the issue is systemic.
9. Update \`$guide_file\` when a wording rule is confirmed.
10. Run focused smoke tests after generator edits.
## Cycles
EOF
printf 'cycle\tutc_time\trequest_file\tstatus\n' > "$events_file"
cycle=0
while :; do
cycle=$((cycle + 1))
echo "sxcp_eval_loop: cycle $cycle waiting ${minutes} minute(s) before requesting evaluation..."
sleep "${minutes}m"
stamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
cycle_dir="$run_dir/cycle_$(printf '%03d' "$cycle")"
mkdir -p "$cycle_dir"
request_file="$cycle_dir/request.md"
cat > "$request_file" <<EOF
Please run SxCP eval cycle $cycle now.
Primary goal:
- Tune the generator for better Krea2 images, not just one isolated image.
- Maintain/update the durable Krea2 prompt guide at: $guide_file
Channels:
- Pull latest graph output from: $in_channel
- Push prompt-only replacement to: $out_channel
- Put analysis/log text in chat or: $log_channel
Evaluation steps:
1. Pull the latest payload from $in_channel.
2. Record payload.seed if present. Keep the same seed for prompt-only A/B tests.
3. Inspect image_path and compare it to the prompt text.
4. Score these Krea2 axes: identity, outfit continuity, pose/action, camera compliance, location coherence, crop/framing, prompt noise, model confusion tokens, seed control, and overall image usefulness.
5. Identify the smallest concrete mismatch that should be tested next.
6. Classify the finding:
- prompt-only: push exactly one edited prompt to $out_channel and preserve payload.seed through the MCP signal when the tool supports it.
- guide-rule: update $guide_file with the confirmed Krea2 wording rule.
- generator-fix: edit the responsible generator path, add/adjust focused smoke coverage, run tests, and summarize the change.
7. Keep a clear link between the image evidence, seed, prompt wording, and generator path.
8. Append the finding to the eval log with: seed, original issue, changed wording/path, expected improvement, test result, guide update, generator update, and next hypothesis.
Current run:
- run_id: $run_id
- cycle: $cycle
- generated_at_utc: $stamp
- request_file: $request_file
- guide_file: $guide_file
EOF
{
echo
echo "### Cycle $cycle - $stamp"
echo
echo "- Request: \`$request_file\`"
echo "- Status: pending evaluation"
} >> "$summary_file"
printf '%s\t%s\t%s\t%s\n' "$cycle" "$stamp" "$request_file" "pending" >> "$events_file"
echo
echo "================ SxCP Eval Request ================"
cat "$request_file"
echo "==================================================="
echo
if [ "${SXCP_EVAL_CODEX_CMD:-}" != "" ]; then
echo "sxcp_eval_loop: piping request to SXCP_EVAL_CODEX_CMD"
SXCP_EVAL_IN_CHANNEL="$in_channel" \
SXCP_EVAL_OUT_CHANNEL="$out_channel" \
SXCP_EVAL_LOG_CHANNEL="$log_channel" \
SXCP_EVAL_GUIDE_FILE="$guide_file" \
SXCP_EVAL_REQUEST_FILE="$request_file" \
SXCP_EVAL_CYCLE_DIR="$cycle_dir" \
SXCP_EVAL_CYCLE="$cycle" \
sh -c "$SXCP_EVAL_CODEX_CMD" < "$request_file"
fi
if [ "$run_once" -eq 1 ]; then
break
fi
done
echo "sxcp_eval_loop: log written to $run_dir"
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
"""Small CLI for one-off SxCP MCP bridge calls.
The repository smoke tests run with the system Python, so MCP dependencies are
imported only after a network subcommand is selected. For live bridge calls, run
this with the Python environment that has the `mcp` package installed.
"""
from __future__ import annotations
import argparse
import json
import sys
from typing import Any
DEFAULT_BRIDGE_URL = "http://192.168.1.12:9188/mcp"
def _json_loads(value: str) -> dict[str, Any]:
try:
parsed = json.loads(value)
except json.JSONDecodeError as exc:
raise argparse.ArgumentTypeError(str(exc)) from exc
if not isinstance(parsed, dict):
raise argparse.ArgumentTypeError("arguments JSON must decode to an object")
return parsed
def _json_default(value: Any) -> Any:
if hasattr(value, "model_dump"):
return value.model_dump(mode="json")
if hasattr(value, "__dict__"):
return value.__dict__
return str(value)
async def _list_tools(bridge_url: str) -> int:
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client(bridge_url) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = (await session.list_tools()).tools
for tool in tools:
print(tool.name)
return 0
async def _call_tool(bridge_url: str, tool_name: str, arguments: dict[str, Any]) -> int:
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
async with streamablehttp_client(bridge_url) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(tool_name, arguments)
print(json.dumps(result, ensure_ascii=True, indent=2, default=_json_default))
return 0
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--bridge-url",
default=DEFAULT_BRIDGE_URL,
help=f"MCP bridge URL. Default: {DEFAULT_BRIDGE_URL}",
)
subparsers = parser.add_subparsers(dest="command", required=True)
subparsers.add_parser("list-tools", help="List available MCP tool names.")
call_parser = subparsers.add_parser("call-tool", help="Call one MCP tool.")
call_parser.add_argument("tool_name", help="Tool name to call.")
call_parser.add_argument(
"--arguments-json",
type=_json_loads,
default={},
help="JSON object with tool arguments.",
)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
import anyio
except ImportError as exc:
raise SystemExit(
"sxcp_mcp_client requires the MCP Python environment for network calls; "
"try /media/p5/miniforge3/bin/python."
) from exc
if args.command == "list-tools":
return anyio.run(_list_tools, args.bridge_url)
if args.command == "call-tool":
return anyio.run(_call_tool, args.bridge_url, args.tool_name, args.arguments_json)
parser.error(f"unknown command: {args.command}")
return 2
if __name__ == "__main__":
raise SystemExit(main())
+541
View File
@@ -0,0 +1,541 @@
#!/usr/bin/env python3
"""Prepare repeatable SxCP prompt-probe batches without opening the MCP bridge.
The live bridge call remains `tools/sxcp_mcp_client.py`. This helper keeps the
batch plan and image-presence checklist deterministic so prompt-axis probes are
less dependent on hand-copied commands.
"""
from __future__ import annotations
import argparse
from datetime import date
import json
import shlex
import subprocess
import sys
import time
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
APPROVED_PYTHON = "/media/p5/miniforge3/bin/python"
MCP_HELPER = "tools/sxcp_mcp_client.py"
DEFAULT_OUT_CHANNEL = "sxcp_eval_out"
DEFAULT_IN_CHANNEL = "sxcp_eval_in"
NEGATIVE_OUT_CHANNEL = "sxcp_eval_negative_out"
PROMPT_ORDERS = {"subject_first", "geometry_only", "prompt_order_test"}
class BatchError(ValueError):
pass
def _load_json(path: Path) -> dict[str, Any]:
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, dict):
raise BatchError("batch JSON must contain one object")
return data
def _load_json_list(path: Path) -> list[Any]:
with path.open("r", encoding="utf-8") as handle:
data = json.load(handle)
if not isinstance(data, list):
raise BatchError("mock pulls JSON must contain a list")
return data
def _json_arg(value: dict[str, Any]) -> str:
return shlex.quote(json.dumps(value, ensure_ascii=True, separators=(",", ":")))
def _text(value: Any) -> str:
return "" if value is None else str(value).strip()
def _validate_no_negative_channel(value: Any, *, field: str) -> None:
text = _text(value)
if text == NEGATIVE_OUT_CHANNEL:
raise BatchError(f"{field} must not use {NEGATIVE_OUT_CHANNEL}")
if NEGATIVE_OUT_CHANNEL in text:
raise BatchError(f"{field} must not mention {NEGATIVE_OUT_CHANNEL}")
def _validate_probe(raw: Any, index: int) -> dict[str, str]:
if not isinstance(raw, dict):
raise BatchError(f"probes[{index}] must be an object")
for forbidden in ("negative", "negative_prompt", "negative_text", "negative_channel"):
if forbidden in raw:
raise BatchError(f"probes[{index}] must not contain {forbidden}")
probe_id = _text(raw.get("id"))
if not probe_id:
raise BatchError(f"probes[{index}].id is required")
prompt_order = _text(raw.get("prompt_order") or "subject_first")
if prompt_order not in PROMPT_ORDERS:
raise BatchError(f"probes[{index}].prompt_order must be one of {sorted(PROMPT_ORDERS)}")
text = _text(raw.get("text"))
if not text:
raise BatchError(f"probes[{index}].text is required")
_validate_no_negative_channel(text, field=f"probes[{index}].text")
return {"id": probe_id, "prompt_order": prompt_order, "text": text}
def _validate_image_path(value: Any, *, field: str) -> str:
path_text = _text(value)
if not path_text:
raise BatchError(f"{field} is required")
path = Path(path_text)
if not path.is_absolute():
raise BatchError(f"{field} must be absolute")
if path.suffix.lower() != ".png":
raise BatchError(f"{field} must reference a PNG artifact")
return path_text
def load_batch(path: Path) -> dict[str, Any]:
batch = _load_json(path)
for forbidden in ("negative", "negative_prompt", "negative_text", "negative_channel"):
if forbidden in batch:
raise BatchError(f"batch must not contain {forbidden}")
seed = batch.get("seed")
if not isinstance(seed, int):
raise BatchError("seed must be an integer sampler seed")
channel_out = _text(batch.get("channel_out") or DEFAULT_OUT_CHANNEL)
channel_in = _text(batch.get("channel_in") or DEFAULT_IN_CHANNEL)
_validate_no_negative_channel(channel_out, field="channel_out")
_validate_no_negative_channel(channel_in, field="channel_in")
probes_raw = batch.get("probes")
if not isinstance(probes_raw, list) or not probes_raw:
raise BatchError("probes must be a non-empty list")
probes = [_validate_probe(raw, index) for index, raw in enumerate(probes_raw)]
return {
"seed": seed,
"channel_out": channel_out,
"channel_in": channel_in,
"probes": probes,
}
def load_results(path: Path) -> dict[str, Any]:
data = _load_json(path)
seed = data.get("seed")
if not isinstance(seed, int):
raise BatchError("result seed must be an integer sampler seed")
channel_in = _text(data.get("channel_in") or DEFAULT_IN_CHANNEL)
_validate_no_negative_channel(channel_in, field="channel_in")
probes_raw = data.get("probes")
if not isinstance(probes_raw, list) or not probes_raw:
raise BatchError("result probes must be a non-empty list")
probes: list[dict[str, Any]] = []
for index, raw in enumerate(probes_raw):
if not isinstance(raw, dict):
raise BatchError(f"result probes[{index}] must be an object")
probe_id = _text(raw.get("id"))
if not probe_id:
raise BatchError(f"result probes[{index}].id is required")
prompt_order = _text(raw.get("prompt_order") or "subject_first")
if prompt_order not in PROMPT_ORDERS:
raise BatchError(f"result probes[{index}].prompt_order must be one of {sorted(PROMPT_ORDERS)}")
turn = raw.get("turn")
if turn is not None and (not isinstance(turn, int) or isinstance(turn, bool)):
raise BatchError(f"result probes[{index}].turn must be an integer when present")
returned_seed = raw.get("returned_seed")
if returned_seed is not None and (not isinstance(returned_seed, int) or isinstance(returned_seed, bool)):
raise BatchError(f"result probes[{index}].returned_seed must be an integer when present")
image_path = _text(raw.get("image_path"))
probes.append(
{
"id": probe_id,
"prompt_order": prompt_order,
"turn": turn,
"image_path": image_path,
"returned_seed": returned_seed,
}
)
return {"seed": seed, "channel_in": channel_in, "probes": probes}
def print_push_commands(batch: dict[str, Any]) -> None:
for index, probe in enumerate(batch["probes"], start=1):
prompt_order = probe["prompt_order"]
caveat = "geometry-only: pose-axis discovery; not subject/look-controlled" if prompt_order == "geometry_only" else prompt_order
print(f"# {index}/{len(batch['probes'])} {probe['id']} ({caveat})")
push_args = {
"channel": batch["channel_out"],
"seed": batch["seed"],
"text": probe["text"],
}
print(f"{APPROVED_PYTHON} {MCP_HELPER} call-tool comfy_push --arguments-json {_json_arg(push_args)}")
pull_args = {"channel": batch["channel_in"]}
print(f"{APPROVED_PYTHON} {MCP_HELPER} call-tool comfy_pull --arguments-json {_json_arg(pull_args)}")
def print_result_template(batch: dict[str, Any]) -> None:
template = {
"seed": batch["seed"],
"channel_in": batch["channel_in"],
"probes": [
{
"id": probe["id"],
"prompt_order": probe["prompt_order"],
"turn": None,
"image_path": "",
"returned_seed": None,
}
for probe in batch["probes"]
],
}
print(json.dumps(template, ensure_ascii=True, indent=2))
def _probe_by_id(probes: list[dict[str, Any]], probe_id: str, *, label: str) -> dict[str, Any]:
for probe in probes:
if probe.get("id") == probe_id:
return probe
raise BatchError(f"{label} {probe_id!r} was not found")
def validate_results(batch: dict[str, Any], results: dict[str, Any]) -> None:
if results["seed"] != batch["seed"]:
raise BatchError(f"result seed {results['seed']} does not match batch seed {batch['seed']}")
batch_probe_ids = [probe["id"] for probe in batch["probes"]]
result_probe_ids = [probe["id"] for probe in results["probes"]]
if result_probe_ids != batch_probe_ids:
raise BatchError(f"result probe ids must match batch probe ids in order: expected {batch_probe_ids}, got {result_probe_ids}")
turns: list[int] = []
for index, (batch_probe, result_probe) in enumerate(zip(batch["probes"], results["probes"])):
expected_order = batch_probe["prompt_order"]
if result_probe["prompt_order"] != expected_order:
raise BatchError(
f"result probes[{index}].prompt_order must match batch prompt_order {expected_order!r}"
)
turn = result_probe.get("turn")
if not isinstance(turn, int) or isinstance(turn, bool):
raise BatchError(f"result probes[{index}].turn is required and must be an integer")
turns.append(turn)
_validate_image_path(result_probe.get("image_path"), field=f"result probes[{index}].image_path")
returned_seed = result_probe.get("returned_seed")
if returned_seed != batch["seed"]:
raise BatchError(
f"result probes[{index}].returned_seed must match batch seed {batch['seed']}"
)
if len(set(turns)) != len(turns):
raise BatchError("result probe turns must be unique")
if turns != sorted(turns):
raise BatchError("result probe turns must be in batch order")
def _payload_from_mcp_response(data: Any) -> dict[str, Any]:
if isinstance(data, dict) and ("turn" in data or "image_path" in data or "seed" in data):
return data
if not isinstance(data, dict):
raise BatchError("MCP response must be an object")
content = data.get("content")
if not isinstance(content, list):
raise BatchError("MCP response did not contain content")
for item in content:
if not isinstance(item, dict):
continue
text = _text(item.get("text"))
if not text:
continue
try:
payload = json.loads(text)
except json.JSONDecodeError as exc:
raise BatchError(f"MCP content text was not JSON: {exc}") from exc
if isinstance(payload, dict):
return payload
raise BatchError("MCP response did not contain a JSON payload")
def load_mock_pulls(path: Path) -> list[dict[str, Any]]:
return [_payload_from_mcp_response(item) for item in _load_json_list(path)]
def _call_mcp_tool(tool_name: str, arguments: dict[str, Any]) -> dict[str, Any]:
result = subprocess.run(
[
APPROVED_PYTHON,
MCP_HELPER,
"call-tool",
tool_name,
"--arguments-json",
json.dumps(arguments, ensure_ascii=True, separators=(",", ":")),
],
cwd=ROOT,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
detail = result.stderr.strip() or result.stdout.strip() or f"exit {result.returncode}"
raise BatchError(f"MCP {tool_name} failed: {detail}")
try:
data = json.loads(result.stdout)
except json.JSONDecodeError as exc:
raise BatchError(f"MCP {tool_name} returned non-JSON output: {exc}") from exc
return _payload_from_mcp_response(data)
def _result_probe_from_payload(
probe: dict[str, Any],
payload: dict[str, Any],
*,
expected_seed: int,
previous_turn: int,
) -> dict[str, Any] | None:
turn = payload.get("turn")
if not isinstance(turn, int) or isinstance(turn, bool):
raise BatchError(f"pull result for {probe['id']} must contain an integer turn")
if turn <= previous_turn:
return None
image_path = _validate_image_path(payload.get("image_path"), field=f"pull result for {probe['id']}.image_path")
returned_seed = payload.get("seed")
if not isinstance(returned_seed, int) or isinstance(returned_seed, bool):
raise BatchError(f"pull result for {probe['id']} must contain an integer seed")
if returned_seed != expected_seed:
raise BatchError(f"pull result for {probe['id']} returned seed {returned_seed}, expected {expected_seed}")
return {
"id": probe["id"],
"prompt_order": probe["prompt_order"],
"turn": turn,
"image_path": image_path,
"returned_seed": returned_seed,
}
def run_batch(
batch: dict[str, Any],
*,
result_path: Path,
previous_turn: int,
max_polls: int,
poll_interval: float,
run_live: bool = False,
mock_pulls: list[dict[str, Any]] | None = None,
) -> dict[str, Any]:
if max_polls <= 0:
raise BatchError("max_polls must be positive")
if poll_interval < 0:
raise BatchError("poll_interval must be non-negative")
if run_live and mock_pulls is not None:
raise BatchError("--run and --mock-pulls-json cannot be used together")
if not run_live and mock_pulls is None:
raise BatchError("run_batch requires live mode or mock pulls")
mock_index = 0
result_probes: list[dict[str, Any]] = []
current_turn = previous_turn
for probe in batch["probes"]:
if run_live:
_call_mcp_tool(
"comfy_push",
{"channel": batch["channel_out"], "seed": batch["seed"], "text": probe["text"]},
)
for poll_index in range(max_polls):
if mock_pulls is not None:
if mock_index >= len(mock_pulls):
raise BatchError(f"mock pulls exhausted while waiting for {probe['id']}")
payload = mock_pulls[mock_index]
mock_index += 1
else:
payload = _call_mcp_tool("comfy_pull", {"channel": batch["channel_in"]})
result_probe = _result_probe_from_payload(
probe,
payload,
expected_seed=batch["seed"],
previous_turn=current_turn,
)
if result_probe is not None:
result_probes.append(result_probe)
current_turn = result_probe["turn"]
break
if run_live and poll_index < max_polls - 1:
time.sleep(poll_interval)
else:
raise BatchError(f"no new result for {probe['id']} after {max_polls} polls")
results = {"seed": batch["seed"], "channel_in": batch["channel_in"], "probes": result_probes}
validate_results(batch, results)
result_path.write_text(json.dumps(results, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
return results
def _entry_id_slug(value: str) -> str:
chars = [char.lower() if char.isalnum() else "-" for char in value]
slug = "".join(chars).strip("-")
while "--" in slug:
slug = slug.replace("--", "-")
return slug or "sxcp-batch"
def eval_entry_draft(
batch: dict[str, Any],
results: dict[str, Any],
*,
variant_key: str,
entry_id: str,
baseline_image: str,
candidate_id: str,
source: str,
result: str,
decision: str,
entry_date: str,
allow_geometry_only: bool = False,
) -> dict[str, Any]:
validate_results(batch, results)
batch_probe = _probe_by_id(batch["probes"], candidate_id, label="candidate probe")
result_probe = _probe_by_id(results["probes"], candidate_id, label="candidate result")
candidate_image = _validate_image_path(result_probe.get("image_path"), field="candidate image_path")
baseline = _validate_image_path(baseline_image, field="baseline_image")
prompt_order = batch_probe["prompt_order"]
turn = result_probe.get("turn")
returned_seed = result_probe.get("returned_seed")
if returned_seed is not None and returned_seed != batch["seed"]:
raise BatchError(f"candidate returned_seed {returned_seed} does not match batch seed {batch['seed']}")
if prompt_order == "geometry_only" and not allow_geometry_only:
raise BatchError("candidate prompt_order is geometry_only; rerun with --allow-geometry-only to draft non-controlled prompt-axis evidence")
order_note = (
"subject/look-controlled candidate"
if prompt_order == "subject_first"
else "geometry-only prompt-order probe; do not treat as subject/look-controlled evidence"
if prompt_order == "geometry_only"
else "prompt-order sensitivity probe"
)
entry = {
"id": entry_id or f"{_entry_id_slug(variant_key)}-{batch['seed']}-{_entry_id_slug(candidate_id)}",
"date": entry_date,
"variant_key": variant_key,
"seed": batch["seed"],
"source": source,
"result": result,
"decision": decision,
"baseline_prompt_summary": f"Replace with the same-seed baseline summary for {variant_key}.",
"candidate_prompt_summary": (
f"Batch candidate {candidate_id!r} used prompt_order={prompt_order!r}; "
f"replace with the pose-axis change and controlled variables."
),
"observation": (
f"Replace with image comparison for candidate {candidate_id!r}"
f"{f' on turn {turn}' if turn is not None else ''}. Prompt-order note: {order_note}."
),
"baseline_image": baseline,
"candidate_image": candidate_image,
"commit": "pending",
}
return entry
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description=__doc__)
subparsers = parser.add_subparsers(dest="command", required=True)
for command in ("validate", "print-push-commands", "print-result-template"):
subparser = subparsers.add_parser(command)
subparser.add_argument("--batch-json", required=True, help="Path to the prompt batch JSON file.")
validate_results_parser = subparsers.add_parser("validate-results")
validate_results_parser.add_argument("--batch-json", required=True, help="Path to the prompt batch JSON file.")
validate_results_parser.add_argument("--result-json", required=True, help="Path to a filled result template JSON file.")
run_parser = subparsers.add_parser("run-batch")
run_parser.add_argument("--batch-json", required=True, help="Path to the prompt batch JSON file.")
run_parser.add_argument("--result-json", required=True, help="Path where the filled result JSON should be written.")
run_parser.add_argument("--run", action="store_true", help="Call the live MCP helper. Omit for dry-run or mock mode.")
run_parser.add_argument("--mock-pulls-json", help="Path to a JSON list of mocked sxcp_eval_in payloads for local testing.")
run_parser.add_argument("--previous-turn", type=int, default=0, help="Ignore pulls at or below this turn before the first probe.")
run_parser.add_argument("--max-polls", type=int, default=60, help="Maximum pull attempts per probe.")
run_parser.add_argument("--poll-interval", type=float, default=2.0, help="Seconds to wait between live pull attempts.")
draft_parser = subparsers.add_parser("print-eval-entry-draft")
draft_parser.add_argument("--batch-json", required=True, help="Path to the prompt batch JSON file.")
draft_parser.add_argument("--result-json", required=True, help="Path to a filled result template JSON file.")
draft_parser.add_argument("--variant-key", required=True, help="Catalog variant key for the eval entry.")
draft_parser.add_argument("--entry-id", default="", help="Durable eval entry id. Defaults to a generated id.")
draft_parser.add_argument("--baseline-image", required=True, help="Absolute PNG path for the baseline image.")
draft_parser.add_argument("--candidate-id", required=True, help="Probe id to use as the candidate image.")
draft_parser.add_argument("--source", default="sxcp_eval_mcp_batch", help="Source label for the eval entry.")
draft_parser.add_argument("--result", default="inconclusive", help="Eval result. Default: inconclusive.")
draft_parser.add_argument("--decision", default="needs_more_tests", help="Eval decision. Default: needs_more_tests.")
draft_parser.add_argument("--date", default=date.today().isoformat(), help="Eval entry date.")
draft_parser.add_argument(
"--allow-geometry-only",
action="store_true",
help="Allow drafting an entry from a geometry_only probe. Use only for non-controlled prompt-axis evidence.",
)
return parser
def main(argv: list[str] | None = None) -> int:
parser = build_parser()
args = parser.parse_args(argv)
try:
batch = load_batch(Path(args.batch_json))
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
if args.command == "validate":
print(f"validated: {len(batch['probes'])} probes, seed {batch['seed']}")
return 0
if args.command == "print-push-commands":
print_push_commands(batch)
return 0
if args.command == "print-result-template":
print_result_template(batch)
return 0
if args.command == "validate-results":
try:
results = load_results(Path(args.result_json))
validate_results(batch, results)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"validated results: {len(batch['probes'])} probes, seed {batch['seed']}")
return 0
if args.command == "run-batch":
try:
if not args.run and not args.mock_pulls_json:
print(f"dry-run: {len(batch['probes'])} probes, seed {batch['seed']}")
print_push_commands(batch)
return 0
mock_pulls = load_mock_pulls(Path(args.mock_pulls_json)) if args.mock_pulls_json else None
results = run_batch(
batch,
result_path=Path(args.result_json),
previous_turn=args.previous_turn,
max_polls=args.max_polls,
poll_interval=args.poll_interval,
run_live=args.run,
mock_pulls=mock_pulls,
)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(f"recorded results: {len(results['probes'])} probes, seed {results['seed']} -> {args.result_json}")
return 0
if args.command == "print-eval-entry-draft":
try:
results = load_results(Path(args.result_json))
entry = eval_entry_draft(
batch,
results,
variant_key=args.variant_key,
entry_id=args.entry_id,
baseline_image=args.baseline_image,
candidate_id=args.candidate_id,
source=args.source,
result=args.result,
decision=args.decision,
entry_date=args.date,
allow_geometry_only=args.allow_geometry_only,
)
except Exception as exc:
print(f"error: {exc}", file=sys.stderr)
return 1
print(json.dumps(entry, ensure_ascii=True, indent=2))
return 0
parser.error(f"unknown command: {args.command}")
return 2
if __name__ == "__main__":
raise SystemExit(main())