Add Krea2 POV routing and eval tooling

This commit is contained in:
2026-06-30 19:28:10 +02:00
parent 284c6279e6
commit f5ba07e340
29 changed files with 6331 additions and 400 deletions
+121 -88
View File
@@ -118,16 +118,22 @@
"atlas_folders": ["ballsucking"],
"action_family": "outercourse",
"position_keys": ["testicle_sucking", "ballsucking"],
"canonical_geometry": "Low first-person pelvis view: the woman bends forward between the viewer's open thighs with her chest low over his pelvis, head below the shaft at testicle height, mouth and tongue at the balls, and the penis pointing upward above or in front of her face.",
"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",
"face is below the viewer's penis at testicle height",
"mouth and tongue licking the viewer's balls",
"penis points upward in the lower foreground above her forehead"
"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"
],
@@ -140,26 +146,28 @@
"route_terms": ["testicle_sucking", "balls licking", "testicle"]
},
"evidence": {
"fixed_seed_tests": [],
"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": "Atlas supports low-head geometry, but this route still needs controlled fixed-seed tests before promotion to proven."
"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": "candidate",
"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, penis upright near the center, and the woman sits opposite with both soles and toes pressing around the shaft while her body and face stay behind the feet.",
"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",
"both soles press around the upright penis",
"toes curl around the shaft as the feet stroke from base toward glans",
"woman's body and face remain visible behind her feet"
"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",
@@ -176,9 +184,9 @@
"route_terms": ["footjob", "foot job", "feet stroking"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Small atlas family with consistent frontal sole-contact geometry; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -221,16 +229,17 @@
{
"key": "pov_wand_foreground_tool_contact",
"family": "wand",
"status": "candidate",
"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 wand-style toy from the foreground with the rounded head pressed to the central contact point.",
"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 toy head is pressed to the central contact point between her open thighs",
"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"
],
@@ -251,9 +260,9 @@
"route_terms": ["wand", "toy", "vibrator"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "The wand folder repeats a foreground-hand toy-contact layout with open thighs and the visible partner behind the tool; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -268,7 +277,7 @@
"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 is still after ejaculation rather than actively thrusting",
"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"
@@ -291,9 +300,9 @@
"route_terms": ["post-ejaculation open-thigh display", "thick visible semen or fluid", "open thighs"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"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; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -328,9 +337,9 @@
"route_terms": ["spread", "open thighs", "legs spread"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows a repeated open-thigh presentation/setup pose; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -381,22 +390,25 @@
"status": "candidate",
"atlas_folders": ["blowjob_top_view"],
"action_family": "oral",
"position_keys": ["reclining_oral", "penis_licking"],
"canonical_geometry": "Top-down first-person oral view: the viewer looks down from chest or pelvis height, viewer torso or thighs stay at the lower edge, shaft is vertical and centered in the foreground, and the woman kneels below looking upward with mouth and hand aligned to the shaft.",
"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": [
"POV top-down oral position",
"viewer looks down from chest or pelvis height",
"viewer torso or thighs stay at the lower edge",
"shaft is vertical and centered in the foreground",
"woman kneels below the viewer looking upward",
"her mouth and hand align to the centered shaft"
"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"
"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",
@@ -405,36 +417,43 @@
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["blowjob_top_view", "top-down oral", "oral"]
"route_terms": ["kneeling oral", "top-down oral", "oral"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows a repeated top-down oral POV with a centered vertical shaft; needs fixed-seed Krea2 tests before promotion to proven."
"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": "candidate",
"status": "proven",
"atlas_folders": ["blowjob_side"],
"action_family": "oral",
"position_keys": ["reclining_oral", "penis_licking"],
"canonical_geometry": "Side-profile first-person oral view: the viewer reclines with torso or thighs visible in the foreground, 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.",
"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 position",
"viewer reclines with torso or thighs visible in the foreground",
"woman leans beside the viewer's pelvis from the side",
"her side-facing mouth aligns to the shaft",
"shaft stays near the lower center of the frame",
"side-facing face, jawline, hand support, and mouth contact remain readable"
"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"
"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",
@@ -443,12 +462,12 @@
],
"generator_hook": {
"module": "krea_pov_actions.py",
"route_terms": ["blowjob_side", "side-profile oral", "oral"]
"route_terms": ["side_lying", "side-lying oral", "blowjob_side", "side-profile oral", "oral"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated side-profile first-person oral geometry; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -484,9 +503,9 @@
"route_terms": ["blowjob_laying", "prone frontal oral", "oral"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated prone, front-facing first-person oral geometry; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -495,15 +514,15 @@
"status": "candidate",
"atlas_folders": ["blowjob_sitting"],
"action_family": "oral",
"position_keys": ["reclining_oral", "penis_licking"],
"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 upright between the viewer's open thighs",
"her shoulders and face stay vertical and close to the camera",
"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 mouth aligns to the centered shaft with hands low near the base"
"her open mouth covers the centered tip with hands wrapped low at the base"
],
"avoid_cues": [
"prone belly-down oral framing",
@@ -522,9 +541,9 @@
"route_terms": ["blowjob_sitting", "upright sitting oral", "oral"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas contains some top-view outliers, but the named sitting files show repeated upright, front-facing first-person oral geometry; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -541,7 +560,8 @@
"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"
"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",
@@ -560,9 +580,9 @@
"route_terms": ["missionary", "open-leg penetration", "front-entry"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated front-facing open-leg missionary geometry; keep separate from folded-leg missionary until fixed-seed Krea2 tests prove which wording is reliable."
"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."
}
},
{
@@ -571,11 +591,13 @@
"status": "candidate",
"atlas_folders": ["missionary_folded"],
"action_family": "penetration",
"position_keys": ["missionary", "folded_legs", "knees_to_chest", "front_entry"],
"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",
@@ -598,15 +620,15 @@
"route_terms": ["missionary_folded", "folded missionary", "knees-to-chest"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated high-leg folded missionary geometry; needs fixed-seed Krea2 tests before promotion to proven."
"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": "candidate",
"status": "proven",
"atlas_folders": ["5.cowgirl"],
"action_family": "penetration",
"position_keys": ["cowgirl", "frontal_straddle", "woman_on_top"],
@@ -615,6 +637,8 @@
"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"
@@ -636,9 +660,9 @@
"route_terms": ["cowgirl", "frontal cowgirl", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated frontal woman-on-top straddle geometry; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -647,13 +671,16 @@
"status": "candidate",
"atlas_folders": ["5.cowgirl_alt"],
"action_family": "penetration",
"position_keys": ["cowgirl", "frontal_straddle", "woman_on_top", "low_squat"],
"canonical_geometry": "Close first-person cowgirl-alt view: the viewer reclines below 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 viewer hands or thighs anchoring the lower foreground.",
"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"
],
@@ -676,9 +703,9 @@
"route_terms": ["cowgirl_alt", "low cowgirl", "seated-squat cowgirl", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated frontal woman-on-top geometry with a lower seated squat and closer thigh/hand anchors than the main cowgirl folder; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -692,9 +719,12 @@
"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": [
@@ -716,9 +746,9 @@
"route_terms": ["cowgirl_reverse", "reverse cowgirl", "back-facing straddle", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated reverse cowgirl geometry with the viewer underneath and the woman facing away; needs fixed-seed Krea2 tests before promotion to proven."
"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."
}
},
{
@@ -727,13 +757,16 @@
"status": "candidate",
"atlas_folders": ["cowgirl_reversere_alt"],
"action_family": "penetration",
"position_keys": ["reverse_cowgirl", "back_facing_straddle", "woman_on_top", "upright_seated"],
"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"
],
@@ -756,9 +789,9 @@
"route_terms": ["cowgirl_reversere_alt", "reverse cowgirl alt", "upright back-facing straddle", "woman-on-top"]
},
"evidence": {
"fixed_seed_tests": [],
"guide_section": "",
"notes": "Atlas shows repeated upright seated reverse cowgirl geometry; keep separate from the closer reverse cowgirl route until fixed-seed Krea2 tests prove whether one wording can cover both."
"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.
+1691 -1
View File
File diff suppressed because it is too large Load Diff
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

+117 -13
View File
@@ -5,6 +5,9 @@ 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
@@ -14,6 +17,76 @@ Confirmed findings become either generator changes or durable prompt rules in
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:
@@ -30,23 +103,30 @@ Every three minutes it prints a structured request asking Codex to:
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, or generator fix.
7. Change generator code/data only when the issue is systemic.
8. Record the finding and update the Krea2 prompt guide when a rule is confirmed.
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
seed, prompt summaries, observation, decision, and commit.
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 > /tmp/krea2-entry.json
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
```
@@ -59,6 +139,7 @@ Entry template:
"date": "2026-06-29",
"variant_key": "pov_example_variant",
"seed": 1234,
"generator_seed": 5678,
"source": "sxcp_eval_mcp",
"result": "accepted",
"decision": "generator_patch",
@@ -141,22 +222,45 @@ pelvis can be correct. Do not treat them as automatic failures.
## Seed Contract
The seed is transport metadata, not prompt text. When the graph emits a seed, an
A/B wording test should reuse that exact seed so the image difference mostly
comes from wording, not sampling randomness. If a payload has no seed, mark that
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
Only edit the generator when the image shows a repeatable, systemic prompt
failure. Examples:
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, send a cleaner prompt to `sxcp_eval_out` and keep the
generator unchanged. For repeated prompt behavior, update the generator and add
the rule to `docs/krea2-prompt-guide.md`.
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
+4 -1
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:
+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 chest low over the POV viewer's pelvis and 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 chest low over his pelvis 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:
+17 -2
View File
@@ -10,7 +10,13 @@ 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", "prompt_guide_rule", "prompt_only_retry", "needs_more_tests"}
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:
@@ -55,6 +61,7 @@ def entry_template(
variant_key: str,
*,
seed: int,
generator_seed: int | None = None,
source: str = "sxcp_eval_mcp",
date: str = "",
result: str = "inconclusive",
@@ -63,10 +70,12 @@ def entry_template(
) -> 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")
return {
entry = {
"id": f"{_entry_id_slug(variant)}-{seed}-eval",
"date": date,
"variant_key": variant,
@@ -81,6 +90,9 @@ def entry_template(
"candidate_image": "",
"commit": commit,
}
if generator_seed is not None:
entry["generator_seed"] = generator_seed
return entry
def validate_entry(
@@ -108,6 +120,9 @@ def validate_entry(
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:
+81 -1
View File
@@ -2,6 +2,7 @@ from __future__ import annotations
from collections import Counter
from pathlib import Path
import sys
from typing import Any
try:
@@ -31,11 +32,13 @@ def _latest_evidence(entries: list[dict[str, Any]], *, result: str | None = None
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 "",
}
@@ -228,6 +231,45 @@ def next_test_plans() -> list[dict[str, Any]]:
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():
@@ -261,14 +303,32 @@ def markdown_report(atlas_root: str | Path | None = None) -> str:
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}, {evidence.get('decision') or 'unknown'}, commit {commit})"
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(
@@ -294,6 +354,16 @@ def markdown_report(atlas_root: str | Path | None = None) -> str:
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"])
@@ -344,3 +414,13 @@ def markdown_report(atlas_root: str | Path | None = None) -> str:
]
)
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:]))
+8 -2
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:
@@ -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"
)
+138 -12
View File
@@ -89,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:
@@ -100,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,
@@ -251,6 +293,8 @@ def pov_hardcore_pose_sentence(
"anal",
"cowgirl",
"missionary",
"knees-to-chest",
"knees to chest",
"doggy",
"rear-entry",
"spooning",
@@ -262,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
@@ -286,10 +343,22 @@ def pov_hardcore_pose_sentence(
)
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 chest low over the viewer's pelvis and 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, "
@@ -305,8 +374,9 @@ def pov_hardcore_pose_sentence(
)
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"
@@ -319,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; "
@@ -396,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(
@@ -416,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(
@@ -449,10 +569,16 @@ 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 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(
"POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; "
+215
View File
@@ -8,9 +8,12 @@ try:
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
@@ -18,9 +21,12 @@ except ImportError: # Allows local smoke tests from the repository root.
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,
)
@@ -34,6 +40,19 @@ 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":
@@ -46,6 +65,90 @@ def _variant_positions(variant):
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):
@@ -142,6 +245,104 @@ class SxCPKrea2PoseVariant:
)
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):
@@ -258,6 +459,13 @@ 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,
}
@@ -265,5 +473,12 @@ 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",
}
+4 -4
View File
@@ -444,15 +444,15 @@ def _pov_clothing_sentence(clothing: str, needs_lower_access: bool) -> str:
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 are visible only as first-person body cues"
return "POV foreground body cue: the viewer's bare hands, forearms, or torso edge are visible only as first-person body cues"
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}, visible only as the viewer's hands, hips, thighs, or lowered waistband"
f"POV foreground clothing cue: {clothing}, appearing as the viewer's hands, hips, thighs, or lowered waistband"
)
return (
f"POV foreground clothing cue: {clothing}, visible only as the viewer's hands, forearms, sleeves, or torso edge"
f"POV foreground clothing cue: {clothing}, appearing as the viewer's hands, forearms, sleeves, or torso edge"
)
+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 (
+2
View File
@@ -30,6 +30,7 @@ def main() -> int:
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.")
@@ -43,6 +44,7 @@ def main() -> int:
entry = krea2_eval_log.entry_template(
args.variant_key,
seed=args.seed,
generator_seed=args.generator_seed,
source=args.source,
date=args.date,
)
+1653 -240
View File
File diff suppressed because it is too large Load Diff
+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())