Compare commits
39 Commits
d937c219ee
...
f5ba07e340
| Author | SHA1 | Date | |
|---|---|---|---|
| f5ba07e340 | |||
| 284c6279e6 | |||
| 364c42103b | |||
| 49d130467b | |||
| 6a37c807bc | |||
| 2aafab03bd | |||
| 1e9794eed0 | |||
| 3467acbd6a | |||
| b8e15289ca | |||
| 03907439a4 | |||
| e028419e6d | |||
| 05f14cecc7 | |||
| 43a71c2353 | |||
| f937d3c109 | |||
| b41d140927 | |||
| f73eb72d68 | |||
| f855c7b022 | |||
| 2a29fcdfbb | |||
| 607c612196 | |||
| 8ff02a181b | |||
| 00e371e4b6 | |||
| 858fbe8d46 | |||
| d77e7631da | |||
| e96b9e9aae | |||
| 5a5d5dd6fe | |||
| 06525c42a3 | |||
| 3a09210f71 | |||
| 333f4752f6 | |||
| fae5423513 | |||
| d384cb8a46 | |||
| 742281f48f | |||
| 40ee843baf | |||
| 484fb40638 | |||
| a484783515 | |||
| 11b7c2acf9 | |||
| bb53967df4 | |||
| ef3b983712 | |||
| 0328e5ca3a | |||
| 54617e4702 |
@@ -2,3 +2,4 @@ __pycache__/
|
|||||||
*.py[cod]
|
*.py[cod]
|
||||||
.pytest_cache/
|
.pytest_cache/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
|
.sxcp_eval/
|
||||||
|
|||||||
@@ -0,0 +1,798 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"atlas_root": "/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2",
|
||||||
|
"purpose": "Machine-readable Krea2 POV pose-geometry catalog for fixed-seed SxCP prompt tuning.",
|
||||||
|
"status_values": {
|
||||||
|
"proven": "A route has atlas support and repeated or structural Krea2 evidence strong enough for generator defaults.",
|
||||||
|
"candidate": "A route has atlas support but needs more fixed-seed Krea2 tests before changing generator defaults.",
|
||||||
|
"unstable": "A route has known text-only limits and should prefer control images or a narrower variant."
|
||||||
|
},
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"key": "pov_doggy_top_down_rear_entry",
|
||||||
|
"family": "doggy",
|
||||||
|
"status": "proven",
|
||||||
|
"atlas_folders": ["doggy", "doggy_alt"],
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_keys": ["doggy", "rear_entry", "on_all_fours"],
|
||||||
|
"canonical_geometry": "Top-down first-person rear-entry view from behind: viewer body cues at the bottom, hands near the woman's hips, woman on all fours with chest low, forearms folded, cheek turned sideways far ahead, back arched, and hips raised toward the camera.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"top-down POV doggy position from behind",
|
||||||
|
"camera looks down over the viewer's hands onto the woman's raised hips",
|
||||||
|
"woman is on all fours with chest low, forearms folded, cheek turned sideways",
|
||||||
|
"back arched, hips raised high toward the camera",
|
||||||
|
"natural lower-body POV cues in the foreground"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"visible shoes or lower legs as the standing cue",
|
||||||
|
"viewer torso and thighs outside frame",
|
||||||
|
"face or mouth as the fluid target for rear-entry climax"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"doggy/65_doggy.png",
|
||||||
|
"doggy_alt/100_doggy_alt.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["doggy", "all fours", "rear-entry"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["65", "52", "5202"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#pov-doggy--rear-entry",
|
||||||
|
"notes": "Visible viewer thighs, torso, or pelvis can be correct; shoes/lower-leg wording caused oral drift."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_boobjob_upright_cleavage",
|
||||||
|
"family": "boobjob",
|
||||||
|
"status": "proven",
|
||||||
|
"atlas_folders": ["boobjob"],
|
||||||
|
"action_family": "outercourse",
|
||||||
|
"position_keys": ["boobjob", "titjob", "breast_sex"],
|
||||||
|
"canonical_geometry": "Frontal upright first-person view: viewer reclines with thighs open while the woman faces him between his legs, breasts pressed together around a vertical shaft, glans above the cleavage near her mouth.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV boobjob position",
|
||||||
|
"woman kneels upright between his legs facing him",
|
||||||
|
"penis rises vertically in the lower foreground",
|
||||||
|
"squeezed between her pressed-together breasts",
|
||||||
|
"woman's own fingers and nails cup her breasts from the outside",
|
||||||
|
"glans emerging above the cleavage directly below her mouth"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"torso bent forward over his pelvis",
|
||||||
|
"both hands push her breasts without naming whose hands",
|
||||||
|
"only foreground hands when the woman's hands are the intended hands"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"boobjob/100_boobjob.png",
|
||||||
|
"boobjob/18_boobjob.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["boobjob", "titjob", "breast sex"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["7301", "7302"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#boobjob--titjob",
|
||||||
|
"notes": "Same-seed A/B showed upright cleavage-sleeve wording improves contact pressure; hand ownership must be explicit."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_handjob_upright_centered",
|
||||||
|
"family": "handjob",
|
||||||
|
"status": "proven",
|
||||||
|
"atlas_folders": ["handjob"],
|
||||||
|
"action_family": "outercourse",
|
||||||
|
"position_keys": ["handjob"],
|
||||||
|
"canonical_geometry": "Centered first-person view: viewer reclines with thighs open, the woman faces him between his legs, and the woman's hand is the main contact anchor on the shaft with her face and torso behind it.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV handjob position",
|
||||||
|
"woman kneels between his legs facing him",
|
||||||
|
"the woman's right hand wraps around the viewer's penis",
|
||||||
|
"her left hand steadies the base",
|
||||||
|
"viewer thighs and pelvis frame the lower edges",
|
||||||
|
"without his hands covering the action"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"generic one hand grips when hand ownership matters",
|
||||||
|
"foreground hands competing with the woman's active hand"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"handjob/18_handjob.png",
|
||||||
|
"handjob/92_handjob.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["handjob", "hand job", "hand stroking"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["7401"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#handjob",
|
||||||
|
"notes": "Same-seed A/B showed explicit woman-hand ownership removed viewer-hand ambiguity."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_ballsucking_low_head",
|
||||||
|
"family": "ballsucking",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["ballsucking"],
|
||||||
|
"action_family": "outercourse",
|
||||||
|
"position_keys": ["testicle_sucking", "ballsucking"],
|
||||||
|
"canonical_geometry": "Low first-person pelvis view: the woman stays low beside or between the viewer's open thighs, with cheek/thigh proximity, the scrotum as the mouth surface, scrotal skin as the nearest mouth surface, and testicles resting across her open lips while both testicles rest against her tongue from below as the accepted partial target.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"woman bends forward and kneels very low between the viewer's open thighs",
|
||||||
|
"chest low over the viewer's pelvis",
|
||||||
|
"low side-pelvis POV",
|
||||||
|
"face is the closest visible partner part",
|
||||||
|
"cheek against the viewer's inner thigh",
|
||||||
|
"scrotum is the mouth surface",
|
||||||
|
"scrotal skin is the nearest mouth surface",
|
||||||
|
"testicles resting across her open lips while her tongue cups them from below",
|
||||||
|
"both testicles rest against her tongue from below",
|
||||||
|
"viewer abdomen and inner thighs frame the close foreground"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"head tucked under the penis shaft without testicle-height wording",
|
||||||
|
"repeating shaft/hand-on-shaft wording before scrotum/testicle contact is established",
|
||||||
|
"viewer first as the main subject",
|
||||||
|
"mid-height head placement"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"ballsucking/101_ballsucking.png",
|
||||||
|
"ballsucking/4_ballsucking.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["testicle_sucking", "balls licking", "testicle"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["238365845574312", "1212121212", "5757575757", "6262626262", "9797979797", "9898989898", "5959595959", "6060606060", "6161616161", "7171717171", "7272727272"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#ballsucking--testicle-sucking",
|
||||||
|
"notes": "Fifty-probe threshold search accepted tongue/lips on testicles as a partial improvement over baseline shaft/glans collapse; generator carried the side-low partial axis provisionally. Fresh seed 6262626262 then showed open-lips scrotum-surface wording on turns 252 and 258 improved target contact over the generated-route controls 250 and 256. Fresh seed 9797979797 repeated the scrotal-skin target-object branch on turns 288 and 293, with scrotal skin as the nearest mouth surface and both testicles resting against tongue from below. Fresh seed 9898989898 validated the patched generated route on turns 296 and 297, preserving side-low cheek/thigh geometry while keeping scrotum/testicles at the tongue/lip contact. Fresh seed 5959595959 tested lip-oval, sideways mouth pocket, and chin-pelvis upward seal wording across three women; all branches kept some low-pelvis geometry but collapsed back toward shaft/glans contact, so record it as a weak case. Fresh seed 6060606060 tested foreground occlusion, under-scrotum tongue shelf, and hand-guided scrotum wording; every branch still became shaft-centered or hand/shaft-dominant, so keep the route candidate and do not patch those axes. Fresh seed 6161616161 tested exact mouth-sucking, single-testicle, hanging-balls-below-shaft, side-mouth-wrap, and chin-pelvis lower-mouth wording across three women; generated-route controls stayed the best repeated partials on turns 331 and 337, side-mouth and chin-pelvis branches produced isolated useful partials on turns 335 and 348, and the rest collapsed back to shaft/glans contact. Fresh seed 7171717171 tested flat pelvis-valley, thigh-tunnel, pubic-hair mouth-line, low-cushion chin-anchor, and pelvis-edge target-first wording across three women; flat pelvis-valley repeated a better viewer-flat body plane on turns 350, 356, and 362 but stayed shaft-centered, while the cushion and pelvis-edge branches drifted into wrong open-thigh/presentation geometry. Fresh seed 7272727272 tested hybrid flat-valley scrotal-skin, valley-floor open-lips, upper-frame shaft lower-scrotum, cropped upper-shaft valley-mouth, and side-low flat-valley wording; the flat-valley branch repeated the body plane on turns 368, 374, and 380 but stayed shaft-centered, and side-low flat-valley gave only look hints. Stop text-only expansion for now: do not patch those hybrid axes. The provisional generator route uses scrotum-as-mouth-surface, testicles resting across open lips, and scrotal-skin nearest-surface wording while staying candidate."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_footjob_frontal_sole_stroke",
|
||||||
|
"family": "footjob",
|
||||||
|
"status": "proven",
|
||||||
|
"atlas_folders": ["footjob"],
|
||||||
|
"action_family": "outercourse",
|
||||||
|
"position_keys": ["footjob"],
|
||||||
|
"canonical_geometry": "Frontal first-person footjob view: viewer reclines with thighs framing the lower foreground while the woman sits opposite with two large overlapping soles dominating the lower center foreground, inner arches pressing inward around the upright shaft, toes curled around both edges, a narrow visible strip of shaft and glans rising between the compressed feet, and her body and face behind the feet.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV footjob position",
|
||||||
|
"viewer reclines with thighs framing the lower foreground",
|
||||||
|
"woman sits opposite facing him with legs open toward the camera",
|
||||||
|
"two large overlapping soles dominate the lower center foreground",
|
||||||
|
"inner arches press inward from both sides around the upright shaft",
|
||||||
|
"toes curl around both edges",
|
||||||
|
"narrow visible strip of shaft and glans rises between the compressed feet",
|
||||||
|
"woman's face and torso stay visible behind the large foreground feet"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"generic foot contact without both soles around the shaft",
|
||||||
|
"hands replacing the feet as the main contact",
|
||||||
|
"mouth or hand action competing with the footjob",
|
||||||
|
"feet off to the side without centered penis contact"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"footjob/59_footjob.png",
|
||||||
|
"footjob/86_footjob.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["footjob", "foot job", "feet stroking"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["238365845574312", "3434343434", "6868686868", "7373737373"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#footjob",
|
||||||
|
"notes": "Same-seed two-woman expansions repeated the two-sole clamp as a provisional generator improvement over valid baselines; seed 6868686868 showed overlapping soles plus a narrow visible shaft/glans strip is more reliable than generic large-sole wording. Fresh seed 7373737373 then repeated the generated overlapping-sole/narrow-strip route across two women on turns 264 and 267, with tight center-gap repeats on turns 265 and 268. Promote the default route to proven and keep cross-foot side press as an alternate branch."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_fingering_reclined_open_thighs",
|
||||||
|
"family": "fingering",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["fingering"],
|
||||||
|
"action_family": "manual",
|
||||||
|
"position_keys": ["fingering", "open_thighs"],
|
||||||
|
"canonical_geometry": "First-person manual-contact view: the woman reclines or sits back with thighs spread wide toward the camera, face and torso visible behind the open legs, and the viewer hand enters from the foreground to make the visible contact between her legs.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV fingering position",
|
||||||
|
"woman reclines with thighs spread wide toward the camera",
|
||||||
|
"viewer hand enters from the foreground",
|
||||||
|
"fingers make the central contact between her open thighs",
|
||||||
|
"her face and torso remain visible behind the open-leg frame",
|
||||||
|
"thighs and knees form the main framing around the action"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"generic hand near the body without visible manual contact",
|
||||||
|
"the woman's own hand replacing the POV hand",
|
||||||
|
"mouth, foot, or penetration action competing with the foreground hand",
|
||||||
|
"closed legs hiding the contact point"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"fingering/103_fingering.png",
|
||||||
|
"fingering/69_fingering.png",
|
||||||
|
"fingering/80_fingering.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["fingering", "finger", "manual stimulation"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": [],
|
||||||
|
"guide_section": "",
|
||||||
|
"notes": "Atlas shows a repeated open-thigh manual-contact POV layout; needs fixed-seed Krea2 tests before promotion to proven."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_wand_foreground_tool_contact",
|
||||||
|
"family": "wand",
|
||||||
|
"status": "proven",
|
||||||
|
"atlas_folders": ["wand"],
|
||||||
|
"action_family": "toy",
|
||||||
|
"position_keys": ["wand", "toy_contact", "open_thighs"],
|
||||||
|
"canonical_geometry": "First-person toy-contact view: the woman reclines or sits back with thighs spread toward the camera, face and torso visible behind the open-leg frame, and the viewer hand holds a single continuous teal wand-style massager from the foreground with the rounded bulb head pressed flat to the central contact point.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV wand toy position",
|
||||||
|
"woman reclines with thighs spread wide toward the camera",
|
||||||
|
"single continuous teal wand-style massager is the largest lower-frame object",
|
||||||
|
"viewer hand holds a wand-style toy from the foreground",
|
||||||
|
"rounded bulb head presses flat to her vulva and clit as the central contact point",
|
||||||
|
"her face and torso remain visible behind the open-leg frame",
|
||||||
|
"thighs and knees form the main frame around the foreground tool"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"generic toy nearby without contact",
|
||||||
|
"the woman holding the toy when the foreground viewer hand is intended",
|
||||||
|
"mouth, foot, or penetration action competing with the toy contact",
|
||||||
|
"closed legs hiding the contact point",
|
||||||
|
"toy floating without a visible hand or handle"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"wand/106_wand_.png",
|
||||||
|
"wand/107_wand_.png",
|
||||||
|
"wand/108_wand_.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["wand", "toy", "vibrator"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["246813579", "8642086420", "7979797979"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#wand-toy-contact",
|
||||||
|
"notes": "The teal lower-right single-continuous-wand axis repeated across two women on sampler seeds 8642086420 and 7979797979 and validated through generated-route turns 197, 234, and 238. The pale upper-left wand remains a useful alternate branch; oversized bulb wording can hide contact."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_ejaculation_aftermath_open_thigh_candidate",
|
||||||
|
"family": "ready",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["ready"],
|
||||||
|
"action_family": "climax",
|
||||||
|
"position_keys": ["open_thighs", "camera_showing"],
|
||||||
|
"canonical_geometry": "First-person post-ejaculation open-thigh display: the woman reclines or sits back facing the viewer with thighs spread open, face and torso readable behind the open-leg frame, viewer body cue or recently withdrawn foreground cue near the lower edge, and thick semen or fluid visibly coating or dripping around the exposed pussy or anal opening.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV post-ejaculation open-thigh display pose",
|
||||||
|
"woman reclines or sits back facing the viewer with thighs spread open",
|
||||||
|
"thick semen or fluid is visible around the exposed pussy or anal opening",
|
||||||
|
"the body stays still after ejaculation",
|
||||||
|
"viewer body cue or recently withdrawn foreground cue stays near the lower edge",
|
||||||
|
"her face and torso remain visible behind the open-leg frame",
|
||||||
|
"thighs and knees frame the wet aftermath detail without hiding it"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"generic ready/setup pose before sex",
|
||||||
|
"active thrusting or penetration-in-progress wording",
|
||||||
|
"turning the setup into oral, toy, or manual contact",
|
||||||
|
"generic wetness without thick visible fluid around the exposed opening",
|
||||||
|
"closed thighs hiding the aftermath detail",
|
||||||
|
"cropping out the face and torso behind the open-leg frame"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"ready/105_ready_.png",
|
||||||
|
"ready/106_ready_.png",
|
||||||
|
"ready/107_ready_.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["post-ejaculation open-thigh display", "thick visible semen or fluid", "open thighs"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["1123581321"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#ready--post-ejaculation-open-thigh-display",
|
||||||
|
"notes": "The ready folder is a post-ejaculation open-thigh display pose with thick visible fluid around the exposed opening, not a neutral ready/setup pose. First fixed-seed evidence on source 52 was mirrored into the generator as a provisional improvement; repeat before promotion to proven."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_spread_open_thigh_presentation",
|
||||||
|
"family": "spread",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["spread"],
|
||||||
|
"action_family": "interaction",
|
||||||
|
"position_keys": ["open_thighs", "camera_showing"],
|
||||||
|
"canonical_geometry": "Frontal open-thigh presentation view: the woman faces the camera with legs raised or knees held wide, thighs forming a wide V-frame toward the viewer, face and torso visible behind the open-leg pose, and no required partner contact.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV open-thigh presentation position",
|
||||||
|
"woman faces the camera with legs raised or knees held wide",
|
||||||
|
"thighs form a wide V-frame toward the viewer",
|
||||||
|
"face and torso remain visible behind the open-leg pose",
|
||||||
|
"hands may hold the knees or thighs open",
|
||||||
|
"no partner contact is required for this setup pose"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"adding penetration or manual contact by default",
|
||||||
|
"closed thighs hiding the open-leg geometry",
|
||||||
|
"cropping out the face and torso behind the leg frame",
|
||||||
|
"turning the pose into doggy or side-lying geometry"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"spread/100_spread_.png",
|
||||||
|
"spread/4_spread_.png",
|
||||||
|
"spread/69_spread_.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["spread", "open thighs", "legs spread"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["3141592653"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#spread--open-thigh-presentation",
|
||||||
|
"notes": "Same-seed A/B on source 50 and 47 showed raised-knee V-frame and hand-on-knee hierarchy improves over generic spread wording. Mirrored into the generator as a provisional improvement; repeat on another seed before promotion to proven."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_sixty_nine_close_reversed_oral",
|
||||||
|
"family": "sixty_nine",
|
||||||
|
"status": "unstable",
|
||||||
|
"difficulty": "hardest",
|
||||||
|
"priority": "low",
|
||||||
|
"control_requirement": "pose_or_image_guidance_first",
|
||||||
|
"atlas_folders": ["69"],
|
||||||
|
"action_family": "oral",
|
||||||
|
"position_keys": ["sixty_nine"],
|
||||||
|
"canonical_geometry": "Close first-person sixty-nine view: the visible partner is reversed over the viewer with hips closest to the camera, head and torso receding away into the upper frame, viewer face or mouth anchoring the lower foreground, and hands optionally holding the hips to keep the reversed body arrangement readable.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV close sixty-nine position",
|
||||||
|
"visible partner is reversed over the viewer with hips closest to the camera",
|
||||||
|
"the partner's head and torso recede away into the upper frame",
|
||||||
|
"viewer face or mouth anchors the lower foreground under the partner's hips",
|
||||||
|
"viewer hands may hold the partner's hips without changing the reversed-over-viewer body arrangement",
|
||||||
|
"keep the mutual oral geometry readable as one continuous first-person frame"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"side-by-side sixty-nine layout",
|
||||||
|
"upright oral pose with the partner facing the viewer",
|
||||||
|
"generic oral contact without the reversed-over-viewer body arrangement",
|
||||||
|
"cropping away the head-and-torso direction that proves the sixty-nine setup",
|
||||||
|
"text-only prompting when exact geometry matters; prefer pose control or image guidance"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"69/105_sixtynine.png",
|
||||||
|
"69/106_sixtynine.png",
|
||||||
|
"69/50_sixtynine.png",
|
||||||
|
"69/80_sixtynine.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["sixty-nine", "reversed over viewer", "mutual oral"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": [],
|
||||||
|
"guide_section": "",
|
||||||
|
"notes": "Lowest-priority atlas route for now: geometry is consistent but visually fragile for text-only Krea2 prompting. Treat it as a pose/control-image or image-guidance-first case, not a normal prompt-only fixed-seed candidate."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_blowjob_top_down_vertical_shaft",
|
||||||
|
"family": "blowjob_top_view",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["blowjob_top_view"],
|
||||||
|
"action_family": "oral",
|
||||||
|
"position_keys": ["kneeling", "top_down_oral"],
|
||||||
|
"canonical_geometry": "Nadir-angle standing male POV top-view oral view: the viewer looks almost straight down from his torso toward the floor, nearby floor plane dominates the image, the viewer abdomen, shorts, thighs, and feet frame the lower foreground, the shaft is a short centered vertical column, and one woman kneels directly below between his feet with hair crown, forehead, shoulders, hands, knees, mouth, and shaft alignment visible from above.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"nadir-angle standing male POV top-view oral position",
|
||||||
|
"viewer looks almost straight down from his torso toward the floor",
|
||||||
|
"nearby carpet/floor plane dominates the image",
|
||||||
|
"viewer abdomen, shorts, thighs, and feet frame the lower foreground",
|
||||||
|
"shaft is a short centered vertical column",
|
||||||
|
"one woman kneels directly below the viewer between his feet",
|
||||||
|
"hair crown, forehead, shoulders, hands, and knees are visible from above",
|
||||||
|
"desk legs, chair wheels, carpet texture, and floor seams act as top-down office anchors"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"side-view oral framing",
|
||||||
|
"woman standing level with the viewer",
|
||||||
|
"shaft angled sideways or cropped away from the mouth",
|
||||||
|
"hands replacing the mouth as the main oral contact",
|
||||||
|
"camera placed behind the woman instead of above the viewer",
|
||||||
|
"literal plumb-line or map wording that renders as drawn graphics"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"blowjob_top_view/102_blowjob_top_view.png",
|
||||||
|
"blowjob_top_view/2_blowjob_top_view.png",
|
||||||
|
"blowjob_top_view/85_blowjob_top_view.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["kneeling oral", "top-down oral", "oral"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["4242424242"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#blowjob-top-view--overhead-vertical-shaft",
|
||||||
|
"notes": "Same-sampler source 46/47 A/B showed that top-down oral hierarchy tightens hand-at-base support and centered shaft-to-mouth alignment over generic kneeling oral. A follow-up axis loop on the same seed showed that generic steep-overhead wording can still feel horizontal, while nadir-angle standing male POV plus a dominating nearby floor plane, one woman directly between the viewer's feet, top-down office anchors, and a short centered vertical shaft column gives the strongest atlas-like verticality. Avoid plumb-line/map wording because Krea2 can literalize it as drawn graphics. Keep candidate until another source or seed repeats the nadir-angle axis."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_blowjob_side_profile_oral",
|
||||||
|
"family": "blowjob_side",
|
||||||
|
"status": "proven",
|
||||||
|
"atlas_folders": ["blowjob_side"],
|
||||||
|
"action_family": "oral",
|
||||||
|
"position_keys": ["side_lying", "reclining_oral", "penis_licking"],
|
||||||
|
"canonical_geometry": "Side-profile first-person oral body-line view: the male viewer's abdomen, navel, pelvis, and near thigh create the broad lower-frame foreground surface, the adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground, the woman enters laterally from the left edge beside his hip, and her side-facing mouth plus hand contact align to the shaft at the male abdomen line.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV side-profile oral body-line position",
|
||||||
|
"male viewer's abdomen, navel, pelvis, and near thigh create a broad horizontal body surface",
|
||||||
|
"adult male viewer's own torso starts at the lower edge and runs diagonally into the lower-right foreground",
|
||||||
|
"navel, abdomen hair, pelvis, and near thigh mark the camera owner's body",
|
||||||
|
"woman enters laterally from the left edge beside his hip",
|
||||||
|
"cheek and jaw stay in profile",
|
||||||
|
"mouth on the shaft at the male abdomen line",
|
||||||
|
"lips touching the shaft at the male abdomen line",
|
||||||
|
"mouth-to-shaft contact is the nearest facial detail",
|
||||||
|
"hand around the base under her lips",
|
||||||
|
"shoulder and torso trail sideways along the edge"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"top-down oral framing",
|
||||||
|
"front-facing centered face instead of side profile",
|
||||||
|
"woman standing level with the viewer",
|
||||||
|
"camera behind the woman",
|
||||||
|
"hands replacing the mouth as the main oral contact",
|
||||||
|
"pure male-body-axis wording that exposes the male as a photographed subject",
|
||||||
|
"transferring the central body surface to the woman"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"blowjob_side/103_blowjob_side.png",
|
||||||
|
"blowjob_side/105_blowjob_side.png",
|
||||||
|
"blowjob_side/29_blowjob_side.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["side_lying", "side-lying oral", "blowjob_side", "side-profile oral", "oral"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["5656565656", "9753197531", "9595959595", "9696969696", "5858585858"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#blowjob-side-profile--side-phone-weak-case",
|
||||||
|
"notes": "Seed 5656565656 first produced attractive side-phone / external side-camera oral compositions across source 46 and 47, but not valid POV evidence. A later source-46 candidate with explicit adult-male foreground ownership recovered a more atlas-like first-person body-line view, while a related source-47 body-axis candidate failed by transferring the central body surface to the woman. Seed 9753197531 then repeated the lateral-edge body-line wording across two women. Generated-route turn 207 showed the route also needs lips-touching and mouth-to-shaft-contact priority to keep the mouth from floating above the shaft. Seed 9595959595 repeated the lower-right torso anchor on turns 279 and 283 across two visible women, improving camera-owner torso ownership over a control that could expose the male as a photographed side subject. Seed 9696969696 generated-route validation repeated the patched route on turns 284 and 285, keeping lower-right own-body foreground, profile mouth contact, and office depth across two visible women. Seed 5858585858 added a three-woman generated-route repeat on turns 298, 301, and 304; all controls preserved the patched camera-owner lower-right body plane, lateral profile entry, mouth contact at the abdomen line, and office depth. Promote the generated side-profile POV hierarchy to proven while keeping side-camera-style self-body crop wording as a look branch rather than the default."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_blowjob_laying_frontal_oral",
|
||||||
|
"family": "blowjob_laying",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["blowjob_laying"],
|
||||||
|
"action_family": "oral",
|
||||||
|
"position_keys": ["reclining_oral", "penis_licking"],
|
||||||
|
"canonical_geometry": "Frontal prone first-person oral view: the viewer reclines with open thighs framing the lower foreground, the woman lies belly-down between the viewer's open thighs, and her front-facing mouth and hands align to a shaft rising from the lower center of the frame.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV prone frontal oral position",
|
||||||
|
"viewer reclines with open thighs framing the lower foreground",
|
||||||
|
"woman lies belly-down between the viewer's open thighs",
|
||||||
|
"her chest and shoulders stay low over the viewer's pelvis",
|
||||||
|
"shaft rises from the lower center toward her front-facing mouth",
|
||||||
|
"her hands support or steady the shaft while mouth contact remains the main action"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"side-profile oral framing",
|
||||||
|
"top-down oral framing from above the viewer",
|
||||||
|
"woman kneeling upright instead of lying forward",
|
||||||
|
"woman standing level with the viewer",
|
||||||
|
"hands replacing the mouth as the main oral contact"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"blowjob_laying/101_blowjob_laying.png",
|
||||||
|
"blowjob_laying/103_blowjob_laying.png",
|
||||||
|
"blowjob_laying/60_blowjob_laying.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["blowjob_laying", "prone frontal oral", "oral"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["6767676767"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#blowjob-laying-frontal--wide-v-frame",
|
||||||
|
"notes": "Seed 6767676767 improved source 46 and 50 with a wide symmetrical V-frame, lower abdomen near-edge anchor, torso stretched low and horizontal between the viewer's thighs, hands at the base, and centered mouth-to-shaft contact. Baselines were already strong but read more raised-hips or all-fours than prone belly-down, so keep the route candidate until another seed repeats the low-horizontal body improvement."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_blowjob_sitting_upright_oral",
|
||||||
|
"family": "blowjob_sitting",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["blowjob_sitting"],
|
||||||
|
"action_family": "oral",
|
||||||
|
"position_keys": ["reclining_oral", "penis_licking", "blowjob_sitting"],
|
||||||
|
"canonical_geometry": "Upright seated first-person oral view: the viewer reclines with open thighs framing the lower foreground, the woman sits upright between the viewer's open thighs, and her close front-facing mouth aligns to a vertical shaft centered between the viewer's legs.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV upright sitting oral position",
|
||||||
|
"viewer reclines with open thighs framing the lower foreground",
|
||||||
|
"woman sits low between the viewer's open thighs with torso upright behind the action",
|
||||||
|
"her face lowers close to the exact center contact point",
|
||||||
|
"vertical shaft centered between the viewer's legs",
|
||||||
|
"her open mouth covers the centered tip with hands wrapped low at the base"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"prone belly-down oral framing",
|
||||||
|
"side-profile oral framing",
|
||||||
|
"top-down oral framing from above the viewer",
|
||||||
|
"woman standing level with the viewer",
|
||||||
|
"cropping away the viewer's open-thigh frame"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"blowjob_sitting/100_blowjob_sitting.png",
|
||||||
|
"blowjob_sitting/24_blowjob_sitting.png",
|
||||||
|
"blowjob_sitting/58_blowjob_sitting.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["blowjob_sitting", "upright sitting oral", "oral"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["7878787878"],
|
||||||
|
"guide_section": "docs/krea2-prompt-guide.md#blowjob-sitting-upright--low-mouth-contact",
|
||||||
|
"notes": "Seed 7878787878 improved source 46 and 50 with low-mouth seated hierarchy: viewer thigh V-frame, lower abdomen near edge, woman sitting low between the thighs with torso upright behind the action, face lowered to the exact center contact point, open mouth covering the centered shaft tip, and both hands wrapped at the base. Source 50 had some outfit looseness/drift, so keep the route candidate and provisional until another seed repeats it."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_missionary_open_leg_penetration",
|
||||||
|
"family": "missionary",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["missionary"],
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_keys": ["missionary", "open_thighs", "front_entry"],
|
||||||
|
"canonical_geometry": "First-person missionary view from above the viewer's pelvis: the woman reclines on her back facing the viewer, knees open toward the viewer, thighs frame the central contact line, and the viewer's lower body or hands anchor the lower foreground.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV missionary open-leg penetration position",
|
||||||
|
"woman reclines on her back with knees open toward the viewer",
|
||||||
|
"her face, torso, and open thighs remain visible in one frame",
|
||||||
|
"viewer is positioned between her legs from the lower foreground",
|
||||||
|
"thighs frame the central penetration line",
|
||||||
|
"viewer hands may hold her thighs without blocking the contact geometry",
|
||||||
|
"for flat elevated-support examples, viewer stands or braces at the foot edge with feet, shins, or side-dropping legs below the support edge"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"folded-leg or knees-to-chest geometry",
|
||||||
|
"rear-entry or doggy geometry",
|
||||||
|
"side-profile framing",
|
||||||
|
"cropping away the woman's face and torso",
|
||||||
|
"viewer standing far back instead of positioned between her legs"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"missionary/101_missionary.png",
|
||||||
|
"missionary/102_missionary.png",
|
||||||
|
"missionary/1_missionary.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["missionary", "open-leg penetration", "front-entry"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["8989898989"],
|
||||||
|
"guide_section": "missionary-open-leg--seated-lounge-drift",
|
||||||
|
"notes": "Same-seed batches on 8989898989 show two valid subcases. Generic/angled missionary can preserve open thighs, viewer hands, and centered contact, while the flatter atlas examples need elevated-support edge placement: woman flat across a table/platform, viewer standing or braced at the foot edge, and viewer feet/shins or side-dropping legs below the support. The accepted turn84 axis is mirrored only into the raised-edge/edge-supported route as a provisional patch; keep generic missionary available and keep the catalog candidate until another seed repeats it."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_missionary_folded_high_leg_penetration",
|
||||||
|
"family": "missionary_folded",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["missionary_folded"],
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_keys": ["missionary_folded", "front_entry"],
|
||||||
|
"canonical_geometry": "First-person folded missionary view from above the viewer's pelvis: the woman reclines on her back facing the viewer, knees folded high toward her chest, feet or ankles close to the camera, and the viewer's hands hold her calves or ankles while the central contact line stays below the raised legs.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV folded missionary high-leg penetration position",
|
||||||
|
"woman reclines on her back with knees folded high toward her chest",
|
||||||
|
"viewer lower abdomen and a large centered shaft anchor the lower center first",
|
||||||
|
"compact folded-knee block sits above the contact point",
|
||||||
|
"feet or ankles sit close to the camera above the contact line",
|
||||||
|
"viewer hands hold her calves or ankles in the foreground",
|
||||||
|
"her face and torso remain visible between or behind the raised legs",
|
||||||
|
"central penetration line stays below the folded knees"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"normal open-leg missionary with knees spread low",
|
||||||
|
"rear-entry or doggy geometry",
|
||||||
|
"side-profile framing",
|
||||||
|
"legs cropped away or relaxed flat on the bed",
|
||||||
|
"feet replacing the penetration geometry as the main action"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"missionary_folded/16_missionary_folded.png",
|
||||||
|
"missionary_folded/50_missionary_folded.png",
|
||||||
|
"missionary_folded/80_missionary_folded.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["missionary_folded", "folded missionary", "knees-to-chest"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["8989898989"],
|
||||||
|
"guide_section": "missionary-folded--contact-first-knee-block",
|
||||||
|
"notes": "Same-seed turns 85-92 show that subject-first knees-to-chest wording can produce folded high-leg geometry, but Krea2 drops readable shaft/contact when the knees and feet dominate first. The accepted turn89 axis puts the viewer lower abdomen and large centered shaft/contact before the compact folded-knee block, then holds calves/ankles and keeps the face/torso behind the raised knees. Mirrored into the folded-missionary route as a provisional patch; keep catalog candidate until another seed or subject repeats it."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_cowgirl_frontal_straddle_penetration",
|
||||||
|
"family": "cowgirl",
|
||||||
|
"status": "proven",
|
||||||
|
"atlas_folders": ["5.cowgirl"],
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_keys": ["cowgirl", "frontal_straddle", "woman_on_top"],
|
||||||
|
"canonical_geometry": "First-person frontal cowgirl view: the viewer reclines below while the woman straddles the viewer facing him, knees open to either side, torso upright above the contact line, and the viewer's thighs, pelvis, or hands anchor the lower foreground.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV frontal cowgirl straddle penetration position",
|
||||||
|
"woman straddles the viewer facing him",
|
||||||
|
"her torso stays upright above the viewer",
|
||||||
|
"viewer lower abdomen and pelvis anchor the bottom edge",
|
||||||
|
"wide horizontal thigh bridge spans left edge to right edge",
|
||||||
|
"her knees are open to either side of the viewer's hips",
|
||||||
|
"viewer reclines below with thighs or pelvis in the lower foreground",
|
||||||
|
"viewer hands may hold her thighs or hips without blocking the centered contact line"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"missionary with the woman lying on her back",
|
||||||
|
"reverse cowgirl with the woman facing away",
|
||||||
|
"folded-leg knees-to-chest geometry",
|
||||||
|
"rear-entry or doggy geometry",
|
||||||
|
"cropping away the upright torso and straddling knees"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"5.cowgirl/100_cowgirl.png",
|
||||||
|
"5.cowgirl/101_cowgirl.png",
|
||||||
|
"5.cowgirl/1_cowgirl.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["cowgirl", "frontal cowgirl", "woman-on-top"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["8989898989", "2828282828", "9191919191"],
|
||||||
|
"guide_section": "cowgirl-frontal--wide-thigh-bridge",
|
||||||
|
"notes": "Same-seed turns 93-96 show the generic baseline already validly hits frontal cowgirl on seed 8989898989. The best atlas-like improvement was turn95: wide horizontal thigh bridge from left edge to right edge, viewer lower abdomen/pelvis at the bottom edge, upright torso above the contact, and hands gripping the thigh sides. Seed 2828282828 then repeated the wide-thigh bridge hierarchy across two visible women on turns 209 and 213, and generated-route turn 216 validated the patched normal cowgirl route. Fresh seed 9191919191 repeated the generated route and three branch wordings across turns 242-249, with turns 242, 243, 244, and 248 giving the clearest atlas-like wide-thigh bridge. Promote the normal cowgirl route to proven while keeping cowgirl-alt and reverse-cowgirl families separate."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_cowgirl_alt_low_squat_penetration",
|
||||||
|
"family": "cowgirl_alt",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["5.cowgirl_alt"],
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_keys": ["cowgirl_alt", "woman_on_top"],
|
||||||
|
"canonical_geometry": "Close first-person cowgirl-alt view: the viewer lies flat on his back underneath while the woman faces him in a low seated squat over the viewer's pelvis, knees bent wide near the camera, torso close above the contact line, and ceiling or upper-wall background cues confirm the low upward viewpoint.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV low cowgirl seated-squat penetration position",
|
||||||
|
"viewer lies flat on his back underneath her",
|
||||||
|
"lens sits low at the viewer's abdomen looking upward from his pelvis",
|
||||||
|
"woman faces the viewer in a low seated squat over the viewer's pelvis",
|
||||||
|
"her knees are bent wide and close to the camera on either side of the viewer's hips",
|
||||||
|
"her torso stays close above the centered contact line",
|
||||||
|
"ceiling lights, upper walls, or high partition lines appear behind her upper body",
|
||||||
|
"viewer reclines below with thighs, pelvis, or lower torso anchoring the foreground",
|
||||||
|
"viewer hands may hold the underside of her thighs or hips without blocking the centered contact line"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"upright distant cowgirl with the torso far from the viewer",
|
||||||
|
"missionary with the woman lying on her back",
|
||||||
|
"reverse cowgirl with the woman facing away",
|
||||||
|
"folded-leg knees-to-chest geometry",
|
||||||
|
"rear-entry or doggy geometry",
|
||||||
|
"cropping away the wide bent knees and close seated position"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"5.cowgirl_alt/101_cowgirl_alt.png",
|
||||||
|
"5.cowgirl_alt/102_cowgirl_alt.png",
|
||||||
|
"5.cowgirl_alt/103_cowgirl_alt.png",
|
||||||
|
"5.cowgirl_alt/16_cowgirl_alt.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["cowgirl_alt", "low cowgirl", "seated-squat cowgirl", "woman-on-top"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["8989898989"],
|
||||||
|
"guide_section": "cowgirl-alt--flat-supine-low-angle",
|
||||||
|
"notes": "Same-seed turns 97-104 show that low-squat/contact wording can still miss the atlas orientation by reading as a platform/high-camera setup. The accepted turn104 axis uses flat-supine viewer wording plus ceiling and upper glass/room background cues: viewer abdomen and chest lie flat at the bottom, lens looks upward from his abdomen/pelvis, woman is mounted over him with wide bent knees, and centered contact remains readable. Mirrored into the cowgirl-alt route as a provisional patch; keep catalog candidate until another seed or subject repeats it."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_reverse_cowgirl_back_facing_penetration",
|
||||||
|
"family": "reverse_cowgirl",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["cowgirl_reverse"],
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_keys": ["reverse_cowgirl", "back_facing_straddle", "woman_on_top"],
|
||||||
|
"canonical_geometry": "First-person reverse cowgirl view: the viewer reclines below while the woman straddles the viewer facing away, her back and hips dominate the frame, her knees or thighs sit to either side of the viewer's hips, and the viewer's thighs, pelvis, hands, or lower torso anchor the lower foreground.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV reverse cowgirl back-facing penetration position",
|
||||||
|
"woman faces away from the viewer in a back-facing straddle",
|
||||||
|
"her back, hips, and ass are the nearest largest shapes to the camera",
|
||||||
|
"her back, hips, and ass are closest to the camera while her face may turn over one shoulder",
|
||||||
|
"her knees or thighs are planted to either side of the viewer's hips",
|
||||||
|
"viewer reclines underneath with thighs, pelvis, or lower torso anchoring the foreground",
|
||||||
|
"viewer thighs frame the lower corners",
|
||||||
|
"centered contact sits directly between her thighs below her ass",
|
||||||
|
"viewer hands may hold her hips or thighs without changing the woman-on-top geometry"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"frontal cowgirl with the woman facing the viewer",
|
||||||
|
"missionary with the woman lying on her back",
|
||||||
|
"rear-entry or doggy geometry with the viewer behind her",
|
||||||
|
"woman on all fours",
|
||||||
|
"side-profile penetration without the back-facing straddle",
|
||||||
|
"cropping away the back, hips, and viewer-underneath foreground cues"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"cowgirl_reverse/101_cowgirl_reverse.png",
|
||||||
|
"cowgirl_reverse/104_cowgirl_reverse.png",
|
||||||
|
"cowgirl_reverse/106_cowgirl_reverse.png",
|
||||||
|
"cowgirl_reverse/1_cowgirl_reverse.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["cowgirl_reverse", "reverse cowgirl", "back-facing straddle", "woman-on-top"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["8989898989"],
|
||||||
|
"guide_section": "reverse-cowgirl--close-back-hip-dominant",
|
||||||
|
"notes": "Same-seed turns 105-108 show that generic facing-away reverse-cowgirl wording can collapse into frontal cowgirl. The accepted turn106 axis makes the back/hips/ass the nearest largest shapes, puts the viewer underneath with thighs framing the lower corners, and keeps centered contact directly between her thighs below her ass. Turns 107 and 108 are useful secondary evidence for viewer-leg V-frame and over-shoulder glance variants. Mirrored into the reverse-cowgirl route as a provisional patch; keep candidate until another seed or subject repeats it, and keep reverse-cowgirl-alt separate for the more upright seated atlas family."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "pov_reverse_cowgirl_alt_upright_back_facing_penetration",
|
||||||
|
"family": "reverse_cowgirl_alt",
|
||||||
|
"status": "candidate",
|
||||||
|
"atlas_folders": ["cowgirl_reversere_alt"],
|
||||||
|
"action_family": "penetration",
|
||||||
|
"position_keys": ["reverse_cowgirl_alt", "reverse_cowgirl", "back_facing_straddle", "woman_on_top", "upright_seated"],
|
||||||
|
"canonical_geometry": "Upright first-person reverse cowgirl alt view: the viewer reclines below while the woman sits upright facing away in a back-facing straddle, her back and ass stay centered above the viewer's pelvis, her knees or thighs frame the viewer's hips, and viewer hands may hold her hips, thighs, wrists, or hands.",
|
||||||
|
"prompt_cues": [
|
||||||
|
"POV upright reverse cowgirl back-facing penetration position",
|
||||||
|
"woman sits upright facing away from the viewer in a back-facing straddle",
|
||||||
|
"her back stays vertical and readable above her hips",
|
||||||
|
"her ass is centered above the viewer's pelvis while her knees or thighs frame the viewer's hips",
|
||||||
|
"viewer hands hold her hips",
|
||||||
|
"viewer thighs frame the lower corners",
|
||||||
|
"centered contact remains visible below her ass",
|
||||||
|
"viewer reclines underneath with thighs, pelvis, or lower torso anchoring the foreground",
|
||||||
|
"viewer hands may hold her hips, thighs, wrists, or hands without changing the upright woman-on-top posture"
|
||||||
|
],
|
||||||
|
"avoid_cues": [
|
||||||
|
"close hip-only reverse cowgirl crop without the upright back",
|
||||||
|
"frontal cowgirl with the woman facing the viewer",
|
||||||
|
"missionary with the woman lying on her back",
|
||||||
|
"rear-entry or doggy geometry with the viewer behind her",
|
||||||
|
"woman on all fours",
|
||||||
|
"cropping away the vertical back and seated woman-on-top posture"
|
||||||
|
],
|
||||||
|
"reference_images": [
|
||||||
|
"cowgirl_reversere_alt/100_cowgirl_reversere_alt.png",
|
||||||
|
"cowgirl_reversere_alt/101_cowgirl_reversere_alt.png",
|
||||||
|
"cowgirl_reversere_alt/102_cowgirl_reversere_alt.png",
|
||||||
|
"cowgirl_reversere_alt/18_cowgirl_reversere_alt.png"
|
||||||
|
],
|
||||||
|
"generator_hook": {
|
||||||
|
"module": "krea_pov_actions.py",
|
||||||
|
"route_terms": ["cowgirl_reversere_alt", "reverse cowgirl alt", "upright back-facing straddle", "woman-on-top"]
|
||||||
|
},
|
||||||
|
"evidence": {
|
||||||
|
"fixed_seed_tests": ["8989898989"],
|
||||||
|
"guide_section": "reverse-cowgirl-alt--upright-seated-back-facing",
|
||||||
|
"notes": "Same-seed turns 109-112 show that the upright seated reverse-cowgirl-alt family is distinct from the close normal reverse-cowgirl route. Turn 109's generic upright baseline was already valid, while turn110's vertical-back plus hands-on-hips wording best matched the alt atlas: back and shoulders stay upright/readable, ass centered over the viewer's pelvis, viewer hands hold both hips, viewer thighs frame the lower corners, and centered contact remains below her ass. Mirrored into a separate reverse-cowgirl-alt route as a provisional patch; keep candidate until another seed or subject repeats it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
"weight": 1.0,
|
"weight": 1.0,
|
||||||
"subject_type": "configured_cast",
|
"subject_type": "configured_cast",
|
||||||
"item_label": "Sexual pose",
|
"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.",
|
"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",
|
"negative_prompt": "minors, childlike appearance, teen, schoolgirl, incest, bestiality, non-consensual, coercion, rape, violence, injury, blood, gore, watermark",
|
||||||
"scene_pools": ["hardcore_private_scenes"],
|
"scene_pools": ["hardcore_private_scenes"],
|
||||||
@@ -1110,8 +1110,11 @@
|
|||||||
],
|
],
|
||||||
"position": [
|
"position": [
|
||||||
"missionary position",
|
"missionary position",
|
||||||
|
"folded missionary position",
|
||||||
"cowgirl position",
|
"cowgirl position",
|
||||||
|
"low cowgirl seated-squat position",
|
||||||
"reverse cowgirl position",
|
"reverse cowgirl position",
|
||||||
|
"upright reverse cowgirl position",
|
||||||
"doggy style position",
|
"doggy style position",
|
||||||
"standing sex position",
|
"standing sex position",
|
||||||
"spooning sex position",
|
"spooning sex position",
|
||||||
@@ -2105,6 +2108,7 @@
|
|||||||
"standing with cum on the body",
|
"standing with cum on the body",
|
||||||
"straddling a partner's hips in cowgirl position",
|
"straddling a partner's hips in cowgirl position",
|
||||||
"reverse cowgirl over a partner's hips",
|
"reverse cowgirl over a partner's hips",
|
||||||
|
"upright reverse cowgirl over a partner's hips",
|
||||||
"on all fours with hips raised",
|
"on all fours with hips raised",
|
||||||
"face-down ass-up on the mattress",
|
"face-down ass-up on the mattress",
|
||||||
"side-lying with thighs parted",
|
"side-lying with thighs parted",
|
||||||
|
|||||||
@@ -0,0 +1,461 @@
|
|||||||
|
# Krea2 A/B Methodology Memory
|
||||||
|
|
||||||
|
This file is the persistent memory for SxCP Krea2 prompt A/B methodology.
|
||||||
|
Update it whenever the testing method improves.
|
||||||
|
|
||||||
|
## Current Method
|
||||||
|
|
||||||
|
Version: `2026-06-30-generated-route-validation-positive-channel-cleanup`
|
||||||
|
|
||||||
|
1. Pull or construct the baseline from an actual SxCP/CodexMCPTest source case.
|
||||||
|
2. Keep the sampler seed fixed across the baseline and candidate.
|
||||||
|
3. Keep subject, location family, camera family, and target pose fixed unless
|
||||||
|
the experiment explicitly tests one of those axes.
|
||||||
|
4. Change one prompt variable at a time when possible, usually the visual
|
||||||
|
hierarchy for the target contact or pose.
|
||||||
|
5. Keep `sxcp_eval_out` positive-only. Do not place negative-conditioning
|
||||||
|
phrases in the visible prompt.
|
||||||
|
6. Use location-compatible anchors only. For coworking/office scenes, use chair
|
||||||
|
edge, desk edge, laptop table, glass partitions, repeated desk rows, plants,
|
||||||
|
and window depth instead of bedroom or bedding anchors.
|
||||||
|
7. Treat a manual prompt win as proof that Krea2 responds to the wording, not
|
||||||
|
proof that the SxCP generator already emits it.
|
||||||
|
8. Mirror a prompt win into the generator as a provisional improvement when
|
||||||
|
leaving a category if same-seed evidence shows it improves over baseline and
|
||||||
|
the wording is generator-safe. Keep the route `candidate` until the broader
|
||||||
|
generator-patch evidence matrix proves it.
|
||||||
|
9. When a subject-first batch preserves appearance but repeatedly misses the
|
||||||
|
atlas body plane, record it as weak-case evidence and consider stronger
|
||||||
|
control before adding more generator text.
|
||||||
|
10. Score spatial orientation against the atlas before accepting evidence,
|
||||||
|
and treat a contradictory room/background read as a rejection even when
|
||||||
|
contact or limb placement is clear. Use background cues to decide whether
|
||||||
|
the viewer or partner is high, low, standing, seated, supine, or on a
|
||||||
|
support before grading pose/contact quality.
|
||||||
|
11. For hard text-only pose families, set an exploration budget before calling
|
||||||
|
the route weak or deciding it needs stronger control. Eight prompt probes
|
||||||
|
are only an early signal. Use batched wording-axis probes and aim for about
|
||||||
|
fifty positive-only tries across meaningful axes before concluding that
|
||||||
|
prompt text cannot reliably express the pose.
|
||||||
|
12. Do not require a perfect atlas hit before carrying progress forward. After
|
||||||
|
the exploration budget, a repeatable partial that beats the baseline failure
|
||||||
|
mode can become an accepted provisional generator improvement while the
|
||||||
|
remaining miss stays documented for later seed/source expansion.
|
||||||
|
13. After patching generator wording, render one prompt produced by the actual
|
||||||
|
code path before closing the category. Manual prompt-axis wins are not
|
||||||
|
enough; the generated route can still drop the key contact hierarchy or add
|
||||||
|
limiting positive-channel wording.
|
||||||
|
|
||||||
|
## Promotion Gates
|
||||||
|
|
||||||
|
- One clean fixed-seed A/B can be recorded as evidence for that source case.
|
||||||
|
- A prompt-guide rule needs repeated evidence across distinct subjects,
|
||||||
|
locations, or seeds, unless the generated prompt is structurally wrong before
|
||||||
|
rendering.
|
||||||
|
- A catalog variant remains candidate until the rule repeats under controlled
|
||||||
|
conditions.
|
||||||
|
- A provisional generator patch is allowed when leaving a category if the best
|
||||||
|
tested wording improves over baseline on a fixed seed. It should preserve the
|
||||||
|
selected subject, outfit, location, and camera semantics, and it must not patch
|
||||||
|
in a scene workaround that only solved one render.
|
||||||
|
- A proven/default generator patch still needs the broader evidence matrix below,
|
||||||
|
unless the generated prompt is structurally wrong before rendering.
|
||||||
|
|
||||||
|
## Generator Mirroring
|
||||||
|
|
||||||
|
After a manual A/B prompt win, do not assume the SxCP generator mirrors the
|
||||||
|
wording. Add a failing regression against the final formatter output first, then
|
||||||
|
patch the narrow route boundary that owns the wording. The regression should
|
||||||
|
assert the accepted hierarchy terms and reject the failure mode that caused the
|
||||||
|
bad render, such as scene-incompatible anchors or negative-conditioning text in
|
||||||
|
the positive prompt.
|
||||||
|
|
||||||
|
After the route patch, run a generated-route probe through `sxcp_eval_out` with
|
||||||
|
the same sampler seed when feasible. Use the actual formatter output, not a
|
||||||
|
hand-normalized prompt. If the generated route regresses compared with the
|
||||||
|
manual prompt-axis winner, record the failed generated-route image as the
|
||||||
|
baseline, tighten the route wording, and validate again before logging the
|
||||||
|
candidate as generated-route evidence.
|
||||||
|
|
||||||
|
For location-specific wins, split the implementation:
|
||||||
|
|
||||||
|
- the action or role graph owns the pose/contact hierarchy;
|
||||||
|
- the final Krea formatter owns scene-compatible anchor expansion because it can
|
||||||
|
see the selected scene, camera, and composition;
|
||||||
|
- existing route phrases that downstream tests rely on should be preserved
|
||||||
|
inside the stronger wording when they do not conflict with the A/B evidence.
|
||||||
|
|
||||||
|
## MCP Command Memory
|
||||||
|
|
||||||
|
Use the checked helper instead of ad hoc Python snippets for bridge calls. The
|
||||||
|
approved command prefix is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Common calls:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py list-tools
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_pull --arguments-json '{"channel":"sxcp_eval_in"}'
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_push --arguments-json '{"channel":"sxcp_eval_out","seed":5656565656,"text":"PROMPT_ONLY_POSITIVE_CONDITIONING"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
For batched prompt-axis search, prepare a JSON batch and use the offline command
|
||||||
|
renderer before touching the bridge manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/sxcp_prompt_batch.py validate --batch-json /tmp/sxcp-batch.json
|
||||||
|
python tools/sxcp_prompt_batch.py print-push-commands --batch-json /tmp/sxcp-batch.json
|
||||||
|
python tools/sxcp_prompt_batch.py print-result-template --batch-json /tmp/sxcp-batch.json
|
||||||
|
python tools/sxcp_prompt_batch.py run-batch --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --previous-turn 80 --run
|
||||||
|
python tools/sxcp_prompt_batch.py validate-results --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json
|
||||||
|
python tools/sxcp_prompt_batch.py print-eval-entry-draft --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --variant-key pov_example_variant --baseline-image /absolute/baseline.png --candidate-id controlled_subject_first
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `run-batch --run` for normal batch execution. It pushes one positive prompt,
|
||||||
|
polls `sxcp_eval_in` until the turn advances and an absolute PNG appears with
|
||||||
|
the fixed sampler seed, writes the filled result JSON, then sends the next
|
||||||
|
prompt. Omit `--run` for a dry-run command preview. Run `validate-results` after
|
||||||
|
the batch and before drafting evidence. It checks that every probe returned a
|
||||||
|
new ordered turn, an absolute PNG image path, and the same sampler seed as the
|
||||||
|
batch. This keeps batched prompt search as image-presence collection first and
|
||||||
|
bulk analysis second.
|
||||||
|
|
||||||
|
Before drafting evidence, compare atlas references and generated images for
|
||||||
|
spatial orientation, not only limb/contact similarity. First decide the
|
||||||
|
atlas's surface and camera-height relationship, then check whether the
|
||||||
|
generated background supports the same read. Use the background as a
|
||||||
|
camera-height witness: ceiling, upper walls, and high partition lines usually
|
||||||
|
support a low viewer looking upward; floor, carpet, table tops, platform edges,
|
||||||
|
or furniture behind the body can reveal a higher camera, seated support, or a
|
||||||
|
different surface. If the atlas target has the viewer flat on his back or the
|
||||||
|
partner mounted over him, do not accept a candidate only because contact is
|
||||||
|
clear; the room geometry must also support that flat/low read. Reject the
|
||||||
|
candidate before generator mirroring when the background says the bodies are on
|
||||||
|
a different surface or at a different height than the atlas.
|
||||||
|
|
||||||
|
`print-eval-entry-draft` rejects `geometry_only` candidates by default. Use
|
||||||
|
`--allow-geometry-only` only when the entry is explicitly labeled as
|
||||||
|
non-controlled prompt-axis evidence rather than subject/look-controlled A/B
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
Keep `sxcp_eval_out` prompt-only and positive-only. Do not use
|
||||||
|
`sxcp_eval_negative_out` for Krea2 tuning.
|
||||||
|
|
||||||
|
## Generator-Patch Evidence Matrix
|
||||||
|
|
||||||
|
Do prompt and image exploration before editing production generator wording. A
|
||||||
|
normal pose-wording generator patch needs all of this evidence first:
|
||||||
|
|
||||||
|
- at least three distinct source cases with different visible subjects;
|
||||||
|
- at least two sampler seeds, unless the source prompt is structurally wrong
|
||||||
|
before rendering;
|
||||||
|
- location-family coverage when the proposed wording changes scene anchors;
|
||||||
|
- one baseline and one candidate per source case, with subject, location family,
|
||||||
|
camera family, and sampler seed fixed inside each pair;
|
||||||
|
- positive-only candidate prompts, with no negative-conditioning phrases in the
|
||||||
|
positive prompt.
|
||||||
|
|
||||||
|
A generated-route probe that works before the full matrix is useful evidence.
|
||||||
|
If it is the best tested improvement when leaving the category, it can become a
|
||||||
|
`provisional_generator_patch` with final prompt regression coverage. It should
|
||||||
|
not become a proven `generator_patch` decision until the matrix repeats and the
|
||||||
|
final generated prompt is regression-tested.
|
||||||
|
|
||||||
|
## Hard-Pose Exploration Budget
|
||||||
|
|
||||||
|
Use this budget for atlas poses where early prompt-only results repeatedly miss
|
||||||
|
the core spatial read.
|
||||||
|
|
||||||
|
- Define the failure threshold before the run. The default threshold is about
|
||||||
|
fifty positive-only prompt tries across distinct wording axes before declaring
|
||||||
|
the pose text-insufficient or moving it to a stronger-control bucket.
|
||||||
|
- Run the search in batches, usually six to twelve prompts at a time. Send each
|
||||||
|
prompt through `sxcp_eval_out`, wait for the image path, then analyze the
|
||||||
|
batch together instead of overreacting to one render.
|
||||||
|
- Keep a short axis ledger for each batch: intended wording axis, seed, source
|
||||||
|
subject, best image, repeated failure mode, and words that literalized or
|
||||||
|
harmed the result.
|
||||||
|
- Treat a small failed batch as direction, not a conclusion. If a batch shows a
|
||||||
|
repeated failure such as head height, camera height, viewer/partner elevation,
|
||||||
|
or background-plane mismatch, the next batch should vary that axis directly.
|
||||||
|
- Stop early only for a strong positive result that is worth repeating on a
|
||||||
|
second source or seed, or for a hard technical blocker. A weak but improving
|
||||||
|
result should feed the next wording batch rather than ending the category.
|
||||||
|
- If the threshold run finds a repeatable partial that is materially better
|
||||||
|
than baseline, accept the partial target explicitly and mirror only that
|
||||||
|
generator-safe improvement. Keep the route candidate and mark the evidence as
|
||||||
|
needing expansion when the full atlas target is still unsolved.
|
||||||
|
|
||||||
|
## Current Fingering Test Pattern
|
||||||
|
|
||||||
|
The prior bedding-based fingering prompt is invalid as a general rule because
|
||||||
|
it solved a lower-foreground artifact by adding bedroom context to an office
|
||||||
|
scene. The corrected test pattern keeps the coworking location intact:
|
||||||
|
|
||||||
|
- baseline: generic POV fingering/manual-contact wording from the same source
|
||||||
|
case;
|
||||||
|
- candidate: foreground hand first, open-thigh geometry second, visible woman
|
||||||
|
face/torso third, office chair and coworking depth fourth;
|
||||||
|
- anchors: black office chair seat/arms, desk edge, laptop table corners, glass
|
||||||
|
partitions, repeated desk rows, plants, tall-window depth;
|
||||||
|
- rejection trigger: any result that fixes contact by changing the scene family
|
||||||
|
instead of improving the pose hierarchy.
|
||||||
|
|
||||||
|
## Improvement Log
|
||||||
|
|
||||||
|
- `2026-06-30`: Added side-camera/result-label separation after ballsucking
|
||||||
|
seed `5757575757` produced attractive low side-camera oral views while still
|
||||||
|
collapsing the requested contact object onto the shaft/glans. Future scoring
|
||||||
|
should record that as side-view oral evidence and keep target-contact evidence
|
||||||
|
separate.
|
||||||
|
- `2026-06-30`: Added generated-route validation discipline after footjob turn
|
||||||
|
`183` kept large foreground soles but hid the shaft/contact that manual probes
|
||||||
|
had preserved. Future provisional generator patches should render the exact
|
||||||
|
final Krea prompt once after the code change; if shared route wording adds
|
||||||
|
limiting positive-channel language, clean it before sending the validation
|
||||||
|
prompt.
|
||||||
|
- `2026-06-30`: Added a hard-pose exploration budget after ballsucking wording
|
||||||
|
tests produced only eight early probes before the first weak-case note. Future
|
||||||
|
hard text-only poses should use batched wording-axis search and aim for about
|
||||||
|
fifty positive-only tries before concluding the pose needs stronger control.
|
||||||
|
- `2026-06-30`: Added partial-acceptance discipline after ballsucking produced
|
||||||
|
repeatable tongue/lips-on-testicles results that beat the shaft/glans
|
||||||
|
baseline but did not fully solve mouth-wrapped contact. Future hard-pose exits
|
||||||
|
should preserve repeatable progress as a provisional generator patch while
|
||||||
|
keeping the remaining miss in the expansion queue.
|
||||||
|
- `2026-06-30`: Added ballsucking target-object refinement after sampler seed
|
||||||
|
`9797979797` repeated the `scrotal skin is the nearest mouth surface` branch
|
||||||
|
on turns `288` and `293`. Score target-object ownership separately from the
|
||||||
|
side-low camera family: a route can preserve face/thigh geometry while still
|
||||||
|
drifting to shaft/base contact. Avoid promoting balls-first center-object
|
||||||
|
wording when it creates multi-subject or body-layout artifacts.
|
||||||
|
- `2026-06-30`: Added ballsucking generated-route validation after sampler seed
|
||||||
|
`9898989898` repeated the patched scrotal-skin route on turns `296` and
|
||||||
|
`297`. Validation can accept a provisional target-object improvement while
|
||||||
|
still keeping the pose queued when the remaining miss is full mouth-wrapped
|
||||||
|
testicle contact.
|
||||||
|
- `2026-06-30`: Added ballsucking fresh weak-case evidence after sampler seed
|
||||||
|
`5959595959` tested lip-oval, sideways mouth pocket, and chin-pelvis upward
|
||||||
|
seal wording across three women. The batch preserved low-pelvis/cheek-thigh
|
||||||
|
geometry in places, but every branch returned to shaft/glans collapse or
|
||||||
|
generic oral contact. Do not retry those axes as generator defaults; the next
|
||||||
|
search should change the target-object control strategy rather than adding
|
||||||
|
more mouth-shape synonyms.
|
||||||
|
- `2026-06-30`: Added ballsucking occlusion weak-case evidence after sampler
|
||||||
|
seed `6060606060` tested foreground occlusion, under-scrotum tongue shelf,
|
||||||
|
and hand-guided scrotum wording across three women. The generated route
|
||||||
|
remained the best partial while those axes became shaft-centered or
|
||||||
|
hand/shaft-dominant. Do not retry occlusion or hand-support synonyms as
|
||||||
|
generator defaults; the next useful move is a different target-object strategy
|
||||||
|
or stronger control.
|
||||||
|
- `2026-06-30`: Added ballsucking mouth-axis mixed-case evidence after sampler
|
||||||
|
seed `6161616161` tested exact mouth-sucking, single-testicle, hanging balls
|
||||||
|
below shaft, side-mouth wrap, and chin-pelvis lower-mouth wording across
|
||||||
|
three women. The generated-route controls stayed the best repeated partials
|
||||||
|
on two subjects, side-mouth and chin-pelvis variants produced isolated useful
|
||||||
|
partials, and the rest drifted back to shaft/glans contact. Record isolated
|
||||||
|
partials as axis hints, but do not patch generator wording unless a branch
|
||||||
|
repeats across subjects or beats the generated-route controls.
|
||||||
|
- `2026-06-30`: Added ballsucking pelvis-valley weak-case evidence after
|
||||||
|
sampler seed `7171717171` tested flat pelvis-valley, thigh tunnel,
|
||||||
|
pubic-hair mouth-line, low-cushion chin-anchor, and pelvis-edge target-first
|
||||||
|
wording across three women. The flat pelvis-valley branch repeated a strong
|
||||||
|
body-plane correction on three subjects, matching the atlas viewer-flat
|
||||||
|
thigh-wall read better, but it stayed shaft-centered. Score body-plane
|
||||||
|
orientation and target-object contact separately; do not patch a route when
|
||||||
|
it improves orientation while regressing the target.
|
||||||
|
- `2026-06-30`: Stopped the ballsucking text-only loop after sampler seed
|
||||||
|
`7272727272` combined `flat-valley scrotal-skin` target wording with the
|
||||||
|
prior side-low route across three women. The hybrid repeated the body-plane
|
||||||
|
hint on turns `368`, `374`, and `380`, but the target stayed shaft-centered,
|
||||||
|
while side-low flat-valley variants only gave look hints. Preserve the
|
||||||
|
current side-low scrotal-skin partial, do not patch the hybrid axes, and move
|
||||||
|
future full-target work toward stronger pose/control evidence rather than
|
||||||
|
more positive-prompt synonyms.
|
||||||
|
- `2026-06-30`: Promoted blowjob side-profile POV after sampler seed
|
||||||
|
`5858585858` produced a three-woman generated-route repeat on turns `298`,
|
||||||
|
`301`, and `304`. When the current generated route repeats across multiple
|
||||||
|
subjects on a fresh seed and alternate branches do not beat it cleanly, mark
|
||||||
|
the route proven instead of continuing to queue it. Keep attractive
|
||||||
|
side-camera-style self-body crop results as a separate look branch when they
|
||||||
|
risk drifting toward external side framing.
|
||||||
|
- `2026-06-29`: Added the multisource/generator-safe method after an overfit
|
||||||
|
single-character coworking test produced a visually usable but invalid
|
||||||
|
bedding foreground. Future A/B runs must test at least two source cases before
|
||||||
|
promoting wording that is meant to become a durable guide or generator rule.
|
||||||
|
- `2026-06-29`: Added generator mirroring discipline after the accepted
|
||||||
|
fingering wording proved Krea2 behavior but not generator output. Future
|
||||||
|
mirroring changes need a red-green regression at final Krea formatter output,
|
||||||
|
not just a guide entry.
|
||||||
|
- `2026-06-29`: Tightened generator-patch promotion after the fingering
|
||||||
|
generated-route probe looked good but had too little image coverage. Future
|
||||||
|
pose-wording generator edits need a broader seed, subject, and location matrix
|
||||||
|
before production route code changes.
|
||||||
|
- `2026-06-29`: Added semantic-axis discipline after source 52 fingering tests.
|
||||||
|
If a candidate succeeds by changing ownership, viewpoint, location family, or
|
||||||
|
role semantics, record it as a weak-case or prompt note unless that semantic
|
||||||
|
change is the intended generator behavior. Do not count it as direct evidence
|
||||||
|
for the original route even when the image is visually cleaner.
|
||||||
|
- `2026-06-29`: Added provisional generator-patch discipline after the user
|
||||||
|
clarified that leaving a category should still carry forward same-seed progress
|
||||||
|
over baseline. Future category exits should patch the generator with the best
|
||||||
|
generator-safe improvement, record it as `provisional_generator_patch`, and
|
||||||
|
keep the catalog route as `candidate` until repeated evidence proves it.
|
||||||
|
- `2026-06-29`: Applied the category-exit rule to spread/open-thigh presentation
|
||||||
|
after two source subjects improved on the same sampler seed. For setup poses
|
||||||
|
that are not structurally broken before rendering, prefer at least two source
|
||||||
|
subjects before mirroring a provisional generator patch, and keep the
|
||||||
|
observation explicit about remaining weak points such as insufficient V-frame
|
||||||
|
width or outfit closure.
|
||||||
|
- `2026-06-29`: Applied the same category-exit rule to blowjob top-view after
|
||||||
|
two source subjects improved on sampler seed `4242424242`. When the baseline is already usable,
|
||||||
|
record the improvement narrowly: name the axis that got better, keep the route
|
||||||
|
candidate, and avoid overstating the finding as proven until another seed
|
||||||
|
repeats it.
|
||||||
|
- `2026-06-29`: Corrected blowjob top-view criteria after atlas review and a
|
||||||
|
same-seed source-`46` probe showed that vertical shaft alignment alone can
|
||||||
|
still render as frontal/eye-height oral. Future top-view evidence must show
|
||||||
|
steep overhead camera geometry: viewer abdomen at the lower edge, camera
|
||||||
|
looking down from above the viewer chest/abdomen, and the woman's hair crown,
|
||||||
|
shoulders, and hands visible from above.
|
||||||
|
- `2026-06-29`: Refined blowjob top-view prompt-axis search after the user
|
||||||
|
rejected horizontally biased probes. Run several prompt-only probes before
|
||||||
|
editing the generator, wait for `sxcp_eval_in` to advance to the new turn, and
|
||||||
|
compare each image against the atlas verticality criteria. The useful axis is
|
||||||
|
`nadir-angle` or `bird's-eye` plus standing male POV, nearby floor plane
|
||||||
|
dominating the image, one woman directly below between the viewer's feet, and
|
||||||
|
top-down office anchors. Avoid `plumb-line` and `map` in generator prompts
|
||||||
|
because Krea2 can literalize them as drawn graphics.
|
||||||
|
- `2026-06-29`: For quick wording-axis search, prefer a batched prompt-probe
|
||||||
|
loop before analysis-heavy iteration. Prepare several positive-only alternate
|
||||||
|
prompts that isolate likely wording axes, send them one at a time through
|
||||||
|
`sxcp_eval_out` with the same sampler seed, pull only until each new
|
||||||
|
`sxcp_eval_in` turn and image path exists, then inspect the returned images as
|
||||||
|
a batch. Use the bulk comparison to pick the best axis, identify literalized
|
||||||
|
or harmful words, and only then update the generator, guide, catalog, or eval
|
||||||
|
log.
|
||||||
|
- `2026-06-29`: Preserve prompt-order controls when testing anything beyond
|
||||||
|
rough pose-axis discovery. Prompts that start with pose geometry and omit or
|
||||||
|
move the subject/look block can reduce female-look adherence, so treat those
|
||||||
|
runs as geometry-only probes. Durable A/B prompts should keep the original
|
||||||
|
subject/look description first, then the pose hierarchy, then location and
|
||||||
|
style/background anchors, unless the test is explicitly about prompt-order
|
||||||
|
sensitivity.
|
||||||
|
- `2026-06-29`: Added result-validation discipline to the batched prompt helper.
|
||||||
|
After sending a batch, fill the result template from `sxcp_eval_in`, run
|
||||||
|
`validate-results`, and only then draft evidence. The validation step proves
|
||||||
|
each probe returned an ordered turn, an absolute PNG artifact, and the fixed
|
||||||
|
sampler seed before bulk analysis or log-entry drafting.
|
||||||
|
- `2026-06-29`: Added `run-batch` automation to the batched prompt helper. It
|
||||||
|
removes manual push/pull copy-paste from normal A/B runs while keeping the same
|
||||||
|
gates: positive-only prompts, fixed sampler seed, turn advancement, absolute
|
||||||
|
PNG image path, and `validate-results` before evidence drafting.
|
||||||
|
- `2026-06-29`: Split missionary subcases after turns `77`-`84`. Turns `76` and
|
||||||
|
`80` are valid angled/cushion missionary results, not failures. The flatter
|
||||||
|
atlas examples need a different positive axis: woman flat across an elevated
|
||||||
|
table/platform, viewer standing or braced at the foot edge, and viewer feet,
|
||||||
|
shins, or side-dropping legs placed below the support edge. Patch this only
|
||||||
|
into the raised-edge/edge-supported route; keep generic missionary available
|
||||||
|
for angled valid views.
|
||||||
|
- `2026-06-29`: Folded-missionary tuning on seed `8989898989` used two
|
||||||
|
subject-first batches before code changes. Turns `85`-`88` showed that
|
||||||
|
compact knee-block and vertical-thigh-column wording can produce the folded
|
||||||
|
high-leg geometry, but the shaft/contact disappears when knees and feet lead
|
||||||
|
the hierarchy. Turns `89`-`92` then tested contact-first variants; turn `89`
|
||||||
|
was accepted because it placed the viewer lower abdomen and large centered
|
||||||
|
shaft/contact before the compact folded-knee block. This confirms the
|
||||||
|
method: use the first batch to identify the failed axis, run a targeted
|
||||||
|
second batch, then mirror only the accepted generator-safe hierarchy as a
|
||||||
|
provisional patch.
|
||||||
|
- `2026-06-29`: Frontal cowgirl on seed `8989898989` used a baseline-plus-
|
||||||
|
variants batch instead of comparing against a previous category. Turn `93`
|
||||||
|
was a valid generic cowgirl baseline, so turn `95`'s wide horizontal thigh
|
||||||
|
bridge improvement became a prompt-guide rule rather than a generator patch.
|
||||||
|
When the baseline already hits the pose, record the useful atlas refinement
|
||||||
|
and leave the generator unchanged unless repeated evidence shows a systemic
|
||||||
|
weakness.
|
||||||
|
- `2026-06-29`: Cowgirl-alt on seed `8989898989` exposed a spatial-orientation
|
||||||
|
blind spot. Turns `97`-`100` had readable contact and squat-like knees, but
|
||||||
|
the background still read as a platform/high-camera setup. After rechecking
|
||||||
|
the atlas, turns `101`-`104` tested flat-supine viewer wording with ceiling
|
||||||
|
and upper-room cues; turn `104` was accepted. Future pose analysis must
|
||||||
|
compare atlas and generated room geometry before accepting an image.
|
||||||
|
- `2026-06-29`: Reverse cowgirl on seed `8989898989` showed that a correct
|
||||||
|
semantic label such as `facing away` can be ignored when the visual hierarchy
|
||||||
|
still resembles frontal cowgirl. Future back-facing straddle tests should
|
||||||
|
score facing direction before contact quality and should name the back, hips,
|
||||||
|
and ass as the nearest largest shapes before viewer-leg and contact details.
|
||||||
|
Treat over-shoulder glances as secondary refinements only after the
|
||||||
|
back-facing straddle is already locked.
|
||||||
|
- `2026-06-29`: Reverse-cowgirl-alt on seed `8989898989` confirmed that atlas
|
||||||
|
sibling folders can need separate generator routes even when the baseline is
|
||||||
|
already valid. Normal reverse cowgirl is close back/hip dominant; reverse-alt
|
||||||
|
is upright seated with vertical back/shoulders and viewer hands or thighs
|
||||||
|
forming the lower frame. Keep those prompt hierarchies separate instead of
|
||||||
|
merging all back-facing woman-on-top evidence into one route.
|
||||||
|
- `2026-06-29`: Added non-target-viewpoint discipline after blowjob side-profile
|
||||||
|
oral produced an attractive side-camera result on seed `5656565656`. If a
|
||||||
|
render is visually useful but reads as a different camera family, record it as
|
||||||
|
a weak case for a future route and do not mirror it into the current POV
|
||||||
|
generator path.
|
||||||
|
- `2026-06-29`: Added MCP command memory after repeated context loss around the
|
||||||
|
bridge workflow. Future A/B calls should use the checked helper command
|
||||||
|
`/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py ...`, with
|
||||||
|
`comfy_push` to `sxcp_eval_out` for prompt-only positive conditioning and
|
||||||
|
`comfy_pull` from `sxcp_eval_in` for returned prompt/image/seed payloads.
|
||||||
|
- `2026-06-29`: Added side-profile oral ownership discipline after source `46`
|
||||||
|
improved with explicit adult-male foreground ownership while source `47`
|
||||||
|
rejected a related `body-axis` cue by transferring the body surface to the
|
||||||
|
woman. Future side-profile tests should name the foreground owner repeatedly
|
||||||
|
and verify that the woman's body stays lateral before considering any
|
||||||
|
generator mirroring.
|
||||||
|
- `2026-06-30`: Promoted the side-profile oral lateral-edge body-line axis
|
||||||
|
after sampler seed `9753197531` repeated it across two visible women. Pure
|
||||||
|
male-body-axis wording can expose the male as a photographed subject or let
|
||||||
|
Krea2 transfer the central body surface away from the intended first-person
|
||||||
|
view. Future generator patches should combine adult-male foreground ownership
|
||||||
|
with explicit lateral entry from the left edge, mouth at the male abdomen
|
||||||
|
line, and hand under the lips; keep the route provisional until another
|
||||||
|
seed/source expansion repeats it.
|
||||||
|
- `2026-06-30`: Added side-profile oral generated-route contact validation
|
||||||
|
after turn `206` kept the male body-line geometry but let the mouth float
|
||||||
|
above the shaft while the hand became the contact anchor. Turn `207` improved
|
||||||
|
after adding lips-touching and mouth-to-shaft-contact priority. Future
|
||||||
|
generated-route validation for oral side-profile should score both viewpoint
|
||||||
|
ownership and which body part actually anchors the contact.
|
||||||
|
- `2026-06-30`: Added the side-profile oral lower-right torso anchor after
|
||||||
|
sampler seed `9595959595` repeated it on turns `279` and `283` across two
|
||||||
|
visible women. The useful wording makes the adult male viewer's own torso
|
||||||
|
start at the lower edge and run diagonally into the lower-right foreground,
|
||||||
|
with navel, abdomen hair, pelvis, and near thigh marking the camera owner's
|
||||||
|
body. Prefer this over generic body-axis wording, which can expose the male
|
||||||
|
as a photographed side subject or transfer the axis onto the woman.
|
||||||
|
- `2026-06-30`: Added side-profile oral generated-route validation after
|
||||||
|
sampler seed `9696969696` repeated the patched route on turns `284` and
|
||||||
|
`285`. Count generated-route validation separately from prompt-axis search:
|
||||||
|
it proves the formatter can carry the new wording, while promotion still
|
||||||
|
requires broader source/seed evidence.
|
||||||
|
- `2026-06-30`: Promoted normal frontal cowgirl from guide-only to provisional
|
||||||
|
generator patch after seed `2828282828` repeated the wide-thigh bridge axis
|
||||||
|
across two visible women. When the baseline is already valid, a generator
|
||||||
|
patch is still appropriate if a later seed repeats a narrow atlas refinement
|
||||||
|
that improves geometry without harming subject/look, contact, or setting.
|
||||||
|
Generated-route turn `216` validated the patched formatter route with viewer
|
||||||
|
hands on outer thighs, wide foreground thigh bridge, upright torso, centered
|
||||||
|
contact, and coworking depth. Keep the route candidate until another
|
||||||
|
source/seed repeats the refinement.
|
||||||
|
- `2026-06-29`: Applied the category-exit rule to blowjob laying frontal after
|
||||||
|
source `46` and source `50` improved on sampler seed `6767676767`. When
|
||||||
|
baselines are already strong, preserve the exact improved axis: wide V-frame and low-horizontal torso hierarchy, while noting residual high-hip posture and
|
||||||
|
keeping the generator patch provisional until another seed repeats it.
|
||||||
|
- `2026-06-29`: Applied the category-exit rule to blowjob sitting upright after
|
||||||
|
source `46` and source `50` improved on sampler seed `7878787878`. When a
|
||||||
|
baseline preserves the seated pose but floats the face above the contact
|
||||||
|
point, prefer low-mouth seated hierarchy over generic `mouth aligned` wording:
|
||||||
|
face lowered to the exact center contact point, open mouth covering the
|
||||||
|
centered tip, and hands directly at the base. Record outfit looseness/drift as
|
||||||
|
residual risk and keep the generator patch provisional until another seed
|
||||||
|
repeats it.
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
|||||||
|
# Krea2 POV Pose Atlas
|
||||||
|
|
||||||
|
Local reference root:
|
||||||
|
|
||||||
|
`/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2`
|
||||||
|
|
||||||
|
Use this dataset as the pose-geometry reference for POV prompt tuning. The pose
|
||||||
|
folders contain rendered POV examples; matching `_control` folders contain the
|
||||||
|
solo/control image for the same pose family. Ignore `bg` and `*_bg` folders for
|
||||||
|
pose analysis; they are background plates without people.
|
||||||
|
|
||||||
|
Machine-readable pose variants live in
|
||||||
|
`categories/krea2_pov_pose_variants.json`. That catalog is intentionally smaller
|
||||||
|
than the full atlas: it only contains variants that are proven or useful
|
||||||
|
candidates for fixed-seed Krea2 tuning. Add a variant there when it has a compact
|
||||||
|
geometry summary, cue phrases, avoid phrases, references, and a known generator
|
||||||
|
hook. Code should read it through `krea2_pose_variant_catalog.py` instead of
|
||||||
|
parsing the JSON directly.
|
||||||
|
|
||||||
|
In ComfyUI, use the `SxCP Krea2 Pose Variant` node when you want a workflow to
|
||||||
|
select one catalog variant and emit a compatible `hardcore_position_config` for
|
||||||
|
the existing Position Pool / Action Filter / Insta-OF chain. Pair it with
|
||||||
|
`SxCP Krea2 Variant Evidence` to display the fixed-seed eval entry, image paths,
|
||||||
|
and generator decision behind that variant.
|
||||||
|
|
||||||
|
For command-line planning, `python tools/krea2_tuning_report.py` shows which
|
||||||
|
catalog variants are proven or pending and which atlas pose folders are still
|
||||||
|
unmapped by the catalog. Unmapped folders include sample pose/control image
|
||||||
|
paths and a suggested candidate key to start the next catalog entry.
|
||||||
|
|
||||||
|
The `ready` folder name is misleading for prompt planning: it is mapped as
|
||||||
|
`pov_ejaculation_aftermath_open_thigh_candidate`, a post-ejaculation
|
||||||
|
open-thigh display family with thick visible fluid around the exposed opening,
|
||||||
|
not as a neutral setup pose.
|
||||||
|
|
||||||
|
## Inventory
|
||||||
|
|
||||||
|
| Family | Pose images | Control images | First sample |
|
||||||
|
| --- | ---: | ---: | --- |
|
||||||
|
| cowgirl | 63 | 63 | `5.cowgirl/100_cowgirl.png` |
|
||||||
|
| cowgirl alt | 62 | 62 | `5.cowgirl_alt/101_cowgirl_alt.png` |
|
||||||
|
| reverse cowgirl | 58 | 58 | `cowgirl_reverse/101_cowgirl_reverse.png` |
|
||||||
|
| reverse cowgirl alt | 50 | 50 | `cowgirl_reversere_alt/100_cowgirl_reversere_alt.png` |
|
||||||
|
| doggy | 57 | 57 | `doggy/101_doggy.png` |
|
||||||
|
| doggy alt | 45 | 45 | `doggy_alt/100_doggy_alt.png` |
|
||||||
|
| missionary | 74 | 74 | `missionary/101_missionary.png` |
|
||||||
|
| missionary folded | 12 | 12 | `missionary_folded/16_missionary_folded.png` |
|
||||||
|
| sixty-nine | 29 | 29 | `69/105_sixtynine.png` |
|
||||||
|
| ballsucking | 25 | 25 | `ballsucking/101_ballsucking.png` |
|
||||||
|
| blowjob laying | 42 | 42 | `blowjob_laying/101_blowjob_laying.png` |
|
||||||
|
| blowjob side | 17 | 17 | `blowjob_side/103_blowjob_side.png` |
|
||||||
|
| blowjob sitting | 27 | 27 | `blowjob_sitting/100_blowjob_sitting.png` |
|
||||||
|
| blowjob top view | 17 | 17 | `blowjob_top_view/102_blowjob_top_view.png` |
|
||||||
|
| boobjob | 11 | 11 | `boobjob/100_boobjob.png` |
|
||||||
|
| handjob | 24 | 24 | `handjob/18_handjob.png` |
|
||||||
|
| footjob | 2 | 2 | `footjob/59_footjob.png` |
|
||||||
|
| fingering | 10 | 10 | `fingering/103_fingering.png` |
|
||||||
|
| spread | 55 | 55 | `spread/100_spread_.png` |
|
||||||
|
| ready | 19 | 19 | `ready/105_ready_.png` |
|
||||||
|
| wand | 7 | 7 | `wand/106_wand_.png` |
|
||||||
|
|
||||||
|
## Tuning Method
|
||||||
|
|
||||||
|
For each pose family:
|
||||||
|
|
||||||
|
1. Sample 5-10 pose images and 2-3 control images.
|
||||||
|
2. Write a compact geometry summary using only repeated visual facts.
|
||||||
|
3. Test one prompt variant with a fixed seed.
|
||||||
|
4. Test the same wording on a second seed or character.
|
||||||
|
5. Patch generator defaults only when the wording improvement repeats or the
|
||||||
|
generated prompt is structurally wrong before rendering.
|
||||||
|
6. Record the evidence in `docs/krea2-prompt-guide.md`.
|
||||||
|
|
||||||
|
## Confirmed Notes
|
||||||
|
|
||||||
|
### Doggy / Rear-Entry
|
||||||
|
|
||||||
|
Dataset references show that visible POV thighs, lower torso, or pelvis can be
|
||||||
|
correct. They should be treated as natural foreground cues, not automatic
|
||||||
|
failures.
|
||||||
|
|
||||||
|
Better Krea2 wording:
|
||||||
|
|
||||||
|
- `top-down POV doggy position from behind`
|
||||||
|
- `camera looks down over the viewer's hands onto the woman's raised hips`
|
||||||
|
- `woman is on all fours with chest low, forearms folded, cheek turned sideways`
|
||||||
|
- `back arched, hips raised high toward the camera`
|
||||||
|
- `viewer hands hold her hips with natural lower-body POV cues in the foreground`
|
||||||
|
|
||||||
|
Avoid using visible shoes or lower legs as the standing cue. In seed `65`, that
|
||||||
|
wording pulled Krea2 toward oral contact and weakened rear-entry geometry.
|
||||||
|
|
||||||
|
### Boobjob / Titjob
|
||||||
|
|
||||||
|
The boobjob folder shows a repeated upright, frontal geometry rather than a
|
||||||
|
forward-bent one: the woman faces the viewer between his thighs, breasts pressed
|
||||||
|
together around a vertical shaft, with the glans above the cleavage near her
|
||||||
|
mouth. For Krea2, name hand ownership when hands matter. In POV prompts, generic
|
||||||
|
`hands` can become the viewer's hands.
|
||||||
|
|
||||||
|
### Handjob
|
||||||
|
|
||||||
|
The handjob folder repeats a centered first-person layout: the viewer's thighs
|
||||||
|
frame the lower edges, the woman faces the viewer between his legs, and her hand
|
||||||
|
is the contact anchor on the shaft. Prompt the woman's hand ownership directly;
|
||||||
|
viewer hands should not cover the action unless that is the intended variant.
|
||||||
|
|
||||||
|
## Candidate Notes
|
||||||
|
|
||||||
|
### Footjob
|
||||||
|
|
||||||
|
The footjob folder is small but visually consistent: the viewer reclines with
|
||||||
|
thighs framing the lower foreground, the penis is upright near the center, and
|
||||||
|
the woman's soles/toes are the contact anchor while her body and face remain
|
||||||
|
behind the feet. Treat `pov_footjob_frontal_sole_stroke` as a candidate until it
|
||||||
|
has fixed-seed Krea2 evidence.
|
||||||
|
|
||||||
|
### Fingering
|
||||||
|
|
||||||
|
The fingering folder repeats a first-person manual-contact layout: the woman is
|
||||||
|
reclined or sitting back with thighs spread wide toward camera, her face and
|
||||||
|
torso visible behind the open-leg frame, and the viewer hand entering from the
|
||||||
|
foreground as the contact anchor. Treat `pov_fingering_reclined_open_thighs` as
|
||||||
|
a candidate until it has fixed-seed Krea2 evidence.
|
||||||
|
|
||||||
|
### Wand / Toy Contact
|
||||||
|
|
||||||
|
The wand folder repeats a close first-person tool-contact layout: the woman is
|
||||||
|
reclined or sitting back with thighs spread toward camera, face and torso visible
|
||||||
|
behind the open-leg frame, and the viewer hand holding a wand-style toy from the
|
||||||
|
foreground with the rounded head pressed to the central contact point. Treat
|
||||||
|
`pov_wand_foreground_tool_contact` as a candidate until it has fixed-seed Krea2
|
||||||
|
evidence. Keep the visible hand/handle in the wording; otherwise Krea2 may float
|
||||||
|
the toy or transfer ownership to the visible partner.
|
||||||
|
|
||||||
|
### Ready / Post-Ejaculation Open-Thigh Display
|
||||||
|
|
||||||
|
The ready folder is not a neutral setup family. It repeats a first-person
|
||||||
|
post-ejaculation display pose: the woman reclines or sits back facing the viewer
|
||||||
|
with thighs spread open, face and torso readable behind the open-leg frame, a
|
||||||
|
viewer body cue or recently withdrawn foreground cue near the lower edge, and
|
||||||
|
thick semen or fluid visible around the exposed pussy or anal opening. Treat
|
||||||
|
`pov_ejaculation_aftermath_open_thigh_candidate` as a candidate until it has
|
||||||
|
fixed-seed Krea2 evidence. Avoid active thrusting wording here; the key state is
|
||||||
|
post-ejaculation fluid visibility, not penetration-in-progress.
|
||||||
|
|
||||||
|
### Spread / Open-Thigh Presentation
|
||||||
|
|
||||||
|
The spread folder is a setup/presentation family rather than a required contact
|
||||||
|
action: the woman faces the camera with legs raised or knees held wide, thighs
|
||||||
|
forming a wide V-frame, and her face and torso visible behind the open-leg pose.
|
||||||
|
Treat `pov_spread_open_thigh_presentation` as a candidate until it has
|
||||||
|
fixed-seed Krea2 evidence.
|
||||||
|
|
||||||
|
### Sixty-Nine / Close Reversed POV
|
||||||
|
|
||||||
|
The `69` folder repeats a close first-person mutual-oral layout rather than a
|
||||||
|
wide side-by-side pose: the visible partner is reversed over the viewer, hips
|
||||||
|
closest to camera, head and torso receding away into the upper frame, and the
|
||||||
|
viewer face or mouth anchoring the lower foreground. Treat
|
||||||
|
`pov_sixty_nine_close_reversed_oral` as the hardest and lowest-priority route in
|
||||||
|
the atlas for now. Do not queue it as a normal prompt-only fixed-seed candidate.
|
||||||
|
When exact geometry matters, prefer a pose/control image or a narrower
|
||||||
|
image-guided route; text alone can collapse this into generic oral contact or
|
||||||
|
lose the reversed-over-viewer body arrangement.
|
||||||
|
|
||||||
|
### Blowjob Top View
|
||||||
|
|
||||||
|
The `blowjob_top_view` folder repeats a top-down first-person oral layout: the
|
||||||
|
viewer looks down from chest or pelvis height, viewer torso or thighs sit at the
|
||||||
|
lower edge, the shaft is vertical and centered, and the woman kneels below
|
||||||
|
looking upward with mouth and hand aligned to it. Treat
|
||||||
|
`pov_blowjob_top_down_vertical_shaft` as a candidate until it has fixed-seed
|
||||||
|
Krea2 evidence.
|
||||||
|
|
||||||
|
### Blowjob Side
|
||||||
|
|
||||||
|
The `blowjob_side` folder repeats a side-profile first-person oral layout: the
|
||||||
|
viewer reclines with torso or thighs visible, the woman leans beside the
|
||||||
|
viewer's pelvis from the side, and her side-facing mouth aligns to the shaft
|
||||||
|
near the lower center of the frame. Treat `pov_blowjob_side_profile_oral` as a
|
||||||
|
candidate until it has fixed-seed Krea2 evidence.
|
||||||
|
|
||||||
|
### Blowjob Laying
|
||||||
|
|
||||||
|
The `blowjob_laying` folder repeats a frontal prone first-person oral layout:
|
||||||
|
the viewer reclines with open thighs framing the lower foreground, the woman
|
||||||
|
lies belly-down between the viewer's thighs, and her front-facing mouth and
|
||||||
|
hands align to a shaft rising from the lower center of the frame. Treat
|
||||||
|
`pov_blowjob_laying_frontal_oral` as a candidate until it has fixed-seed Krea2
|
||||||
|
evidence.
|
||||||
|
|
||||||
|
### Blowjob Sitting
|
||||||
|
|
||||||
|
The `blowjob_sitting` folder includes a few top-view outliers, but the named
|
||||||
|
sitting files repeat an upright seated first-person oral layout: the viewer
|
||||||
|
reclines with open thighs framing the lower foreground, the woman sits upright
|
||||||
|
between the viewer's thighs, and her close front-facing mouth aligns to a
|
||||||
|
vertical centered shaft. Treat `pov_blowjob_sitting_upright_oral` as a candidate
|
||||||
|
until it has fixed-seed Krea2 evidence.
|
||||||
|
|
||||||
|
### Missionary / Open-Leg Penetration
|
||||||
|
|
||||||
|
The `missionary` folder repeats a front-facing first-person penetration layout:
|
||||||
|
the woman reclines on her back facing the viewer, her knees open toward the
|
||||||
|
viewer, her face and torso stay visible behind the open-thigh frame, and the
|
||||||
|
viewer is positioned between her legs from the lower foreground. Treat
|
||||||
|
`pov_missionary_open_leg_penetration` as a candidate until it has fixed-seed
|
||||||
|
Krea2 evidence. Keep this separate from `missionary_folded`, where the legs are
|
||||||
|
pressed much higher and need different wording.
|
||||||
|
|
||||||
|
### Missionary Folded / High-Leg Penetration
|
||||||
|
|
||||||
|
The `missionary_folded` folder repeats a high-leg first-person penetration
|
||||||
|
layout: the woman reclines on her back facing the viewer, her knees are folded
|
||||||
|
high toward her chest, feet or ankles sit close to the camera, and the viewer's
|
||||||
|
hands often hold her calves or ankles while the contact line stays below the
|
||||||
|
raised legs. Treat `pov_missionary_folded_high_leg_penetration` as a candidate
|
||||||
|
until it has fixed-seed Krea2 evidence.
|
||||||
|
|
||||||
|
### Cowgirl / Frontal Straddle Penetration
|
||||||
|
|
||||||
|
The `5.cowgirl` folder repeats a frontal woman-on-top first-person layout: the
|
||||||
|
viewer reclines below, the woman straddles the viewer facing him, her torso
|
||||||
|
stays upright above the contact line, and her knees open to either side of the
|
||||||
|
viewer. Treat `pov_cowgirl_frontal_straddle_penetration` as a candidate until it
|
||||||
|
has fixed-seed Krea2 evidence. Keep this separate from the alt and reverse
|
||||||
|
cowgirl folders, which need their own geometry wording.
|
||||||
|
|
||||||
|
### Cowgirl Alt / Low Seated-Squat Penetration
|
||||||
|
|
||||||
|
The `5.cowgirl_alt` folder is still frontal woman-on-top, not reverse cowgirl,
|
||||||
|
but the repeated pose is lower and closer than the main cowgirl folder: the
|
||||||
|
woman faces the viewer in a low seated squat over the viewer's pelvis, knees
|
||||||
|
bent wide close to the camera, with viewer hands often anchoring the underside
|
||||||
|
of her thighs or hips. Treat `pov_cowgirl_alt_low_squat_penetration` as a
|
||||||
|
candidate until it has fixed-seed Krea2 evidence. Keep this separate from the
|
||||||
|
main cowgirl route so Krea2 can choose between upright straddle wording and
|
||||||
|
closer seated-squat wording.
|
||||||
|
|
||||||
|
### Reverse Cowgirl / Back-Facing Straddle Penetration
|
||||||
|
|
||||||
|
The `cowgirl_reverse` folder repeats a woman-on-top first-person layout where
|
||||||
|
the viewer reclines underneath and the woman faces away from the viewer. Her
|
||||||
|
back, hips, and ass are the closest readable body anchors, with her knees or
|
||||||
|
thighs planted to either side of the viewer's hips; her face may turn back over
|
||||||
|
one shoulder. Treat `pov_reverse_cowgirl_back_facing_penetration` as a
|
||||||
|
candidate until it has fixed-seed Krea2 evidence. Keep it separate from doggy:
|
||||||
|
the viewer is underneath her in a back-facing straddle, not kneeling behind her
|
||||||
|
while she is on all fours.
|
||||||
|
|
||||||
|
### Reverse Cowgirl Alt / Upright Back-Facing Straddle
|
||||||
|
|
||||||
|
The `cowgirl_reversere_alt` folder repeats an upright seated reverse-cowgirl
|
||||||
|
layout. The viewer reclines underneath, while the woman sits upright facing
|
||||||
|
away in a back-facing straddle; her back remains vertical and readable above
|
||||||
|
her hips, with viewer hands often holding her hips, thighs, wrists, or hands.
|
||||||
|
Treat `pov_reverse_cowgirl_alt_upright_back_facing_penetration` as a candidate
|
||||||
|
until it has fixed-seed Krea2 evidence. Keep it separate from
|
||||||
|
`pov_reverse_cowgirl_back_facing_penetration`, which can be closer and more
|
||||||
|
hip-cropped; this alt needs wording that preserves the vertical torso and
|
||||||
|
seated woman-on-top posture.
|
||||||
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,266 @@
|
|||||||
|
# SxCP Eval Loop
|
||||||
|
|
||||||
|
This loop is for tuning the SxCP generator toward stronger Krea2 images.
|
||||||
|
ComfyUI sends a generated prompt, image, and seed to Codex, Codex analyzes the
|
||||||
|
result, then sends back exactly one edited prompt for the next A/B test.
|
||||||
|
Confirmed findings become either generator changes or durable prompt rules in
|
||||||
|
[`krea2-prompt-guide.md`](krea2-prompt-guide.md).
|
||||||
|
The active A/B testing method is recorded in
|
||||||
|
[`krea2-ab-methodology.md`](krea2-ab-methodology.md); update that memory when
|
||||||
|
the method improves.
|
||||||
|
|
||||||
|
## Channels
|
||||||
|
|
||||||
|
- `sxcp_eval_in`: ComfyUI to Codex. Contains the prompt text, image path, and
|
||||||
|
seed.
|
||||||
|
- `sxcp_eval_out`: Codex to ComfyUI. Prompt-only text plus the same seed through
|
||||||
|
the MCP signal when supported. Do not put analysis here.
|
||||||
|
- `sxcp_eval_log`: optional analysis/log channel.
|
||||||
|
|
||||||
|
## MCP Helper Command
|
||||||
|
|
||||||
|
Use the checked helper for bridge calls instead of ad hoc Python snippets. The
|
||||||
|
approved command prefix is:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Common calls:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py list-tools
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_pull --arguments-json '{"channel":"sxcp_eval_in"}'
|
||||||
|
/media/p5/miniforge3/bin/python tools/sxcp_mcp_client.py call-tool comfy_push --arguments-json '{"channel":"sxcp_eval_out","seed":5656565656,"text":"PROMPT_ONLY_POSITIVE_CONDITIONING"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Batch Prompt Helper
|
||||||
|
|
||||||
|
For prompt-axis batches, prepare a local JSON file and use the offline helper to
|
||||||
|
render the approved MCP push/pull commands and an image-presence checklist:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/sxcp_prompt_batch.py validate --batch-json /tmp/sxcp-batch.json
|
||||||
|
python tools/sxcp_prompt_batch.py print-push-commands --batch-json /tmp/sxcp-batch.json
|
||||||
|
python tools/sxcp_prompt_batch.py print-result-template --batch-json /tmp/sxcp-batch.json
|
||||||
|
python tools/sxcp_prompt_batch.py run-batch --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --previous-turn 80 --run
|
||||||
|
python tools/sxcp_prompt_batch.py validate-results --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json
|
||||||
|
python tools/sxcp_prompt_batch.py print-eval-entry-draft --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --variant-key pov_example_variant --baseline-image /absolute/baseline.png --candidate-id controlled_subject_first
|
||||||
|
```
|
||||||
|
|
||||||
|
Batch files use the fixed sampler seed and one positive prompt per probe:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"seed": 8989898989,
|
||||||
|
"channel_out": "sxcp_eval_out",
|
||||||
|
"channel_in": "sxcp_eval_in",
|
||||||
|
"probes": [
|
||||||
|
{
|
||||||
|
"id": "controlled_subject_first",
|
||||||
|
"prompt_order": "subject_first",
|
||||||
|
"text": "SUBJECT_LOOK_FIRST. POSE_HIERARCHY. LOCATION_ANCHORS."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "rough_geometry_axis",
|
||||||
|
"prompt_order": "geometry_only",
|
||||||
|
"text": "POSE_AXIS_ONLY_FOR_DISCOVERY."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`geometry_only` probes are for rough pose-axis discovery and are not durable
|
||||||
|
subject/look-controlled A/B evidence. The helper rejects
|
||||||
|
`sxcp_eval_negative_out`; keep batch prompts positive-only.
|
||||||
|
|
||||||
|
Use `run-batch --run` to push one positive prompt, poll `sxcp_eval_in` until a
|
||||||
|
new turn and absolute PNG image path appear with the fixed sampler seed, write
|
||||||
|
the filled result JSON, then send the next probe. Omit `--run` for a dry-run
|
||||||
|
command preview. After a live run, run `validate-results`; it requires the
|
||||||
|
result probe ids to match the batch order, each turn to advance in batch order,
|
||||||
|
every image path to be an absolute PNG artifact, and every returned seed to
|
||||||
|
match the fixed sampler seed. Then use `print-eval-entry-draft` to create a
|
||||||
|
valid `krea2-eval-log.json` entry draft. Replace the generated summaries and
|
||||||
|
observation with the real visual comparison before recording it with
|
||||||
|
`tools/krea2_record_eval.py`. By default the draft command rejects
|
||||||
|
`geometry_only` candidates; pass `--allow-geometry-only` only when deliberately
|
||||||
|
recording non-controlled prompt-axis evidence.
|
||||||
|
|
||||||
|
## Manual Loop
|
||||||
|
|
||||||
|
Start the helper after sending a test prompt:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tools/sxcp_eval_loop.sh 3
|
||||||
|
```
|
||||||
|
|
||||||
|
Every three minutes it prints a structured request asking Codex to:
|
||||||
|
|
||||||
|
1. Pull `sxcp_eval_in`.
|
||||||
|
2. Record the emitted seed.
|
||||||
|
3. Inspect the image.
|
||||||
|
4. Compare it to the prompt and previous edit.
|
||||||
|
5. Push one prompt-only edit to `sxcp_eval_out`, preserving the same seed through
|
||||||
|
the MCP signal when available.
|
||||||
|
6. Classify the finding as prompt-only, prompt-guide rule, provisional generator
|
||||||
|
improvement, or proven generator fix.
|
||||||
|
7. When leaving a category after same-seed progress over baseline, mirror the
|
||||||
|
best generator-safe wording into the responsible generator path as
|
||||||
|
`provisional_generator_patch`.
|
||||||
|
8. Promote a generator change to proven only when the issue is systemic,
|
||||||
|
repeated, or structurally wrong before rendering.
|
||||||
|
9. Record the finding and update the Krea2 prompt guide when a rule is confirmed.
|
||||||
|
|
||||||
|
Runtime logs are written under `.sxcp_eval/` and ignored by git.
|
||||||
|
|
||||||
|
Durable fixed-seed findings that justify a guide rule, generator patch, or pose
|
||||||
|
variant promotion are recorded in [`krea2-eval-log.json`](krea2-eval-log.json).
|
||||||
|
Method changes belong in [`krea2-ab-methodology.md`](krea2-ab-methodology.md).
|
||||||
|
Use runtime logs for scratch notes; use the JSON log only for evidence that
|
||||||
|
should remain tied to a catalog variant. Image paths in that log point at
|
||||||
|
external ComfyUI artifacts and may be cleaned; the durable evidence is the fixed
|
||||||
|
sampler seed, optional generator seed, prompt summaries, observation, decision,
|
||||||
|
and commit.
|
||||||
|
|
||||||
|
Record durable findings with the checked helper instead of hand-editing the log:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/krea2_record_eval.py --print-template --variant-key pov_footjob_frontal_sole_stroke --seed 1234 --generator-seed 5678 > /tmp/krea2-entry.json
|
||||||
|
python tools/krea2_record_eval.py --entry-json /tmp/krea2-entry.json --dry-run
|
||||||
|
python tools/krea2_record_eval.py --entry-json /tmp/krea2-entry.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Entry template:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "variant-seed-short-finding",
|
||||||
|
"date": "2026-06-29",
|
||||||
|
"variant_key": "pov_example_variant",
|
||||||
|
"seed": 1234,
|
||||||
|
"generator_seed": 5678,
|
||||||
|
"source": "sxcp_eval_mcp",
|
||||||
|
"result": "accepted",
|
||||||
|
"decision": "generator_patch",
|
||||||
|
"baseline_prompt_summary": "What the generated prompt did before the edit.",
|
||||||
|
"candidate_prompt_summary": "What the edited prompt changed for the same seed.",
|
||||||
|
"observation": "What the image comparison proved and why it matters for the generator or guide.",
|
||||||
|
"baseline_image": "/absolute/path/to/baseline.png",
|
||||||
|
"candidate_image": "/absolute/path/to/candidate.png",
|
||||||
|
"commit": "pending"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
To see catalog coverage and the next variants that still need controlled
|
||||||
|
testing, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python tools/krea2_tuning_report.py
|
||||||
|
```
|
||||||
|
|
||||||
|
The report includes atlas references plus prompt cues and avoid cues for the
|
||||||
|
next fixed-seed test candidate. It also shows the latest durable evidence for
|
||||||
|
variants that already have fixed-seed results, including the evidence id, seed,
|
||||||
|
decision, candidate prompt summary, and observation. For each normal next-test
|
||||||
|
candidate, it prints a `krea2_record_eval.py --print-template` command; replace
|
||||||
|
`<fixed_seed>` with the seed from the run you are recording.
|
||||||
|
|
||||||
|
## Optional Command Hook
|
||||||
|
|
||||||
|
If you have a one-shot Codex command you want to run automatically, set:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SXCP_EVAL_CODEX_CMD="codex exec" tools/sxcp_eval_loop.sh 3
|
||||||
|
```
|
||||||
|
|
||||||
|
The request is sent on stdin. The command also receives:
|
||||||
|
|
||||||
|
- `SXCP_EVAL_IN_CHANNEL`
|
||||||
|
- `SXCP_EVAL_OUT_CHANNEL`
|
||||||
|
- `SXCP_EVAL_LOG_CHANNEL`
|
||||||
|
- `SXCP_EVAL_GUIDE_FILE`
|
||||||
|
- `SXCP_EVAL_REQUEST_FILE`
|
||||||
|
- `SXCP_EVAL_CYCLE_DIR`
|
||||||
|
- `SXCP_EVAL_CYCLE`
|
||||||
|
|
||||||
|
## Evaluation Axes
|
||||||
|
|
||||||
|
- Identity consistency
|
||||||
|
- Outfit continuity
|
||||||
|
- Pose/action accuracy
|
||||||
|
- Camera compliance
|
||||||
|
- Location coherence
|
||||||
|
- Crop/framing
|
||||||
|
- Prompt noise/repetition
|
||||||
|
- Model confusion tokens
|
||||||
|
- Seed control/reproducibility
|
||||||
|
- Overall Krea2 image usefulness
|
||||||
|
|
||||||
|
## POV Pose Atlas
|
||||||
|
|
||||||
|
Use `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2` as the local
|
||||||
|
reference atlas for POV pose geometry. The top-level pose folders contain real
|
||||||
|
POV examples, and matching `_control` folders contain solo/control versions.
|
||||||
|
Ignore `bg` and `*_bg` folders for pose rules; they are background plates
|
||||||
|
without people. Treat the pose image folders as the primary source for body
|
||||||
|
geometry; captions are optional and are not present for every folder.
|
||||||
|
|
||||||
|
Suggested workflow:
|
||||||
|
|
||||||
|
1. Choose one pose family, for example `doggy`, `doggy_alt`, `cowgirl`, or
|
||||||
|
`missionary`.
|
||||||
|
2. Sample 5-10 real pose images and their control images.
|
||||||
|
3. Write the repeated geometry as a compact prompt rule.
|
||||||
|
4. Run one fixed-seed Krea2 prompt using that rule.
|
||||||
|
5. Repeat on a second seed or character before changing generator defaults.
|
||||||
|
6. If the prompt itself is structurally contradictory before rendering, patch
|
||||||
|
immediately and add a regression test.
|
||||||
|
|
||||||
|
For POV doggy, the atlas shows that visible viewer thighs, lower torso, or
|
||||||
|
pelvis can be correct. Do not treat them as automatic failures.
|
||||||
|
|
||||||
|
## Seed Contract
|
||||||
|
|
||||||
|
The sampler seed is transport metadata, not prompt text. When the graph emits a
|
||||||
|
sampler seed, an A/B wording test should reuse that exact seed so the image
|
||||||
|
difference mostly comes from wording, not sampling randomness. If the SxCP
|
||||||
|
generator/control seed differs from the sampler seed, record it as
|
||||||
|
`generator_seed` in the eval entry. If a payload has no sampler seed, mark that
|
||||||
|
cycle as uncontrolled and avoid turning the result into a durable generator rule
|
||||||
|
without another controlled run.
|
||||||
|
|
||||||
|
## Positive-Only Conditioning
|
||||||
|
|
||||||
|
`sxcp_eval_out` is positive conditioning only. Never send negative-conditioning
|
||||||
|
phrases such as `no shaft`, `no hands`, `without clothing`, or `avoid X` inside
|
||||||
|
the positive prompt; distilled Krea2 can reinforce or hallucinate the unwanted
|
||||||
|
object from that wording.
|
||||||
|
|
||||||
|
This loop has no active negative-output channel. A same-positive, same-seed
|
||||||
|
probe on seed `424242` compared empty negative conditioning against strong
|
||||||
|
negative text targeting visible prompt attributes, and the rendered image stayed
|
||||||
|
visually unchanged. Do not rely on negative conditioning for Krea2 pose tuning;
|
||||||
|
keep prompt fixes positive-only.
|
||||||
|
|
||||||
|
## Generator Fix Rule
|
||||||
|
|
||||||
|
Use two levels of generator change:
|
||||||
|
|
||||||
|
- `provisional_generator_patch`: apply the best generator-safe wording when
|
||||||
|
leaving a category after fixed-seed progress over baseline. Keep the catalog
|
||||||
|
variant as `candidate`.
|
||||||
|
- `generator_patch`: promote as a proven/default generator rule when the issue
|
||||||
|
is repeated, systemic, or structurally wrong before rendering.
|
||||||
|
|
||||||
|
Examples of proven generator fixes:
|
||||||
|
|
||||||
|
- Selfie wording overrides orbit camera.
|
||||||
|
- Clothing continuity loses the selected softcore outfit.
|
||||||
|
- POV wording makes the off-camera participant the visual subject.
|
||||||
|
- Location camera layout inserts foreground anchors in the wrong place.
|
||||||
|
|
||||||
|
For one-off model drift inside an active category, send a cleaner prompt to
|
||||||
|
`sxcp_eval_out` and keep collecting evidence. When exiting a category, carry
|
||||||
|
forward same-seed improvements over baseline as provisional generator changes
|
||||||
|
and add the rule or weak case to `docs/krea2-prompt-guide.md`.
|
||||||
@@ -39,8 +39,11 @@ HARDCORE_POSITION_FOCUS_CHOICES = [
|
|||||||
]
|
]
|
||||||
HARDCORE_POSITION_KEY_CHOICES = [
|
HARDCORE_POSITION_KEY_CHOICES = [
|
||||||
"missionary",
|
"missionary",
|
||||||
|
"missionary_folded",
|
||||||
"cowgirl",
|
"cowgirl",
|
||||||
|
"cowgirl_alt",
|
||||||
"reverse_cowgirl",
|
"reverse_cowgirl",
|
||||||
|
"reverse_cowgirl_alt",
|
||||||
"doggy",
|
"doggy",
|
||||||
"bent_over",
|
"bent_over",
|
||||||
"face_down_ass_up",
|
"face_down_ass_up",
|
||||||
@@ -123,8 +126,11 @@ HARDCORE_POSITION_FAMILY_SUBCATEGORIES = {
|
|||||||
}
|
}
|
||||||
HARDCORE_POSITION_KEY_MATCHES = {
|
HARDCORE_POSITION_KEY_MATCHES = {
|
||||||
"missionary": ("missionary", "above her", "under her"),
|
"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": ("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": ("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"),
|
"doggy": ("doggy", "all fours", "rear-entry", "from behind"),
|
||||||
"bent_over": ("bent-over", "bent over", "hips raised"),
|
"bent_over": ("bent-over", "bent over", "hips raised"),
|
||||||
"face_down_ass_up": ("face-down", "ass-up"),
|
"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"),
|
"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"),
|
"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 = {
|
HARDCORE_POSITION_AXIS_KEYS = {
|
||||||
"position",
|
"position",
|
||||||
"body_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))
|
return bool(set(metadata_keys) & set(positions))
|
||||||
text = _entry_text(entry).lower()
|
text = _entry_text(entry).lower()
|
||||||
for position in positions:
|
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 True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -749,8 +777,8 @@ def hardcore_position_entry_conflicts(entry: Any, config: dict[str, Any]) -> boo
|
|||||||
text = _entry_text(entry).lower()
|
text = _entry_text(entry).lower()
|
||||||
matched = {
|
matched = {
|
||||||
position
|
position
|
||||||
for position, terms in HARDCORE_POSITION_KEY_MATCHES.items()
|
for position in HARDCORE_POSITION_KEY_MATCHES
|
||||||
if any(term in text for term in terms)
|
if _text_matches_position_key(text, position)
|
||||||
}
|
}
|
||||||
return bool(matched) and not bool(matched & selected)
|
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:
|
if not text:
|
||||||
return []
|
return []
|
||||||
keys: list[str] = []
|
keys: list[str] = []
|
||||||
for key, tokens in HARDCORE_POSITION_KEY_MATCHES.items():
|
for key in HARDCORE_POSITION_KEY_MATCHES:
|
||||||
if any(token in text for token in tokens):
|
if _text_matches_position_key(text, key):
|
||||||
keys.append(key)
|
keys.append(key)
|
||||||
return keys
|
return keys
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,10 @@ def build_climax_role_graph(
|
|||||||
if "lying at the bed edge with thighs open" in context:
|
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."
|
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:
|
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:
|
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."
|
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:
|
if "face-down ass-up" in context:
|
||||||
@@ -53,7 +56,7 @@ def build_climax_role_graph(
|
|||||||
if "kneeling with mouth open" in context:
|
if "kneeling with mouth open" in context:
|
||||||
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
|
return f"{woman} kneels in front of {man} at hip height while {man} ejaculates semen onto her face, lips, and chest."
|
||||||
if "kneeling in front of a standing partner" in context:
|
if "kneeling in front of a standing partner" in context:
|
||||||
return f"{woman} kneels in front of {man} at hip height while {man} stands over her for visible ejaculation."
|
return f"{woman} kneels in front of {man} at hip height while {man} stands over her and ejaculates semen across her body."
|
||||||
if "standing with cum on the body" in context:
|
if "standing with cum on the body" in context:
|
||||||
return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body."
|
return f"{woman} stands braced in front of {man} while he stays close at hip level and ejaculates semen across her body."
|
||||||
if "squatting on top of a partner" in context:
|
if "squatting on top of a partner" in context:
|
||||||
|
|||||||
@@ -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."
|
return f"{primary} reclines with thighs open, one hand between her legs and fingers visibly stimulating her pussy."
|
||||||
if "mutual" in text:
|
if "mutual" in text:
|
||||||
return f"{primary} and {partner} sit close facing each other, both touching themselves while keeping hands, faces, and bodies visible."
|
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:
|
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."
|
return (
|
||||||
if "toy" in text or "vibrator" in text:
|
f"{primary} reclines with thighs open while {partner}'s foreground hand is the largest lower-frame object; "
|
||||||
return f"{primary} reclines with thighs open while {partner} holds a vibrator or toy against her clit, one hand keeping her thigh open."
|
f"her open thighs form a V around the hand, the wrist enters from the bottom center, "
|
||||||
return f"{primary} reclines with thighs open while {partner}'s hand is between her legs, fingers visibly stimulating her pussy."
|
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(
|
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} 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."
|
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 "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:
|
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 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."
|
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."
|
||||||
|
|||||||
@@ -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 "side-lying oral" in position_text or ("side-lying oral" in text and not position_text):
|
||||||
if woman_gives and not man_gives:
|
if woman_gives and not man_gives:
|
||||||
if man_is_pov:
|
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."
|
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:
|
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."
|
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."
|
||||||
|
|||||||
@@ -42,15 +42,34 @@ def build_outercourse_role_graph(
|
|||||||
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
f"{woman} bends forward and kneels very low between the POV viewer's open thighs with her shoulders between his knees, "
|
f"{woman} lies low in a side-pelvis POV beside the POV viewer's open thighs, "
|
||||||
"her face below the POV viewer's penis at testicle height, mouth and tongue on the POV viewer's balls, "
|
"her face is the closest visible partner part, her cheek against the POV viewer's inner thigh and her head low under his pelvis, "
|
||||||
"while his penis points upward in the lower foreground above her forehead."
|
"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 (
|
return (
|
||||||
f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, "
|
f"{man} reclines with legs apart while {woman} lies low beside his inner thigh, "
|
||||||
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"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:
|
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:
|
if man_is_pov:
|
||||||
return (
|
return (
|
||||||
f"{woman} bends forward between the POV viewer's open thighs with her head low under the POV viewer's penis, "
|
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 action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
return (
|
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, "
|
f"{woman} faces the POV viewer with 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."
|
"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 (
|
return (
|
||||||
f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, "
|
f"{man} reclines with hips forward while {woman} faces him with her hips back and both knees bent open, "
|
||||||
f"wrapping both soles around {man}'s penis shaft while the contact stays centered."
|
f"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 action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
if man_is_pov:
|
if man_is_pov:
|
||||||
|
|||||||
@@ -19,15 +19,34 @@ def build_penetration_role_graph(
|
|||||||
item_axis_values: dict[str, Any] | None = None,
|
item_axis_values: dict[str, Any] | None = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
text = _context_text(item_text, item_axis_values)
|
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:
|
if "missionary" in text:
|
||||||
return (
|
return (
|
||||||
f"{woman} lies on her back with legs open around {man}'s hips while {man} is above her between her thighs; "
|
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."
|
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:
|
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."
|
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:
|
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:
|
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."
|
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:
|
if "standing" in text:
|
||||||
|
|||||||
@@ -0,0 +1,235 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
DEFAULT_EVAL_LOG_PATH = ROOT / "docs" / "krea2-eval-log.json"
|
||||||
|
VALID_RESULTS = {"accepted", "rejected", "inconclusive"}
|
||||||
|
VALID_DECISIONS = {
|
||||||
|
"generator_patch",
|
||||||
|
"provisional_generator_patch",
|
||||||
|
"prompt_guide_rule",
|
||||||
|
"prompt_only_retry",
|
||||||
|
"needs_more_tests",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _path_key(path: str | Path | None = None) -> str:
|
||||||
|
return str(Path(path or DEFAULT_EVAL_LOG_PATH).resolve())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=8)
|
||||||
|
def _load_raw_eval_log(path_key: str) -> dict[str, Any]:
|
||||||
|
with Path(path_key).open("r", encoding="utf-8") as handle:
|
||||||
|
data = json.load(handle)
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def clear_cache() -> None:
|
||||||
|
_load_raw_eval_log.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def load_eval_log(path: str | Path | None = None) -> dict[str, Any]:
|
||||||
|
return copy.deepcopy(_load_raw_eval_log(_path_key(path)))
|
||||||
|
|
||||||
|
|
||||||
|
def _text(value: Any) -> str:
|
||||||
|
return value if isinstance(value, str) else ""
|
||||||
|
|
||||||
|
|
||||||
|
def _require_text(errors: list[str], entry: dict[str, Any], key: str, min_len: int) -> None:
|
||||||
|
value = _text(entry.get(key)).strip()
|
||||||
|
if len(value) < min_len:
|
||||||
|
errors.append(f"{key} must be at least {min_len} characters")
|
||||||
|
|
||||||
|
|
||||||
|
def _entry_id_slug(variant_key: str) -> str:
|
||||||
|
value = variant_key.removeprefix("pov_")
|
||||||
|
chars = [char.lower() if char.isalnum() else "-" for char in value]
|
||||||
|
slug = "".join(chars).strip("-")
|
||||||
|
while "--" in slug:
|
||||||
|
slug = slug.replace("--", "-")
|
||||||
|
return slug or "krea2-eval"
|
||||||
|
|
||||||
|
|
||||||
|
def entry_template(
|
||||||
|
variant_key: str,
|
||||||
|
*,
|
||||||
|
seed: int,
|
||||||
|
generator_seed: int | None = None,
|
||||||
|
source: str = "sxcp_eval_mcp",
|
||||||
|
date: str = "",
|
||||||
|
result: str = "inconclusive",
|
||||||
|
decision: str = "needs_more_tests",
|
||||||
|
commit: str = "pending",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if not isinstance(seed, int) or isinstance(seed, bool):
|
||||||
|
raise ValueError("seed must be an integer")
|
||||||
|
if generator_seed is not None and (not isinstance(generator_seed, int) or isinstance(generator_seed, bool)):
|
||||||
|
raise ValueError("generator_seed must be an integer")
|
||||||
|
variant = _text(variant_key).strip()
|
||||||
|
if not variant:
|
||||||
|
raise ValueError("variant_key is required")
|
||||||
|
entry = {
|
||||||
|
"id": f"{_entry_id_slug(variant)}-{seed}-eval",
|
||||||
|
"date": date,
|
||||||
|
"variant_key": variant,
|
||||||
|
"seed": seed,
|
||||||
|
"source": source,
|
||||||
|
"result": result,
|
||||||
|
"decision": decision,
|
||||||
|
"baseline_prompt_summary": f"Replace this with what the generated {variant} prompt did before the edit.",
|
||||||
|
"candidate_prompt_summary": f"Replace this with what the same-seed candidate prompt changed for {variant}.",
|
||||||
|
"observation": f"Replace this with the fixed-seed Krea2 image comparison observation for {variant}.",
|
||||||
|
"baseline_image": "",
|
||||||
|
"candidate_image": "",
|
||||||
|
"commit": commit,
|
||||||
|
}
|
||||||
|
if generator_seed is not None:
|
||||||
|
entry["generator_seed"] = generator_seed
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def validate_entry(
|
||||||
|
entry: dict[str, Any],
|
||||||
|
*,
|
||||||
|
existing_entries: list[dict[str, Any]] | None = None,
|
||||||
|
catalog_keys: set[str] | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
errors: list[str] = []
|
||||||
|
if not isinstance(entry, dict):
|
||||||
|
return ["entry must be an object"]
|
||||||
|
|
||||||
|
_require_text(errors, entry, "id", 6)
|
||||||
|
entry_id = _text(entry.get("id")).strip()
|
||||||
|
if entry_id and existing_entries:
|
||||||
|
existing_ids = {_text(row.get("id")).strip() for row in existing_entries if isinstance(row, dict)}
|
||||||
|
if entry_id in existing_ids:
|
||||||
|
errors.append(f"duplicate id {entry_id!r}")
|
||||||
|
|
||||||
|
_require_text(errors, entry, "variant_key", 8)
|
||||||
|
variant_key = _text(entry.get("variant_key")).strip()
|
||||||
|
if variant_key and catalog_keys is not None and variant_key not in catalog_keys:
|
||||||
|
errors.append(f"unknown variant {variant_key!r}")
|
||||||
|
|
||||||
|
seed = entry.get("seed")
|
||||||
|
if not isinstance(seed, int) or isinstance(seed, bool):
|
||||||
|
errors.append("seed must be an integer")
|
||||||
|
generator_seed = entry.get("generator_seed")
|
||||||
|
if generator_seed is not None and (not isinstance(generator_seed, int) or isinstance(generator_seed, bool)):
|
||||||
|
errors.append("generator_seed must be an integer")
|
||||||
|
|
||||||
|
result = entry.get("result")
|
||||||
|
if result not in VALID_RESULTS:
|
||||||
|
errors.append(f"result must be one of {sorted(VALID_RESULTS)}")
|
||||||
|
|
||||||
|
decision = entry.get("decision")
|
||||||
|
if decision not in VALID_DECISIONS:
|
||||||
|
errors.append(f"decision must be one of {sorted(VALID_DECISIONS)}")
|
||||||
|
|
||||||
|
_require_text(errors, entry, "baseline_prompt_summary", 20)
|
||||||
|
_require_text(errors, entry, "candidate_prompt_summary", 20)
|
||||||
|
_require_text(errors, entry, "observation", 30)
|
||||||
|
|
||||||
|
for image_key in ("baseline_image", "candidate_image"):
|
||||||
|
image_path = _text(entry.get(image_key)).strip()
|
||||||
|
if not image_path:
|
||||||
|
continue
|
||||||
|
path = Path(image_path)
|
||||||
|
if not path.is_absolute():
|
||||||
|
errors.append(f"{image_key} must be absolute when present")
|
||||||
|
if path.suffix.lower() != ".png":
|
||||||
|
errors.append(f"{image_key} must reference a PNG artifact")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def save_eval_log(log: dict[str, Any], *, path: str | Path | None = None) -> None:
|
||||||
|
target = Path(path or DEFAULT_EVAL_LOG_PATH)
|
||||||
|
target.write_text(json.dumps(log, ensure_ascii=True, indent=2) + "\n", encoding="utf-8")
|
||||||
|
clear_cache()
|
||||||
|
|
||||||
|
|
||||||
|
def append_entry(
|
||||||
|
entry: dict[str, Any],
|
||||||
|
*,
|
||||||
|
path: str | Path | None = None,
|
||||||
|
catalog_path: str | Path | None = None,
|
||||||
|
dry_run: bool = False,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
try:
|
||||||
|
from . import krea2_pose_variant_catalog
|
||||||
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
|
import krea2_pose_variant_catalog
|
||||||
|
|
||||||
|
log = load_eval_log(path)
|
||||||
|
rows = log.get("entries")
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
rows = []
|
||||||
|
log["entries"] = rows
|
||||||
|
new_entry = copy.deepcopy(entry)
|
||||||
|
errors = validate_entry(
|
||||||
|
new_entry,
|
||||||
|
existing_entries=[row for row in rows if isinstance(row, dict)],
|
||||||
|
catalog_keys=set(krea2_pose_variant_catalog.variant_keys(path=catalog_path)),
|
||||||
|
)
|
||||||
|
if errors:
|
||||||
|
raise ValueError("; ".join(errors))
|
||||||
|
rows.append(new_entry)
|
||||||
|
if not dry_run:
|
||||||
|
save_eval_log(log, path=path)
|
||||||
|
return copy.deepcopy(log)
|
||||||
|
|
||||||
|
|
||||||
|
def entries(
|
||||||
|
*,
|
||||||
|
variant_key: str | None = None,
|
||||||
|
result: str | None = None,
|
||||||
|
decision: str | None = None,
|
||||||
|
path: str | Path | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
log = load_eval_log(path)
|
||||||
|
rows = log.get("entries") or []
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
return []
|
||||||
|
filtered: list[dict[str, Any]] = []
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
if variant_key is not None and row.get("variant_key") != variant_key:
|
||||||
|
continue
|
||||||
|
if result is not None and row.get("result") != result:
|
||||||
|
continue
|
||||||
|
if decision is not None and row.get("decision") != decision:
|
||||||
|
continue
|
||||||
|
filtered.append(row)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def entries_for_variant(
|
||||||
|
variant_key: str,
|
||||||
|
*,
|
||||||
|
result: str | None = None,
|
||||||
|
decision: str | None = None,
|
||||||
|
path: str | Path | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
return entries(variant_key=variant_key, result=result, decision=decision, path=path)
|
||||||
|
|
||||||
|
|
||||||
|
def variant_keys(
|
||||||
|
*,
|
||||||
|
result: str | None = None,
|
||||||
|
decision: str | None = None,
|
||||||
|
path: str | Path | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
keys: list[str] = []
|
||||||
|
for row in entries(result=result, decision=decision, path=path):
|
||||||
|
key = row.get("variant_key")
|
||||||
|
if key and key not in keys:
|
||||||
|
keys.append(str(key))
|
||||||
|
return keys
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import copy
|
||||||
|
import json
|
||||||
|
from functools import lru_cache
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent
|
||||||
|
DEFAULT_CATALOG_PATH = ROOT / "categories" / "krea2_pov_pose_variants.json"
|
||||||
|
|
||||||
|
|
||||||
|
def _path_key(path: str | Path | None = None) -> str:
|
||||||
|
return str(Path(path or DEFAULT_CATALOG_PATH).resolve())
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=8)
|
||||||
|
def _load_raw_catalog(path_key: str) -> dict[str, Any]:
|
||||||
|
with Path(path_key).open("r", encoding="utf-8") as handle:
|
||||||
|
data = json.load(handle)
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def clear_cache() -> None:
|
||||||
|
_load_raw_catalog.cache_clear()
|
||||||
|
|
||||||
|
|
||||||
|
def load_catalog(path: str | Path | None = None) -> dict[str, Any]:
|
||||||
|
return copy.deepcopy(_load_raw_catalog(_path_key(path)))
|
||||||
|
|
||||||
|
|
||||||
|
def variants(
|
||||||
|
*,
|
||||||
|
status: str | None = None,
|
||||||
|
family: str | None = None,
|
||||||
|
action_family: str | None = None,
|
||||||
|
path: str | Path | None = None,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
catalog = load_catalog(path)
|
||||||
|
rows = catalog.get("variants") or []
|
||||||
|
if not isinstance(rows, list):
|
||||||
|
return []
|
||||||
|
filtered: list[dict[str, Any]] = []
|
||||||
|
for row in rows:
|
||||||
|
if not isinstance(row, dict):
|
||||||
|
continue
|
||||||
|
if status is not None and row.get("status") != status:
|
||||||
|
continue
|
||||||
|
if family is not None and row.get("family") != family:
|
||||||
|
continue
|
||||||
|
if action_family is not None and row.get("action_family") != action_family:
|
||||||
|
continue
|
||||||
|
filtered.append(row)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
def variant_keys(
|
||||||
|
*,
|
||||||
|
status: str | None = None,
|
||||||
|
family: str | None = None,
|
||||||
|
action_family: str | None = None,
|
||||||
|
path: str | Path | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
return [
|
||||||
|
str(row.get("key"))
|
||||||
|
for row in variants(status=status, family=family, action_family=action_family, path=path)
|
||||||
|
if row.get("key")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_variant(key: str, *, path: str | Path | None = None) -> dict[str, Any]:
|
||||||
|
for row in variants(path=path):
|
||||||
|
if row.get("key") == key:
|
||||||
|
return row
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def reference_paths(key: str, *, path: str | Path | None = None) -> list[Path]:
|
||||||
|
catalog = load_catalog(path)
|
||||||
|
atlas_root = Path(str(catalog.get("atlas_root") or ""))
|
||||||
|
variant = get_variant(key, path=path)
|
||||||
|
refs = variant.get("reference_images") or []
|
||||||
|
if not isinstance(refs, list):
|
||||||
|
return []
|
||||||
|
paths: list[Path] = []
|
||||||
|
for ref in refs:
|
||||||
|
ref_path = Path(str(ref))
|
||||||
|
if ".." in ref_path.parts:
|
||||||
|
continue
|
||||||
|
paths.append(atlas_root / ref_path)
|
||||||
|
return paths
|
||||||
|
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
try:
|
||||||
|
from . import krea2_eval_log, krea2_pose_variant_catalog
|
||||||
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
|
import krea2_eval_log
|
||||||
|
import krea2_pose_variant_catalog
|
||||||
|
|
||||||
|
|
||||||
|
def _coverage_state(status: str, accepted_count: int) -> str:
|
||||||
|
if status == "proven" and accepted_count > 0:
|
||||||
|
return "proven_with_evidence"
|
||||||
|
if status == "proven":
|
||||||
|
return "proven_missing_evidence"
|
||||||
|
if status == "candidate" and accepted_count == 0:
|
||||||
|
return "needs_fixed_seed_tests"
|
||||||
|
if status == "unstable":
|
||||||
|
return "needs_stronger_control"
|
||||||
|
return "tracked"
|
||||||
|
|
||||||
|
|
||||||
|
def _latest_evidence(entries: list[dict[str, Any]], *, result: str | None = None) -> dict[str, Any]:
|
||||||
|
filtered = [entry for entry in entries if result is None or entry.get("result") == result]
|
||||||
|
if not filtered:
|
||||||
|
return {}
|
||||||
|
entry = filtered[-1]
|
||||||
|
return {
|
||||||
|
"id": entry.get("id") or "",
|
||||||
|
"seed": entry.get("seed"),
|
||||||
|
"generator_seed": entry.get("generator_seed"),
|
||||||
|
"result": entry.get("result") or "",
|
||||||
|
"decision": entry.get("decision") or "",
|
||||||
|
"baseline_prompt_summary": entry.get("baseline_prompt_summary") or "",
|
||||||
|
"candidate_prompt_summary": entry.get("candidate_prompt_summary") or "",
|
||||||
|
"observation": entry.get("observation") or "",
|
||||||
|
"needs_expansion": bool(entry.get("needs_expansion")),
|
||||||
|
"commit": entry.get("commit") or "",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def coverage_rows() -> list[dict[str, Any]]:
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for variant in krea2_pose_variant_catalog.variants():
|
||||||
|
key = str(variant.get("key") or "")
|
||||||
|
evidence = krea2_eval_log.entries_for_variant(key)
|
||||||
|
accepted = [entry for entry in evidence if entry.get("result") == "accepted"]
|
||||||
|
status = str(variant.get("status") or "")
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"family": variant.get("family") or "",
|
||||||
|
"action_family": variant.get("action_family") or "",
|
||||||
|
"status": status,
|
||||||
|
"difficulty": variant.get("difficulty") or "",
|
||||||
|
"priority": variant.get("priority") or "",
|
||||||
|
"control_requirement": variant.get("control_requirement") or "",
|
||||||
|
"coverage_state": _coverage_state(status, len(accepted)),
|
||||||
|
"accepted_evidence_count": len(accepted),
|
||||||
|
"total_evidence_count": len(evidence),
|
||||||
|
"latest_evidence": _latest_evidence(evidence),
|
||||||
|
"latest_accepted_evidence": _latest_evidence(evidence, result="accepted"),
|
||||||
|
"reference_count": len(variant.get("reference_images") or []),
|
||||||
|
"guide_section": (variant.get("evidence") or {}).get("guide_section", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def coverage_summary() -> dict[str, Any]:
|
||||||
|
rows = coverage_rows()
|
||||||
|
status_counts = Counter(row.get("status") for row in rows)
|
||||||
|
state_counts = Counter(row.get("coverage_state") for row in rows)
|
||||||
|
return {
|
||||||
|
"variant_count": len(rows),
|
||||||
|
"status_counts": dict(status_counts),
|
||||||
|
"coverage_state_counts": dict(state_counts),
|
||||||
|
"variants_without_accepted_evidence": [
|
||||||
|
str(row.get("key"))
|
||||||
|
for row in rows
|
||||||
|
if int(row.get("accepted_evidence_count") or 0) == 0
|
||||||
|
],
|
||||||
|
"next_test_candidates": [
|
||||||
|
str(row.get("key"))
|
||||||
|
for row in rows
|
||||||
|
if row.get("coverage_state") in {"needs_fixed_seed_tests", "proven_missing_evidence"}
|
||||||
|
],
|
||||||
|
"stronger_control_cases": [
|
||||||
|
str(row.get("key"))
|
||||||
|
for row in rows
|
||||||
|
if row.get("coverage_state") == "needs_stronger_control"
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_atlas_root() -> Path:
|
||||||
|
catalog = krea2_pose_variant_catalog.load_catalog()
|
||||||
|
return Path(str(catalog.get("atlas_root") or ""))
|
||||||
|
|
||||||
|
|
||||||
|
def _mapped_atlas_folders() -> dict[str, list[str]]:
|
||||||
|
mapped: dict[str, list[str]] = {}
|
||||||
|
for variant in krea2_pose_variant_catalog.variants():
|
||||||
|
key = str(variant.get("key") or "")
|
||||||
|
for folder in variant.get("atlas_folders") or []:
|
||||||
|
folder_name = str(folder)
|
||||||
|
if not folder_name:
|
||||||
|
continue
|
||||||
|
mapped.setdefault(folder_name, []).append(key)
|
||||||
|
return mapped
|
||||||
|
|
||||||
|
|
||||||
|
def _is_background_or_control_folder(folder_name: str) -> bool:
|
||||||
|
lower = folder_name.lower()
|
||||||
|
return (
|
||||||
|
lower == "bg"
|
||||||
|
or lower == "woman"
|
||||||
|
or lower.endswith("_control")
|
||||||
|
or lower.endswith("_bg")
|
||||||
|
or lower.endswith("_control_bg")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _sample_pngs(folder: Path, limit: int) -> list[str]:
|
||||||
|
if not folder.is_dir() or limit <= 0:
|
||||||
|
return []
|
||||||
|
return [str(path) for path in sorted(folder.glob("*.png"), key=lambda path: path.name.lower())[:limit]]
|
||||||
|
|
||||||
|
|
||||||
|
def atlas_folder_rows(atlas_root: str | Path | None = None) -> list[dict[str, Any]]:
|
||||||
|
root = Path(atlas_root) if atlas_root is not None else _catalog_atlas_root()
|
||||||
|
if not root.is_dir():
|
||||||
|
return []
|
||||||
|
mapped = _mapped_atlas_folders()
|
||||||
|
rows: list[dict[str, Any]] = []
|
||||||
|
for folder in sorted(root.iterdir(), key=lambda path: path.name.lower()):
|
||||||
|
if not folder.is_dir():
|
||||||
|
continue
|
||||||
|
folder_name = folder.name
|
||||||
|
if _is_background_or_control_folder(folder_name):
|
||||||
|
continue
|
||||||
|
image_count = sum(1 for _ in folder.glob("*.png"))
|
||||||
|
if image_count <= 0:
|
||||||
|
continue
|
||||||
|
control_folder = root / f"{folder_name}_control"
|
||||||
|
variant_keys = mapped.get(folder_name, [])
|
||||||
|
if not variant_keys and not control_folder.is_dir():
|
||||||
|
continue
|
||||||
|
rows.append(
|
||||||
|
{
|
||||||
|
"folder": folder_name,
|
||||||
|
"image_count": image_count,
|
||||||
|
"mapped": bool(variant_keys),
|
||||||
|
"variant_keys": list(variant_keys),
|
||||||
|
"control_folder": str(control_folder) if control_folder.is_dir() else "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
def atlas_coverage_summary(atlas_root: str | Path | None = None) -> dict[str, Any]:
|
||||||
|
rows = atlas_folder_rows(atlas_root=atlas_root)
|
||||||
|
unmapped = [str(row.get("folder")) for row in rows if not row.get("mapped")]
|
||||||
|
return {
|
||||||
|
"pose_folder_count": len(rows),
|
||||||
|
"mapped_folder_count": len(rows) - len(unmapped),
|
||||||
|
"unmapped_folder_count": len(unmapped),
|
||||||
|
"unmapped_folders": unmapped,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _suggested_variant_key(folder_name: str) -> str:
|
||||||
|
if folder_name.lower() == "ready":
|
||||||
|
return "pov_ejaculation_aftermath_open_thigh_candidate"
|
||||||
|
normalized = "".join(char if char.isalnum() else "_" for char in folder_name.lower()).strip("_")
|
||||||
|
while "__" in normalized:
|
||||||
|
normalized = normalized.replace("__", "_")
|
||||||
|
return f"pov_{normalized}_candidate" if normalized else "pov_unmapped_candidate"
|
||||||
|
|
||||||
|
|
||||||
|
def atlas_gap_plans(atlas_root: str | Path | None = None, sample_limit: int = 3) -> list[dict[str, Any]]:
|
||||||
|
root = Path(atlas_root) if atlas_root is not None else _catalog_atlas_root()
|
||||||
|
plans: list[dict[str, Any]] = []
|
||||||
|
for row in atlas_folder_rows(atlas_root=root):
|
||||||
|
if row.get("mapped"):
|
||||||
|
continue
|
||||||
|
folder_name = str(row.get("folder") or "")
|
||||||
|
folder_path = root / folder_name
|
||||||
|
control_folder = Path(str(row.get("control_folder") or ""))
|
||||||
|
plans.append(
|
||||||
|
{
|
||||||
|
"folder": folder_name,
|
||||||
|
"suggested_variant_key": _suggested_variant_key(folder_name),
|
||||||
|
"image_count": int(row.get("image_count") or 0),
|
||||||
|
"sample_images": _sample_pngs(folder_path, sample_limit),
|
||||||
|
"control_images": _sample_pngs(control_folder, sample_limit),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return plans
|
||||||
|
|
||||||
|
|
||||||
|
def next_test_plans() -> list[dict[str, Any]]:
|
||||||
|
rows_by_key = {str(row.get("key")): row for row in coverage_rows()}
|
||||||
|
plans: list[dict[str, Any]] = []
|
||||||
|
for key in coverage_summary()["next_test_candidates"]:
|
||||||
|
variant = krea2_pose_variant_catalog.get_variant(key)
|
||||||
|
if not variant:
|
||||||
|
continue
|
||||||
|
row = rows_by_key.get(key, {})
|
||||||
|
evidence = variant.get("evidence") or {}
|
||||||
|
plans.append(
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"family": variant.get("family") or "",
|
||||||
|
"action_family": variant.get("action_family") or "",
|
||||||
|
"status": variant.get("status") or "",
|
||||||
|
"coverage_state": row.get("coverage_state") or "",
|
||||||
|
"canonical_geometry": variant.get("canonical_geometry") or "",
|
||||||
|
"prompt_cues": list(variant.get("prompt_cues") or []),
|
||||||
|
"avoid_cues": list(variant.get("avoid_cues") or []),
|
||||||
|
"reference_paths": [str(path) for path in krea2_pose_variant_catalog.reference_paths(key)],
|
||||||
|
"generator_hook": variant.get("generator_hook") or {},
|
||||||
|
"guide_section": evidence.get("guide_section") or "",
|
||||||
|
"notes": evidence.get("notes") or "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return plans
|
||||||
|
|
||||||
|
|
||||||
|
def guide_expansion_plans() -> list[dict[str, Any]]:
|
||||||
|
plans: list[dict[str, Any]] = []
|
||||||
|
for row in coverage_rows():
|
||||||
|
latest_accepted = row.get("latest_accepted_evidence") or {}
|
||||||
|
decision = str(latest_accepted.get("decision") or "")
|
||||||
|
if decision not in {"prompt_guide_rule", "needs_more_tests"} and not (
|
||||||
|
decision == "provisional_generator_patch" and latest_accepted.get("needs_expansion")
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
key = str(row.get("key") or "")
|
||||||
|
variant = krea2_pose_variant_catalog.get_variant(key)
|
||||||
|
if not variant:
|
||||||
|
continue
|
||||||
|
evidence = variant.get("evidence") or {}
|
||||||
|
plans.append(
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"family": variant.get("family") or "",
|
||||||
|
"action_family": variant.get("action_family") or "",
|
||||||
|
"status": variant.get("status") or "",
|
||||||
|
"coverage_state": row.get("coverage_state") or "",
|
||||||
|
"target": "multi_seed_multi_woman_matrix",
|
||||||
|
"latest_accepted_id": latest_accepted.get("id") or "",
|
||||||
|
"latest_accepted_seed": latest_accepted.get("seed"),
|
||||||
|
"latest_accepted_decision": decision,
|
||||||
|
"accepted_evidence_count": row.get("accepted_evidence_count") or 0,
|
||||||
|
"total_evidence_count": row.get("total_evidence_count") or 0,
|
||||||
|
"canonical_geometry": variant.get("canonical_geometry") or "",
|
||||||
|
"prompt_cues": list(variant.get("prompt_cues") or []),
|
||||||
|
"avoid_cues": list(variant.get("avoid_cues") or []),
|
||||||
|
"reference_paths": [str(path) for path in krea2_pose_variant_catalog.reference_paths(key)],
|
||||||
|
"generator_hook": variant.get("generator_hook") or {},
|
||||||
|
"guide_section": evidence.get("guide_section") or "",
|
||||||
|
"notes": evidence.get("notes") or "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return plans
|
||||||
|
|
||||||
|
|
||||||
|
def next_eval_template_commands(*, seed_token: str = "<fixed_seed>") -> list[dict[str, str]]:
|
||||||
|
commands: list[dict[str, str]] = []
|
||||||
|
for plan in next_test_plans():
|
||||||
|
key = str(plan.get("key") or "")
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
commands.append(
|
||||||
|
{
|
||||||
|
"key": key,
|
||||||
|
"command": f"python tools/krea2_record_eval.py --print-template --variant-key {key} --seed {seed_token}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return commands
|
||||||
|
|
||||||
|
|
||||||
|
def markdown_report(atlas_root: str | Path | None = None) -> str:
|
||||||
|
lines = [
|
||||||
|
"# Krea2 Pose Variant Coverage",
|
||||||
|
"",
|
||||||
|
"| Variant | Status | Evidence | State |",
|
||||||
|
"| --- | --- | ---: | --- |",
|
||||||
|
]
|
||||||
|
for row in coverage_rows():
|
||||||
|
lines.append(
|
||||||
|
f"| {row['key']} | {row['status']} | {row['accepted_evidence_count']}/{row['total_evidence_count']} | {row['coverage_state']} |"
|
||||||
|
)
|
||||||
|
evidence_rows = [row for row in coverage_rows() if row.get("latest_evidence")]
|
||||||
|
if evidence_rows:
|
||||||
|
lines.extend(["", "## Latest Evidence", ""])
|
||||||
|
for row in evidence_rows:
|
||||||
|
evidence = row.get("latest_evidence") or {}
|
||||||
|
seed = evidence.get("seed")
|
||||||
|
seed_text = f"seed {seed}" if isinstance(seed, int) else "seed unknown"
|
||||||
|
generator_seed = evidence.get("generator_seed")
|
||||||
|
generator_seed_text = f", generator seed {generator_seed}" if isinstance(generator_seed, int) else ""
|
||||||
|
commit = evidence.get("commit") or "uncommitted"
|
||||||
|
lines.append(
|
||||||
|
f"- {row['key']}: {evidence.get('id') or 'unnamed'} ({evidence.get('result') or 'unknown'}, {seed_text}{generator_seed_text}, {evidence.get('decision') or 'unknown'}, commit {commit})"
|
||||||
|
)
|
||||||
|
if evidence.get("candidate_prompt_summary"):
|
||||||
|
lines.append(f" Candidate: {evidence['candidate_prompt_summary']}")
|
||||||
|
if evidence.get("observation"):
|
||||||
|
lines.append(f" Observation: {evidence['observation']}")
|
||||||
|
accepted = row.get("latest_accepted_evidence") or {}
|
||||||
|
if accepted and accepted.get("id") != evidence.get("id"):
|
||||||
|
accepted_seed = accepted.get("seed")
|
||||||
|
accepted_seed_text = f"seed {accepted_seed}" if isinstance(accepted_seed, int) else "seed unknown"
|
||||||
|
accepted_generator_seed = accepted.get("generator_seed")
|
||||||
|
accepted_generator_seed_text = (
|
||||||
|
f", generator seed {accepted_generator_seed}" if isinstance(accepted_generator_seed, int) else ""
|
||||||
|
)
|
||||||
|
accepted_commit = accepted.get("commit") or "uncommitted"
|
||||||
|
lines.append(
|
||||||
|
f" Latest accepted: {accepted.get('id') or 'unnamed'} ({accepted.get('result') or 'unknown'}, {accepted_seed_text}{accepted_generator_seed_text}, {accepted.get('decision') or 'unknown'}, commit {accepted_commit})"
|
||||||
|
)
|
||||||
|
if accepted.get("candidate_prompt_summary"):
|
||||||
|
lines.append(f" Accepted candidate: {accepted['candidate_prompt_summary']}")
|
||||||
|
if accepted.get("observation"):
|
||||||
|
lines.append(f" Accepted observation: {accepted['observation']}")
|
||||||
|
summary = coverage_summary()
|
||||||
|
if summary["next_test_candidates"]:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"## Next Fixed-Seed Tests",
|
||||||
|
"",
|
||||||
|
*[f"- {key}" for key in summary["next_test_candidates"]],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
template_commands = next_eval_template_commands()
|
||||||
|
if template_commands:
|
||||||
|
lines.extend(["", "## Eval Entry Template Commands", ""])
|
||||||
|
for command in template_commands:
|
||||||
|
lines.append(f"- {command['key']}: `{command['command']}`")
|
||||||
|
stronger_control_rows = [row for row in coverage_rows() if row.get("coverage_state") == "needs_stronger_control"]
|
||||||
|
if stronger_control_rows:
|
||||||
|
lines.extend(["", "## Stronger Control Cases", ""])
|
||||||
|
for row in stronger_control_rows:
|
||||||
|
difficulty = row.get("difficulty") or "unrated"
|
||||||
|
priority = row.get("priority") or "unprioritized"
|
||||||
|
control_requirement = row.get("control_requirement") or "control_needed"
|
||||||
|
lines.append(
|
||||||
|
f"- {row['key']}: {difficulty}, {priority} priority, {control_requirement}"
|
||||||
|
)
|
||||||
|
expansion_plans = guide_expansion_plans()
|
||||||
|
if expansion_plans:
|
||||||
|
lines.extend(["", "## Guide/Fragile Evidence Expansion", ""])
|
||||||
|
for plan in expansion_plans:
|
||||||
|
seed = plan.get("latest_accepted_seed")
|
||||||
|
seed_text = f"seed {seed}" if isinstance(seed, int) else "seed unknown"
|
||||||
|
lines.append(
|
||||||
|
f"- {plan['key']}: {plan['target']} after {plan['latest_accepted_decision']} "
|
||||||
|
f"({plan['latest_accepted_id']}, {seed_text})"
|
||||||
|
)
|
||||||
|
plans = next_test_plans()
|
||||||
|
if plans:
|
||||||
|
lines.extend(["", "## Next Test Plans"])
|
||||||
|
for plan in plans:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
f"### {plan['key']}",
|
||||||
|
"",
|
||||||
|
f"- Geometry: {plan['canonical_geometry']}",
|
||||||
|
f"- References: {', '.join(plan['reference_paths']) or 'none'}",
|
||||||
|
"- Prompt cues:",
|
||||||
|
*[f" - {cue}" for cue in plan["prompt_cues"]],
|
||||||
|
"- Avoid cues:",
|
||||||
|
*[f" - {cue}" for cue in plan["avoid_cues"]],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
atlas_summary = atlas_coverage_summary(atlas_root=atlas_root)
|
||||||
|
if atlas_summary["pose_folder_count"]:
|
||||||
|
unmapped = atlas_summary["unmapped_folders"]
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"## Atlas Folder Coverage",
|
||||||
|
"",
|
||||||
|
f"- Pose folders: {atlas_summary['pose_folder_count']}",
|
||||||
|
f"- Mapped folders: {atlas_summary['mapped_folder_count']}",
|
||||||
|
f"- Unmapped folders: {atlas_summary['unmapped_folder_count']}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
if unmapped:
|
||||||
|
lines.extend(["", "Unmapped atlas folders:", *[f"- {folder}" for folder in unmapped]])
|
||||||
|
gap_plans = atlas_gap_plans(atlas_root=atlas_root)
|
||||||
|
if gap_plans:
|
||||||
|
lines.extend(["", "## Atlas Gap Plans"])
|
||||||
|
for plan in gap_plans:
|
||||||
|
sample_images = plan["sample_images"]
|
||||||
|
control_images = plan["control_images"]
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
f"### {plan['folder']}",
|
||||||
|
"",
|
||||||
|
f"- Suggested key: {plan['suggested_variant_key']}",
|
||||||
|
f"- Pose images: {plan['image_count']}",
|
||||||
|
f"- Samples: {', '.join(sample_images) or 'none'}",
|
||||||
|
f"- Controls: {', '.join(control_images) or 'none'}",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
_ = argv
|
||||||
|
print(markdown_report())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv[1:]))
|
||||||
@@ -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:
|
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"
|
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:
|
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:
|
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"
|
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:
|
if "face-down ass-up" in text or "lies face-down" in text or "face down" in text:
|
||||||
@@ -96,7 +99,7 @@ def climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None)
|
|||||||
if "kneeling with mouth open" in text:
|
if "kneeling with mouth open" in text:
|
||||||
return "the woman kneels in front of the man at hip height as he ejaculates semen onto her face, lips, and chest"
|
return "the woman kneels in front of the man at hip height as he ejaculates semen onto her face, lips, and chest"
|
||||||
if "kneeling in front of a standing partner" in text:
|
if "kneeling in front of a standing partner" in text:
|
||||||
return "the woman kneels in front of the man at hip height while he stands over her for visible ejaculation"
|
return "the woman kneels in front of the man at hip height while he stands over her and ejaculates semen across her body"
|
||||||
if "standing with cum on the body" in text:
|
if "standing with cum on the body" in text:
|
||||||
return "the woman stands braced in front of the man while he stands close at hip level and ejaculates semen across her body"
|
return "the woman stands braced in front of the man while he stands close at hip level and ejaculates semen across her body"
|
||||||
if "squatting on top of a partner" in text:
|
if "squatting on top of a partner" in text:
|
||||||
@@ -116,7 +119,10 @@ def climax_role_graph(role_graph: str, hard_item: str, axis_values: Any = None)
|
|||||||
or "arousal dripping from pussy" in text
|
or "arousal dripping from pussy" in text
|
||||||
or "open thighs" 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:
|
if role_graph:
|
||||||
return 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"
|
return "the woman lies on her back with thighs open while the man kneels between her legs and ejaculates semen across her body"
|
||||||
|
|||||||
@@ -179,6 +179,10 @@ def hardcore_pose_anchor(role_graph: str, hard_item: str, composition: str = "",
|
|||||||
return "rear-entry anal pose"
|
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:
|
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"
|
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 = (
|
positions = (
|
||||||
"missionary",
|
"missionary",
|
||||||
"reverse cowgirl",
|
"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",
|
"with the giver kneeling between the receiver's open thighs",
|
||||||
)
|
)
|
||||||
return "with the giver kneeling at the receiver's hips"
|
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:
|
if "reverse cowgirl" in text:
|
||||||
return cast_phrase(
|
return cast_phrase(
|
||||||
"with the man lying on his back under the woman while she straddles his hips facing away",
|
"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()
|
action = _clean(action).lower()
|
||||||
if is_close_foreplay_text(action):
|
if is_close_foreplay_text(action):
|
||||||
return "single-frame close-body first-person position"
|
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:
|
if "pov reverse cowgirl" in action:
|
||||||
return "reverse-cowgirl first-person position"
|
return "reverse-cowgirl first-person position"
|
||||||
if "pov cowgirl" in action:
|
if "pov cowgirl" in action:
|
||||||
|
|||||||
@@ -51,6 +51,31 @@ class KreaConfiguredCastDependencies:
|
|||||||
paragraph: Callable[[list[str]], str]
|
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(
|
def format_configured_cast_result(
|
||||||
request: KreaConfiguredCastRequest,
|
request: KreaConfiguredCastRequest,
|
||||||
deps: KreaConfiguredCastDependencies,
|
deps: KreaConfiguredCastDependencies,
|
||||||
@@ -88,13 +113,14 @@ def format_configured_cast_result(
|
|||||||
item = deps.natural_label_text(item, cast_labels)
|
item = deps.natural_label_text(item, cast_labels)
|
||||||
axis_values = deps.sanitize_hardcore_axis_values(row.get("item_axis_values"))
|
axis_values = deps.sanitize_hardcore_axis_values(row.get("item_axis_values"))
|
||||||
detail_density = deps.normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
|
detail_density = deps.normalize_hardcore_detail_density(row.get("hardcore_detail_density"))
|
||||||
|
action_family = deps.row_action_family(row)
|
||||||
action = deps.hardcore_action_sentence(
|
action = deps.hardcore_action_sentence(
|
||||||
role_graph,
|
role_graph,
|
||||||
item,
|
item,
|
||||||
source_composition,
|
source_composition,
|
||||||
axis_values,
|
axis_values,
|
||||||
detail_density,
|
detail_density,
|
||||||
deps.row_action_family(row),
|
action_family,
|
||||||
)
|
)
|
||||||
action = deps.pov_action_phrase(
|
action = deps.pov_action_phrase(
|
||||||
action,
|
action,
|
||||||
@@ -105,9 +131,15 @@ def format_configured_cast_result(
|
|||||||
axis_values,
|
axis_values,
|
||||||
detail_density,
|
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)
|
output_composition = deps.pov_composition_text(composition, pov_labels)
|
||||||
parts = [
|
parts = [
|
||||||
action,
|
action,
|
||||||
|
scene_anchor,
|
||||||
deps.pov_camera_phrase(pov_labels),
|
deps.pov_camera_phrase(pov_labels),
|
||||||
cast_prose,
|
cast_prose,
|
||||||
f"A consensual explicit adult scene with {subject}" if not action else "",
|
f"A consensual explicit adult scene with {subject}" if not action else "",
|
||||||
|
|||||||
+1
-1
@@ -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"
|
"Camera is the male participant's first-person creator view in one continuous frame, with him implied by perspective or foreground cues"
|
||||||
)
|
)
|
||||||
return (
|
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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+171
-23
@@ -36,12 +36,30 @@ def _clean(value: Any) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def pov_ejaculation_target(context: str) -> str:
|
def pov_ejaculation_target(context: str) -> str:
|
||||||
if any(token in context for token in ("face", "mouth", "lips", "tongue", "chin")):
|
if any(
|
||||||
return "onto her face and chest"
|
token in context
|
||||||
if any(token in context for token in ("lower back", "ass", "rear-entry", "face-down", "bent-over", "doggy")):
|
for token in (
|
||||||
|
"lower back",
|
||||||
|
"ass",
|
||||||
|
"rear-entry",
|
||||||
|
"rear entry",
|
||||||
|
"face-down",
|
||||||
|
"face down",
|
||||||
|
"bent-over",
|
||||||
|
"bent over",
|
||||||
|
"doggy",
|
||||||
|
"on all fours",
|
||||||
|
"hips high",
|
||||||
|
"hips raised",
|
||||||
|
"raised ass",
|
||||||
|
"behind her",
|
||||||
|
)
|
||||||
|
):
|
||||||
return "across her ass, thighs, and lower back"
|
return "across her ass, thighs, and lower back"
|
||||||
if any(token in context for token in ("pussy", "open thighs", "thighs", "legs open")):
|
if any(token in context for token in ("pussy", "open thighs", "thighs", "legs open")):
|
||||||
return "across her pussy and thighs"
|
return "across her pussy and thighs"
|
||||||
|
if any(token in context for token in ("face", "mouth", "lips", "tongue", "chin")):
|
||||||
|
return "onto her face and chest"
|
||||||
return "onto her body"
|
return "onto her body"
|
||||||
|
|
||||||
|
|
||||||
@@ -71,6 +89,48 @@ def pov_contact_clause(
|
|||||||
return contact
|
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:
|
def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
|
||||||
detail = _clean(detail).strip(" .;")
|
detail = _clean(detail).strip(" .;")
|
||||||
if not detail:
|
if not detail:
|
||||||
@@ -82,7 +142,7 @@ def pov_clean_detail(detail: Any, context: str, detail_density: str) -> str:
|
|||||||
detail = re.sub(r"\bhis\b", "the viewer's", detail, flags=re.IGNORECASE)
|
detail = re.sub(r"\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"\bhim\b", "the viewer", detail, flags=re.IGNORECASE)
|
||||||
detail = re.sub(
|
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,
|
detail,
|
||||||
flags=re.IGNORECASE,
|
flags=re.IGNORECASE,
|
||||||
@@ -233,6 +293,8 @@ def pov_hardcore_pose_sentence(
|
|||||||
"anal",
|
"anal",
|
||||||
"cowgirl",
|
"cowgirl",
|
||||||
"missionary",
|
"missionary",
|
||||||
|
"knees-to-chest",
|
||||||
|
"knees to chest",
|
||||||
"doggy",
|
"doggy",
|
||||||
"rear-entry",
|
"rear-entry",
|
||||||
"spooning",
|
"spooning",
|
||||||
@@ -244,6 +306,19 @@ def pov_hardcore_pose_sentence(
|
|||||||
"climax",
|
"climax",
|
||||||
)
|
)
|
||||||
has_penetrative_context = any(token in context or token in action_lower for token in penetrative_tokens)
|
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 (
|
if (
|
||||||
"face-sitting" in context
|
"face-sitting" in context
|
||||||
@@ -261,16 +336,29 @@ def pov_hardcore_pose_sentence(
|
|||||||
action_kind = outercourse_policy.infer_outercourse_action_kind(context, action_lower)
|
action_kind = outercourse_policy.infer_outercourse_action_kind(context, action_lower)
|
||||||
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
if action_kind == outercourse_policy.OUTERCOURSE_BOOBJOB:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman kneels low between the viewer's open thighs with her torso bent forward over his pelvis; "
|
"POV boobjob position: the viewer reclines with thighs open while the woman kneels upright between his legs facing him; "
|
||||||
"both hands push her breasts inward around the viewer's penis in the lower foreground, the penis held between her breasts, "
|
"the viewer's penis rises vertically in the lower foreground and is squeezed between her pressed-together breasts; "
|
||||||
"with her chin and lips directly above the glans at the tip"
|
"the woman's own fingers and nails cup her breasts from the outside and push soft breast tissue inward around the shaft, "
|
||||||
|
"with the glans emerging above the cleavage directly below her mouth"
|
||||||
)
|
)
|
||||||
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman bends forward and kneels very low between the viewer's open thighs with her shoulders between his knees; "
|
"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 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"
|
"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:
|
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(
|
return outercourse_sentence(
|
||||||
"The woman bends forward between the viewer's open thighs with her head low under the viewer's penis; "
|
"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, "
|
"her face is just under the penis while her tongue touches the underside from the base toward the glans at the tip, "
|
||||||
@@ -278,14 +366,17 @@ def pov_hardcore_pose_sentence(
|
|||||||
)
|
)
|
||||||
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
if action_kind == outercourse_policy.OUTERCOURSE_HANDJOB:
|
||||||
return outercourse_sentence(
|
return outercourse_sentence(
|
||||||
"The woman kneels between the viewer's open thighs with her torso leaning forward and face visible behind the viewer's penis; "
|
"POV handjob position: the viewer reclines with thighs open while the woman kneels between his legs facing him, "
|
||||||
"one hand grips and strokes the viewer's penis in the lower foreground while the other hand steadies its base, "
|
"torso leaning forward and face visible behind the penis; "
|
||||||
"thumb and fingers visible around the penis as she strokes toward the glans"
|
"the woman's right hand wraps around the viewer's penis in the lower foreground and strokes along the shaft toward the glans, "
|
||||||
|
"while her left hand steadies the base with her fingers and nails visibly gripping the penis; "
|
||||||
|
"the viewer's thighs and pelvis frame the lower edges without his hands covering the action"
|
||||||
)
|
)
|
||||||
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
if action_kind == outercourse_policy.OUTERCOURSE_FOOTJOB:
|
||||||
return outercourse_sentence(
|
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; "
|
"Frontal POV footjob close-up: the woman faces the viewer with hips back, torso behind raised legs, and 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"
|
"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(
|
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"
|
"The woman stays close to the viewer's pelvis, keeping the non-penetrative contact centered in the lower foreground with her face visible behind the contact"
|
||||||
@@ -298,11 +389,29 @@ def pov_hardcore_pose_sentence(
|
|||||||
"POV sixty-nine oral position: the woman lies head-to-hips over the viewer, her pelvis close to his face and her head lowered toward his hips; "
|
"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"
|
"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 "side-lying oral" in position_context or "side lying oral" in position_context:
|
||||||
if woman_gives and not man_gives:
|
if woman_gives and not man_gives:
|
||||||
return oral_sentence(
|
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; "
|
"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; "
|
||||||
"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"
|
"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(
|
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; "
|
"POV side-lying cunnilingus position: the woman lies on her side with her top thigh lifted while the viewer lies beside her hips; "
|
||||||
@@ -375,8 +484,10 @@ def pov_hardcore_pose_sentence(
|
|||||||
"his face is at pussy height, with her knees, hips, and torso readable from the first-person angle"
|
"his face is at pussy height, with her knees, hips, and torso readable from the first-person angle"
|
||||||
)
|
)
|
||||||
return oral_sentence(
|
return oral_sentence(
|
||||||
"POV kneeling oral position: the viewer stands over her with hips forward while the woman kneels directly in front of him; "
|
"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; "
|
||||||
"her head is at penis height, mouth on the viewer's penis, shoulders below his hips and his thighs framing the lower foreground"
|
"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:
|
if man_gives and not woman_gives:
|
||||||
return oral_sentence(
|
return oral_sentence(
|
||||||
@@ -395,16 +506,46 @@ def pov_hardcore_pose_sentence(
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
contact = pov_contact_clause(action, role_graph, hard_item, axis_values, context)
|
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:
|
if "reverse cowgirl" in position_context:
|
||||||
return sentence(
|
return sentence(
|
||||||
"POV reverse cowgirl position: the viewer lies on his back while the woman straddles his hips facing away; "
|
"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:
|
if "cowgirl" in position_context or "straddling a partner" in position_context or "squatting on top" in position_context:
|
||||||
return sentence(
|
return sentence(
|
||||||
"POV cowgirl position: the viewer lies on his back while the woman straddles his hips facing him; "
|
"POV frontal cowgirl wide-thigh bridge position: the viewer reclines underneath her with lower abdomen and pelvis anchoring the bottom edge; "
|
||||||
f"her torso, hips, and open thighs fill the frame from below {contact}"
|
"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:
|
if "lotus" in position_context or "seated in a partner's lap" in position_context:
|
||||||
return sentence(
|
return sentence(
|
||||||
@@ -428,10 +569,16 @@ def pov_hardcore_pose_sentence(
|
|||||||
or "bed edge" in position_context
|
or "bed edge" in position_context
|
||||||
or (not position_text and "kneels between her legs" in context)
|
or (not position_text and "kneels between her legs" in context)
|
||||||
):
|
):
|
||||||
|
if "penetrates her ass" in contact:
|
||||||
return sentence(
|
return sentence(
|
||||||
"POV raised-edge penetration position: the woman reclines at the raised edge with thighs open toward the viewer; "
|
"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}"
|
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:
|
if "standing" in position_context:
|
||||||
return sentence(
|
return sentence(
|
||||||
"POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; "
|
"POV standing rear-entry position: the woman stands braced in front of the viewer with hips angled back and legs steady; "
|
||||||
@@ -444,8 +591,9 @@ def pov_hardcore_pose_sentence(
|
|||||||
)
|
)
|
||||||
if "doggy" in position_context or "all fours" in position_context or "rear-entry" in position_context:
|
if "doggy" in position_context or "all fours" in position_context or "rear-entry" in position_context:
|
||||||
return sentence(
|
return sentence(
|
||||||
"The woman is seen from behind with her ass raised toward the POV viewer, on all fours directly in front of him with hips high and back arched; "
|
"Top-down POV doggy position from behind: the camera looks down over the viewer's hands onto the woman's raised hips; "
|
||||||
f"the viewer looks down at her raised ass with his hands on her hips in the foreground {contact}"
|
f"the woman is on all fours with chest low, forearms folded, cheek turned sideways far ahead, back arched, and hips raised high toward the camera; "
|
||||||
|
f"the viewer's hands hold her hips with natural lower-body POV cues in the foreground {contact}"
|
||||||
)
|
)
|
||||||
if "kneeling" in position_context:
|
if "kneeling" in position_context:
|
||||||
return sentence(
|
return sentence(
|
||||||
|
|||||||
@@ -3,20 +3,30 @@ from __future__ import annotations
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
from . import krea2_eval_log
|
||||||
|
from . import krea2_pose_variant_catalog
|
||||||
from .hardcore_position_config import (
|
from .hardcore_position_config import (
|
||||||
build_hardcore_action_filter_json,
|
build_hardcore_action_filter_json,
|
||||||
build_hardcore_position_pool_json,
|
build_hardcore_position_pool_json,
|
||||||
|
empty_hardcore_position_config,
|
||||||
hardcore_position_family_choices,
|
hardcore_position_family_choices,
|
||||||
hardcore_position_focus_choices,
|
hardcore_position_focus_choices,
|
||||||
hardcore_position_key_choices,
|
hardcore_position_key_choices,
|
||||||
|
hardcore_position_summary,
|
||||||
|
parse_hardcore_position_config,
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests from the repository root.
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
|
import krea2_eval_log
|
||||||
|
import krea2_pose_variant_catalog
|
||||||
from hardcore_position_config import (
|
from hardcore_position_config import (
|
||||||
build_hardcore_action_filter_json,
|
build_hardcore_action_filter_json,
|
||||||
build_hardcore_position_pool_json,
|
build_hardcore_position_pool_json,
|
||||||
|
empty_hardcore_position_config,
|
||||||
hardcore_position_family_choices,
|
hardcore_position_family_choices,
|
||||||
hardcore_position_focus_choices,
|
hardcore_position_focus_choices,
|
||||||
hardcore_position_key_choices,
|
hardcore_position_key_choices,
|
||||||
|
hardcore_position_summary,
|
||||||
|
parse_hardcore_position_config,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -30,6 +40,115 @@ def _choice_input_key(prefix, choice):
|
|||||||
return f"{prefix}_{key}"
|
return f"{prefix}_{key}"
|
||||||
|
|
||||||
|
|
||||||
|
def _variant_input_key(variant_key):
|
||||||
|
return _choice_input_key("include", str(variant_key or "").removeprefix("pov_"))
|
||||||
|
|
||||||
|
|
||||||
|
def _unique_extend(values):
|
||||||
|
selected = []
|
||||||
|
for value in values:
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text and text not in selected:
|
||||||
|
selected.append(text)
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def _variant_family(value):
|
||||||
|
family = str(value or "any")
|
||||||
|
if family == "penetration":
|
||||||
|
family = "penetrative"
|
||||||
|
return family if family in hardcore_position_family_choices() else "any"
|
||||||
|
|
||||||
|
|
||||||
|
def _variant_positions(variant):
|
||||||
|
valid = set(hardcore_position_key_choices())
|
||||||
|
return [str(key) for key in variant.get("position_keys", []) if str(key) in valid]
|
||||||
|
|
||||||
|
|
||||||
|
def _variants_for_action_family(action_family):
|
||||||
|
return krea2_pose_variant_catalog.variants(action_family=action_family)
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_variant_rows(action_family, kwargs):
|
||||||
|
return [
|
||||||
|
variant
|
||||||
|
for variant in _variants_for_action_family(action_family)
|
||||||
|
if bool(kwargs.get(_variant_input_key(variant.get("key")), False))
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _join_variant_cues(variants, cue_key):
|
||||||
|
cues = []
|
||||||
|
for variant in variants:
|
||||||
|
cues.extend(str(cue) for cue in variant.get(cue_key, []) if str(cue).strip())
|
||||||
|
return "; ".join(_unique_extend(cues))
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_variant_positions(variants):
|
||||||
|
positions = []
|
||||||
|
for variant in variants:
|
||||||
|
positions.extend(_variant_positions(variant))
|
||||||
|
return _unique_extend(positions)
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_variant_keys(variants):
|
||||||
|
return [str(variant.get("key")) for variant in variants if variant.get("key")]
|
||||||
|
|
||||||
|
|
||||||
|
def _merged_family_for_variant_filter(incoming_config, combine_mode, family):
|
||||||
|
family = _variant_family(family)
|
||||||
|
if combine_mode != "add":
|
||||||
|
return family
|
||||||
|
incoming = parse_hardcore_position_config(incoming_config)
|
||||||
|
incoming_family = _variant_family(incoming.get("family"))
|
||||||
|
incoming_positions = incoming.get("positions") or []
|
||||||
|
if not incoming.get("enabled") or (not incoming_positions and incoming_family == "any"):
|
||||||
|
return family
|
||||||
|
if incoming_family == family:
|
||||||
|
return family
|
||||||
|
return "any"
|
||||||
|
|
||||||
|
|
||||||
|
def _empty_or_incoming_config(incoming_config, combine_mode):
|
||||||
|
if combine_mode == "add" and incoming_config:
|
||||||
|
config = parse_hardcore_position_config(incoming_config)
|
||||||
|
else:
|
||||||
|
config = empty_hardcore_position_config()
|
||||||
|
config["summary"] = hardcore_position_summary(config)
|
||||||
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_variant_metadata(config_json, variants):
|
||||||
|
config = json.loads(config_json)
|
||||||
|
selected_keys = _selected_variant_keys(variants)
|
||||||
|
existing_keys = config.get("krea2_variant_keys") or []
|
||||||
|
if not isinstance(existing_keys, list):
|
||||||
|
existing_keys = [existing_keys]
|
||||||
|
variant_keys = _unique_extend([*existing_keys, *selected_keys])
|
||||||
|
config["krea2_variant_keys"] = variant_keys
|
||||||
|
|
||||||
|
selected_statuses = {str(variant.get("key")): str(variant.get("status") or "") for variant in variants if variant.get("key")}
|
||||||
|
existing_statuses = config.get("krea2_variant_statuses") if isinstance(config.get("krea2_variant_statuses"), dict) else {}
|
||||||
|
config["krea2_variant_statuses"] = {**existing_statuses, **selected_statuses}
|
||||||
|
|
||||||
|
prompt_cues = _unique_extend(
|
||||||
|
[*(config.get("krea2_prompt_cues") or []), *(_join_variant_cues(variants, "prompt_cues").split("; ") if variants else [])]
|
||||||
|
)
|
||||||
|
avoid_cues = _unique_extend(
|
||||||
|
[*(config.get("krea2_avoid_cues") or []), *(_join_variant_cues(variants, "avoid_cues").split("; ") if variants else [])]
|
||||||
|
)
|
||||||
|
if prompt_cues:
|
||||||
|
config["krea2_prompt_cues"] = prompt_cues
|
||||||
|
if avoid_cues:
|
||||||
|
config["krea2_avoid_cues"] = avoid_cues
|
||||||
|
|
||||||
|
base_summary = str(config.get("summary") or hardcore_position_summary(config))
|
||||||
|
if variant_keys and "variants=" not in base_summary:
|
||||||
|
base_summary = f"{base_summary}; variants={','.join(variant_keys)}"
|
||||||
|
config["summary"] = base_summary
|
||||||
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
class SxCPHardcorePositionPool:
|
class SxCPHardcorePositionPool:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -66,6 +185,217 @@ class SxCPHardcorePositionPool:
|
|||||||
return config, json.loads(config).get("summary", "")
|
return config, json.loads(config).get("summary", "")
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2PoseVariant:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
keys = krea2_pose_variant_catalog.variant_keys()
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"variant_key": (keys or ["missing_catalog_variant"], {"default": keys[0] if keys else "missing_catalog_variant"}),
|
||||||
|
"combine_mode": (["replace", "add"], {"default": "replace"}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = (
|
||||||
|
"hardcore_position_config",
|
||||||
|
"variant_key",
|
||||||
|
"prompt_cues",
|
||||||
|
"avoid_cues",
|
||||||
|
"summary",
|
||||||
|
"variant_json",
|
||||||
|
)
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(self, variant_key, combine_mode="replace", hardcore_position_config=""):
|
||||||
|
variant = krea2_pose_variant_catalog.get_variant(variant_key)
|
||||||
|
if not variant:
|
||||||
|
empty = {
|
||||||
|
"key": str(variant_key or ""),
|
||||||
|
"status": "missing",
|
||||||
|
"summary": "missing Krea2 pose variant",
|
||||||
|
}
|
||||||
|
return hardcore_position_config or "", str(variant_key or ""), "", "", empty["summary"], json.dumps(empty, sort_keys=True)
|
||||||
|
|
||||||
|
positions = _variant_positions(variant)
|
||||||
|
family = _variant_family(variant.get("action_family") or variant.get("family"))
|
||||||
|
config = build_hardcore_position_pool_json(
|
||||||
|
hardcore_position_config=hardcore_position_config or "",
|
||||||
|
combine_mode=combine_mode,
|
||||||
|
family=family,
|
||||||
|
selected_positions=positions,
|
||||||
|
)
|
||||||
|
prompt_cues = "; ".join(str(cue) for cue in variant.get("prompt_cues", []) if str(cue).strip())
|
||||||
|
avoid_cues = "; ".join(str(cue) for cue in variant.get("avoid_cues", []) if str(cue).strip())
|
||||||
|
summary = (
|
||||||
|
f"variant={variant.get('key')}; status={variant.get('status')}; "
|
||||||
|
f"family={family}; positions={','.join(positions) or 'none'}"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
config,
|
||||||
|
str(variant.get("key") or variant_key),
|
||||||
|
prompt_cues,
|
||||||
|
avoid_cues,
|
||||||
|
summary,
|
||||||
|
json.dumps(variant, ensure_ascii=True, sort_keys=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class _SxCPKrea2POVVariantFilter:
|
||||||
|
ACTION_FAMILY = ""
|
||||||
|
POSITION_FAMILY = "any"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
required = {
|
||||||
|
"combine_mode": (["replace", "add"], {"default": "replace"}),
|
||||||
|
}
|
||||||
|
for variant in _variants_for_action_family(cls.ACTION_FAMILY):
|
||||||
|
required[_variant_input_key(variant.get("key"))] = ("BOOLEAN", {"default": False})
|
||||||
|
return {
|
||||||
|
"required": required,
|
||||||
|
"optional": {
|
||||||
|
"hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = (SXCP_HARDCORE_POSITION_CONFIG, "STRING", "STRING", "STRING", "STRING", "STRING")
|
||||||
|
RETURN_NAMES = (
|
||||||
|
"hardcore_position_config",
|
||||||
|
"selected_variant_keys",
|
||||||
|
"selected_positions",
|
||||||
|
"prompt_cues",
|
||||||
|
"summary",
|
||||||
|
"variants_json",
|
||||||
|
)
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(self, combine_mode="replace", hardcore_position_config="", **kwargs):
|
||||||
|
variants = _selected_variant_rows(self.ACTION_FAMILY, kwargs)
|
||||||
|
if not variants:
|
||||||
|
config = _empty_or_incoming_config(hardcore_position_config or "", combine_mode)
|
||||||
|
return config, "", "", "", json.loads(config).get("summary", ""), "[]"
|
||||||
|
|
||||||
|
positions = _selected_variant_positions(variants)
|
||||||
|
family = _merged_family_for_variant_filter(
|
||||||
|
hardcore_position_config or "",
|
||||||
|
combine_mode,
|
||||||
|
self.POSITION_FAMILY or self.ACTION_FAMILY,
|
||||||
|
)
|
||||||
|
config = build_hardcore_position_pool_json(
|
||||||
|
hardcore_position_config=hardcore_position_config or "",
|
||||||
|
combine_mode=combine_mode,
|
||||||
|
family=family,
|
||||||
|
selected_positions=positions,
|
||||||
|
)
|
||||||
|
config = _merge_variant_metadata(config, variants)
|
||||||
|
parsed = json.loads(config)
|
||||||
|
selected_keys = parsed.get("krea2_variant_keys") or []
|
||||||
|
selected_positions = parsed.get("positions") or []
|
||||||
|
prompt_cues = _join_variant_cues(variants, "prompt_cues")
|
||||||
|
return (
|
||||||
|
config,
|
||||||
|
",".join(str(key) for key in selected_keys),
|
||||||
|
",".join(str(position) for position in selected_positions),
|
||||||
|
prompt_cues,
|
||||||
|
str(parsed.get("summary") or ""),
|
||||||
|
json.dumps(variants, ensure_ascii=True, sort_keys=True),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVPenetrationFilter(_SxCPKrea2POVVariantFilter):
|
||||||
|
ACTION_FAMILY = "penetration"
|
||||||
|
POSITION_FAMILY = "penetration"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVOralFilter(_SxCPKrea2POVVariantFilter):
|
||||||
|
ACTION_FAMILY = "oral"
|
||||||
|
POSITION_FAMILY = "oral"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVOutercourseFilter(_SxCPKrea2POVVariantFilter):
|
||||||
|
ACTION_FAMILY = "outercourse"
|
||||||
|
POSITION_FAMILY = "outercourse"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVManualFilter(_SxCPKrea2POVVariantFilter):
|
||||||
|
ACTION_FAMILY = "manual"
|
||||||
|
POSITION_FAMILY = "manual"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVToyFilter(_SxCPKrea2POVVariantFilter):
|
||||||
|
ACTION_FAMILY = "toy"
|
||||||
|
POSITION_FAMILY = "any"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVClimaxFilter(_SxCPKrea2POVVariantFilter):
|
||||||
|
ACTION_FAMILY = "climax"
|
||||||
|
POSITION_FAMILY = "climax"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2POVInteractionFilter(_SxCPKrea2POVVariantFilter):
|
||||||
|
ACTION_FAMILY = "interaction"
|
||||||
|
POSITION_FAMILY = "interaction"
|
||||||
|
|
||||||
|
|
||||||
|
class SxCPKrea2VariantEvidence:
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
keys = krea2_pose_variant_catalog.variant_keys()
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"variant_key": (keys or ["missing_catalog_variant"], {"default": keys[0] if keys else "missing_catalog_variant"}),
|
||||||
|
"result": (["accepted", "rejected", "inconclusive", "any"], {"default": "accepted"}),
|
||||||
|
},
|
||||||
|
"optional": {
|
||||||
|
"variant_key_in": ("STRING", {"default": ""}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING", "INT", "STRING")
|
||||||
|
RETURN_NAMES = (
|
||||||
|
"summary",
|
||||||
|
"baseline_image_path",
|
||||||
|
"candidate_image_path",
|
||||||
|
"evidence_json",
|
||||||
|
"seed",
|
||||||
|
"decision",
|
||||||
|
)
|
||||||
|
FUNCTION = "build"
|
||||||
|
CATEGORY = "prompt_builder"
|
||||||
|
|
||||||
|
def build(self, variant_key, result="accepted", variant_key_in=""):
|
||||||
|
key = str(variant_key_in or variant_key or "").strip()
|
||||||
|
result_filter = None if result == "any" else result
|
||||||
|
entries = krea2_eval_log.entries_for_variant(key, result=result_filter)
|
||||||
|
if not entries:
|
||||||
|
empty = {
|
||||||
|
"variant_key": key,
|
||||||
|
"result": result,
|
||||||
|
"summary": "no Krea2 eval evidence found",
|
||||||
|
}
|
||||||
|
return empty["summary"], "", "", json.dumps(empty, ensure_ascii=True, sort_keys=True), -1, ""
|
||||||
|
entry = entries[0]
|
||||||
|
summary = (
|
||||||
|
f"evidence={entry.get('id')}; variant={entry.get('variant_key')}; "
|
||||||
|
f"seed={entry.get('seed')}; result={entry.get('result')}; decision={entry.get('decision')}"
|
||||||
|
)
|
||||||
|
seed = entry.get("seed")
|
||||||
|
return (
|
||||||
|
summary,
|
||||||
|
str(entry.get("baseline_image") or ""),
|
||||||
|
str(entry.get("candidate_image") or ""),
|
||||||
|
json.dumps(entry, ensure_ascii=True, sort_keys=True),
|
||||||
|
int(seed) if isinstance(seed, int) else -1,
|
||||||
|
str(entry.get("decision") or ""),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class SxCPHardcoreActionFilter:
|
class SxCPHardcoreActionFilter:
|
||||||
@classmethod
|
@classmethod
|
||||||
def INPUT_TYPES(cls):
|
def INPUT_TYPES(cls):
|
||||||
@@ -128,9 +458,27 @@ class SxCPHardcoreActionFilter:
|
|||||||
NODE_CLASS_MAPPINGS = {
|
NODE_CLASS_MAPPINGS = {
|
||||||
"SxCPHardcorePositionPool": SxCPHardcorePositionPool,
|
"SxCPHardcorePositionPool": SxCPHardcorePositionPool,
|
||||||
"SxCPHardcoreActionFilter": SxCPHardcoreActionFilter,
|
"SxCPHardcoreActionFilter": SxCPHardcoreActionFilter,
|
||||||
|
"SxCPKrea2PoseVariant": SxCPKrea2PoseVariant,
|
||||||
|
"SxCPKrea2POVPenetrationFilter": SxCPKrea2POVPenetrationFilter,
|
||||||
|
"SxCPKrea2POVOralFilter": SxCPKrea2POVOralFilter,
|
||||||
|
"SxCPKrea2POVOutercourseFilter": SxCPKrea2POVOutercourseFilter,
|
||||||
|
"SxCPKrea2POVManualFilter": SxCPKrea2POVManualFilter,
|
||||||
|
"SxCPKrea2POVToyFilter": SxCPKrea2POVToyFilter,
|
||||||
|
"SxCPKrea2POVClimaxFilter": SxCPKrea2POVClimaxFilter,
|
||||||
|
"SxCPKrea2POVInteractionFilter": SxCPKrea2POVInteractionFilter,
|
||||||
|
"SxCPKrea2VariantEvidence": SxCPKrea2VariantEvidence,
|
||||||
}
|
}
|
||||||
|
|
||||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||||
"SxCPHardcorePositionPool": "SxCP Hardcore Position Pool",
|
"SxCPHardcorePositionPool": "SxCP Hardcore Position Pool",
|
||||||
"SxCPHardcoreActionFilter": "SxCP Hardcore Action Filter",
|
"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",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -322,6 +322,16 @@ NODE_INPUT_TOOLTIPS = {
|
|||||||
"combine_mode": "replace discards incoming position choices; add merges these choices with the incoming config.",
|
"combine_mode": "replace discards incoming position choices; add merges these choices with the incoming config.",
|
||||||
"hardcore_position_config": "Optional incoming config. Usually connect previous Position Pool here only when chaining pools.",
|
"hardcore_position_config": "Optional incoming config. Usually connect previous Position Pool here only when chaining pools.",
|
||||||
},
|
},
|
||||||
|
"SxCPKrea2PoseVariant": {
|
||||||
|
"variant_key": "Atlas-calibrated Krea2 POV pose variant. Proven variants have fixed-seed evidence in the eval log.",
|
||||||
|
"combine_mode": "replace discards incoming position choices; add merges this variant with the incoming position config.",
|
||||||
|
"hardcore_position_config": "Optional incoming hardcore position config. Connect this when layering a variant on an existing pool.",
|
||||||
|
},
|
||||||
|
"SxCPKrea2VariantEvidence": {
|
||||||
|
"variant_key": "Catalog variant whose fixed-seed eval evidence should be shown.",
|
||||||
|
"result": "Filter eval entries by result. accepted is the evidence used for proven variants.",
|
||||||
|
"variant_key_in": "Optional connected variant key from SxCP Krea2 Pose Variant. When connected, it overrides the selector.",
|
||||||
|
},
|
||||||
"SxCPHardcoreActionFilter": {
|
"SxCPHardcoreActionFilter": {
|
||||||
"focus": "keep_pool preserves/broadens the incoming pool; *_only modes force one action family.",
|
"focus": "keep_pool preserves/broadens the incoming pool; *_only modes force one action family.",
|
||||||
"allow_toys": "Allow toy/strap-on wording in hardcore actions.",
|
"allow_toys": "Allow toy/strap-on wording in hardcore actions.",
|
||||||
|
|||||||
@@ -247,6 +247,8 @@ def build_insta_of_pair(request: InstaPairBuildRequest, deps: InstaPairBuildDepe
|
|||||||
rng=hard_content_rng,
|
rng=hard_content_rng,
|
||||||
continuity_map=deps.hardcore_clothing_continuity,
|
continuity_map=deps.hardcore_clothing_continuity,
|
||||||
choose=deps.choose,
|
choose=deps.choose,
|
||||||
|
label_map=character_slot_map,
|
||||||
|
slot_hardcore_clothing=deps.slot_hardcore_clothing,
|
||||||
)
|
)
|
||||||
if clothing_route.requires_body_exposure_scene:
|
if clothing_route.requires_body_exposure_scene:
|
||||||
hard_scene = pair_clothing.body_exposure_scene_text(hard_scene)
|
hard_scene = pair_clothing.body_exposure_scene_text(hard_scene)
|
||||||
@@ -295,6 +297,7 @@ def build_insta_of_pair(request: InstaPairBuildRequest, deps: InstaPairBuildDepe
|
|||||||
camera_caption_text=deps.camera_caption_text,
|
camera_caption_text=deps.camera_caption_text,
|
||||||
cast_descriptors=cast_context["cast_descriptors"],
|
cast_descriptors=cast_context["cast_descriptors"],
|
||||||
character_hardcore_clothing_entries=character_hardcore_clothing_entries,
|
character_hardcore_clothing_entries=character_hardcore_clothing_entries,
|
||||||
|
pov_hardcore_clothing_entries=clothing_route.pov_hardcore_clothing,
|
||||||
default_man_hardcore_clothing_entries=clothing_route.default_man_hardcore_clothing,
|
default_man_hardcore_clothing_entries=clothing_route.default_man_hardcore_clothing,
|
||||||
hard_clothing_state=clothing_route.hardcore_clothing_state,
|
hard_clothing_state=clothing_route.hardcore_clothing_state,
|
||||||
hard_detail_density=hard_detail_density,
|
hard_detail_density=hard_detail_density,
|
||||||
|
|||||||
@@ -437,10 +437,51 @@ def default_man_hardcore_clothing_entries(
|
|||||||
return entries
|
return entries
|
||||||
|
|
||||||
|
|
||||||
|
def _pov_clothing_sentence(clothing: str, needs_lower_access: bool) -> str:
|
||||||
|
clothing = _clean_pair_punctuation(str(clothing or "").strip().rstrip("."))
|
||||||
|
if not clothing:
|
||||||
|
return ""
|
||||||
|
lower = clothing.lower()
|
||||||
|
if lower.startswith(("fully nude", "nude")):
|
||||||
|
if needs_lower_access:
|
||||||
|
return "POV foreground body cue: the viewer's bare hips, thighs, hands, and penis appear as first-person body cues"
|
||||||
|
return "POV foreground body cue: the viewer's bare hands, forearms, or torso edge appear as first-person body cues"
|
||||||
|
clothing = re.sub(r"^(?:wears|wearing|keeps|has|with)\s+", "", clothing, flags=re.IGNORECASE).strip()
|
||||||
|
if needs_lower_access:
|
||||||
|
return (
|
||||||
|
f"POV foreground clothing cue: {clothing}, appearing as the viewer's hands, hips, thighs, or lowered waistband"
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"POV foreground clothing cue: {clothing}, appearing as the viewer's hands, forearms, sleeves, or torso edge"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pov_hardcore_clothing_entries(
|
||||||
|
label_map: dict[str, dict[str, Any]],
|
||||||
|
pov_labels: list[str] | None,
|
||||||
|
rng: Any,
|
||||||
|
needs_lower_access: bool,
|
||||||
|
choose: Callable[[Any, list[str]], str],
|
||||||
|
slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str] | None = None,
|
||||||
|
) -> list[str]:
|
||||||
|
entries: list[str] = []
|
||||||
|
pool = INSTA_OF_HARDCORE_MEN_CLOTHING_LOWER_ACCESS if needs_lower_access else INSTA_OF_HARDCORE_MEN_CLOTHING_VISIBLE
|
||||||
|
for label in pov_labels or []:
|
||||||
|
slot = label_map.get(label)
|
||||||
|
clothing = slot_hardcore_clothing(slot, rng) if slot_hardcore_clothing is not None else ""
|
||||||
|
if not clothing:
|
||||||
|
clothing = choose(rng, pool)
|
||||||
|
sentence = _pov_clothing_sentence(clothing, needs_lower_access)
|
||||||
|
if sentence:
|
||||||
|
entries.append(sentence)
|
||||||
|
return entries
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class HardcorePairClothingRoute:
|
class HardcorePairClothingRoute:
|
||||||
access_flags: dict[str, bool]
|
access_flags: dict[str, bool]
|
||||||
woman_access: str
|
woman_access: str
|
||||||
|
pov_hardcore_clothing: list[str]
|
||||||
default_man_hardcore_clothing: list[str]
|
default_man_hardcore_clothing: list[str]
|
||||||
hardcore_clothing_state: str
|
hardcore_clothing_state: str
|
||||||
hardcore_clothing_sentence: str
|
hardcore_clothing_sentence: str
|
||||||
@@ -450,6 +491,7 @@ class HardcorePairClothingRoute:
|
|||||||
return {
|
return {
|
||||||
"access_flags": dict(self.access_flags),
|
"access_flags": dict(self.access_flags),
|
||||||
"woman_access": self.woman_access,
|
"woman_access": self.woman_access,
|
||||||
|
"pov_hardcore_clothing": list(self.pov_hardcore_clothing),
|
||||||
"default_man_hardcore_clothing": list(self.default_man_hardcore_clothing),
|
"default_man_hardcore_clothing": list(self.default_man_hardcore_clothing),
|
||||||
"hardcore_clothing_state": self.hardcore_clothing_state,
|
"hardcore_clothing_state": self.hardcore_clothing_state,
|
||||||
"hardcore_clothing_sentence": self.hardcore_clothing_sentence,
|
"hardcore_clothing_sentence": self.hardcore_clothing_sentence,
|
||||||
@@ -468,9 +510,19 @@ def resolve_hardcore_pair_clothing_result(
|
|||||||
rng: Any,
|
rng: Any,
|
||||||
continuity_map: dict[str, str],
|
continuity_map: dict[str, str],
|
||||||
choose: Callable[[Any, list[str]], str],
|
choose: Callable[[Any, list[str]], str],
|
||||||
|
label_map: dict[str, dict[str, Any]] | None = None,
|
||||||
|
slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str] | None = None,
|
||||||
) -> HardcorePairClothingRoute:
|
) -> HardcorePairClothingRoute:
|
||||||
access_flags = hardcore_row_access_flags(hard_row)
|
access_flags = hardcore_row_access_flags(hard_row)
|
||||||
woman_access = "lower" if access_flags["woman_lower"] else "upper" if access_flags["woman_upper"] else ""
|
woman_access = "lower" if access_flags["woman_lower"] else "upper" if access_flags["woman_upper"] else ""
|
||||||
|
pov_entries = pov_hardcore_clothing_entries(
|
||||||
|
label_map or {},
|
||||||
|
pov_labels,
|
||||||
|
rng,
|
||||||
|
access_flags["man_lower"],
|
||||||
|
choose,
|
||||||
|
slot_hardcore_clothing,
|
||||||
|
)
|
||||||
default_man_entries = default_man_hardcore_clothing_entries(
|
default_man_entries = default_man_hardcore_clothing_entries(
|
||||||
men_count,
|
men_count,
|
||||||
pov_labels,
|
pov_labels,
|
||||||
@@ -491,6 +543,7 @@ def resolve_hardcore_pair_clothing_result(
|
|||||||
for part in (
|
for part in (
|
||||||
fallback_state,
|
fallback_state,
|
||||||
*character_hardcore_clothing_entries,
|
*character_hardcore_clothing_entries,
|
||||||
|
*pov_entries,
|
||||||
*default_man_entries,
|
*default_man_entries,
|
||||||
)
|
)
|
||||||
if str(part or "").strip()
|
if str(part or "").strip()
|
||||||
@@ -510,6 +563,7 @@ def resolve_hardcore_pair_clothing_result(
|
|||||||
return HardcorePairClothingRoute(
|
return HardcorePairClothingRoute(
|
||||||
access_flags=access_flags,
|
access_flags=access_flags,
|
||||||
woman_access=woman_access,
|
woman_access=woman_access,
|
||||||
|
pov_hardcore_clothing=pov_entries,
|
||||||
default_man_hardcore_clothing=default_man_entries,
|
default_man_hardcore_clothing=default_man_entries,
|
||||||
hardcore_clothing_state=hard_clothing_state,
|
hardcore_clothing_state=hard_clothing_state,
|
||||||
hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "",
|
hardcore_clothing_sentence=f"{hard_clothing_state}. " if hard_clothing_state else "",
|
||||||
@@ -531,6 +585,8 @@ def resolve_hardcore_pair_clothing(
|
|||||||
rng: Any,
|
rng: Any,
|
||||||
continuity_map: dict[str, str],
|
continuity_map: dict[str, str],
|
||||||
choose: Callable[[Any, list[str]], str],
|
choose: Callable[[Any, list[str]], str],
|
||||||
|
label_map: dict[str, dict[str, Any]] | None = None,
|
||||||
|
slot_hardcore_clothing: Callable[[dict[str, Any] | None, Any], str] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
return resolve_hardcore_pair_clothing_result(
|
return resolve_hardcore_pair_clothing_result(
|
||||||
hard_row=hard_row,
|
hard_row=hard_row,
|
||||||
@@ -542,4 +598,6 @@ def resolve_hardcore_pair_clothing(
|
|||||||
rng=rng,
|
rng=rng,
|
||||||
continuity_map=continuity_map,
|
continuity_map=continuity_map,
|
||||||
choose=choose,
|
choose=choose,
|
||||||
|
label_map=label_map,
|
||||||
|
slot_hardcore_clothing=slot_hardcore_clothing,
|
||||||
).as_dict()
|
).as_dict()
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ def assemble_insta_pair_metadata(
|
|||||||
camera_caption_text: Callable[[dict[str, Any]], str],
|
camera_caption_text: Callable[[dict[str, Any]], str],
|
||||||
cast_descriptors: list[str],
|
cast_descriptors: list[str],
|
||||||
character_hardcore_clothing_entries: list[str],
|
character_hardcore_clothing_entries: list[str],
|
||||||
|
pov_hardcore_clothing_entries: list[str],
|
||||||
default_man_hardcore_clothing_entries: list[str],
|
default_man_hardcore_clothing_entries: list[str],
|
||||||
hard_clothing_state: str,
|
hard_clothing_state: str,
|
||||||
hard_detail_density: str,
|
hard_detail_density: str,
|
||||||
@@ -154,6 +155,7 @@ def assemble_insta_pair_metadata(
|
|||||||
"pov_prompt_directive": pov_directive,
|
"pov_prompt_directive": pov_directive,
|
||||||
"softcore_partner_styling": soft_partner_styling,
|
"softcore_partner_styling": soft_partner_styling,
|
||||||
"character_hardcore_clothing": character_hardcore_clothing_entries,
|
"character_hardcore_clothing": character_hardcore_clothing_entries,
|
||||||
|
"pov_hardcore_clothing": pov_hardcore_clothing_entries,
|
||||||
"default_man_hardcore_clothing": default_man_hardcore_clothing_entries,
|
"default_man_hardcore_clothing": default_man_hardcore_clothing_entries,
|
||||||
"hardcore_clothing_state": hard_clothing_state,
|
"hardcore_clothing_state": hard_clothing_state,
|
||||||
"hardcore_detail_density": hard_detail_density,
|
"hardcore_detail_density": hard_detail_density,
|
||||||
|
|||||||
@@ -923,12 +923,12 @@ def scene_direction_detail(
|
|||||||
if "left side" in direction:
|
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"
|
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:
|
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":
|
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:
|
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} 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:
|
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"
|
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:
|
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)
|
subject, _pronoun = scene_subject_terms(subject_kind, pov_labels)
|
||||||
return (
|
return (
|
||||||
f"{profile['layout_label']} from POV{geometry_clause}: keep {subject} and the action primary; "
|
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."
|
f"POV body or hand cues stay in the lower foreground."
|
||||||
)
|
)
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
from datetime import date
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) in sys.path:
|
||||||
|
sys.path.remove(str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
import krea2_eval_log # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _load_entry(path: Path) -> dict:
|
||||||
|
with path.open("r", encoding="utf-8") as handle:
|
||||||
|
data = json.load(handle)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise ValueError("entry JSON must contain one object")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Validate and append one durable Krea2 fixed-seed eval entry.")
|
||||||
|
parser.add_argument("--entry-json", help="Path to a JSON object containing one eval entry.")
|
||||||
|
parser.add_argument("--print-template", action="store_true", help="Print a valid eval entry template instead of recording.")
|
||||||
|
parser.add_argument("--variant-key", help="Catalog variant key for --print-template.")
|
||||||
|
parser.add_argument("--seed", type=int, help="Fixed seed for --print-template.")
|
||||||
|
parser.add_argument("--generator-seed", type=int, help="Optional SxCP generator/control seed for --print-template.")
|
||||||
|
parser.add_argument("--source", default="sxcp_eval_mcp", help="Source label for --print-template.")
|
||||||
|
parser.add_argument("--date", default=date.today().isoformat(), help="Date for --print-template.")
|
||||||
|
parser.add_argument("--log-path", default=str(krea2_eval_log.DEFAULT_EVAL_LOG_PATH), help="Eval log path to update.")
|
||||||
|
parser.add_argument("--dry-run", action="store_true", help="Validate without writing the log.")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.print_template:
|
||||||
|
if not args.variant_key or args.seed is None:
|
||||||
|
raise ValueError("--print-template requires --variant-key and --seed")
|
||||||
|
entry = krea2_eval_log.entry_template(
|
||||||
|
args.variant_key,
|
||||||
|
seed=args.seed,
|
||||||
|
generator_seed=args.generator_seed,
|
||||||
|
source=args.source,
|
||||||
|
date=args.date,
|
||||||
|
)
|
||||||
|
print(json.dumps(entry, ensure_ascii=True, indent=2))
|
||||||
|
return 0
|
||||||
|
if not args.entry_json:
|
||||||
|
raise ValueError("--entry-json is required unless --print-template is used")
|
||||||
|
entry = _load_entry(Path(args.entry_json))
|
||||||
|
log = krea2_eval_log.append_entry(entry, path=args.log_path, dry_run=args.dry_run)
|
||||||
|
except Exception as exc:
|
||||||
|
print(f"error: {exc}", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
action = "validated" if args.dry_run else "recorded"
|
||||||
|
print(f"{action}: {entry.get('id')} ({len(log.get('entries') or [])} entries)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) in sys.path:
|
||||||
|
sys.path.remove(str(ROOT))
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
|
import krea2_tuning_report # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
print(krea2_tuning_report.markdown_report())
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main())
|
||||||
@@ -1661,6 +1661,8 @@ def run_simulation(seed: int = 3901, *, include_prompts: bool = False) -> dict[s
|
|||||||
cases.extend(_pair_reports("insta_pair.penetration", penetration_pair, include_prompts=include_prompts))
|
cases.extend(_pair_reports("insta_pair.penetration", penetration_pair, include_prompts=include_prompts))
|
||||||
pov_pair = _insta_pair_case(seed + 2, pov=True, position="penis_licking", focus="outercourse_only", family="outercourse")
|
pov_pair = _insta_pair_case(seed + 2, pov=True, position="penis_licking", focus="outercourse_only", family="outercourse")
|
||||||
cases.extend(_pair_reports("insta_pair.pov_outercourse", pov_pair, include_prompts=include_prompts))
|
cases.extend(_pair_reports("insta_pair.pov_outercourse", pov_pair, include_prompts=include_prompts))
|
||||||
|
ballsucking_pair = _insta_pair_case(seed + 5, pov=True, position="testicle_sucking", focus="outercourse_only", family="outercourse")
|
||||||
|
cases.extend(_pair_reports("insta_pair.pov_ballsucking", ballsucking_pair, include_prompts=include_prompts))
|
||||||
coverage_checks = _route_family_coverage_checks(cases)
|
coverage_checks = _route_family_coverage_checks(cases)
|
||||||
axis_checks = _seed_axis_checks(seed + 3)
|
axis_checks = _seed_axis_checks(seed + 3)
|
||||||
pair_seed_checks = _pair_seed_checks(seed + 4)
|
pair_seed_checks = _pair_seed_checks(seed + 4)
|
||||||
|
|||||||
+2308
-36
File diff suppressed because it is too large
Load Diff
Executable
+232
@@ -0,0 +1,232 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
tools/sxcp_eval_loop.sh [minutes] [options]
|
||||||
|
|
||||||
|
Loop protocol for Krea2 prompt-generator tuning. Start it right after sending a
|
||||||
|
prompt to sxcp_eval_out. Every N minutes it writes a structured evaluation
|
||||||
|
request, prints it, and optionally pipes it to a command. Each cycle should
|
||||||
|
produce either a prompt-only A/B edit, a generator fix, or a prompt-guide rule.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-m, --minutes N Wait N minutes between evaluation requests.
|
||||||
|
-i, --in CHANNEL Graph-to-agent channel. Default: sxcp_eval_in.
|
||||||
|
-o, --out CHANNEL Agent-to-graph prompt-only channel. Default: sxcp_eval_out.
|
||||||
|
-l, --log CHANNEL Analysis/log channel name. Default: sxcp_eval_log.
|
||||||
|
-g, --guide FILE Durable Krea2 prompt guide. Default: docs/krea2-prompt-guide.md.
|
||||||
|
-d, --dir DIR Runtime log directory. Default: .sxcp_eval.
|
||||||
|
--once Run one wait/check cycle and exit.
|
||||||
|
-h, --help Show this help.
|
||||||
|
|
||||||
|
Optional automation:
|
||||||
|
SXCP_EVAL_CODEX_CMD If set, the request is piped to this command.
|
||||||
|
Example: SXCP_EVAL_CODEX_CMD="codex exec"
|
||||||
|
|
||||||
|
The command receives the request on stdin and these environment variables:
|
||||||
|
SXCP_EVAL_IN_CHANNEL, SXCP_EVAL_OUT_CHANNEL, SXCP_EVAL_LOG_CHANNEL,
|
||||||
|
SXCP_EVAL_GUIDE_FILE, SXCP_EVAL_REQUEST_FILE, SXCP_EVAL_CYCLE_DIR,
|
||||||
|
SXCP_EVAL_CYCLE.
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
die() {
|
||||||
|
echo "sxcp_eval_loop: $*" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
is_positive_number() {
|
||||||
|
case "${1:-}" in
|
||||||
|
''|*[!0-9.]*|.*.*|0|0.0|0.00) return 1 ;;
|
||||||
|
*) return 0 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
minutes="${SXCP_EVAL_MINUTES:-}"
|
||||||
|
in_channel="${SXCP_EVAL_IN_CHANNEL:-sxcp_eval_in}"
|
||||||
|
out_channel="${SXCP_EVAL_OUT_CHANNEL:-sxcp_eval_out}"
|
||||||
|
log_channel="${SXCP_EVAL_LOG_CHANNEL:-sxcp_eval_log}"
|
||||||
|
guide_file="${SXCP_EVAL_GUIDE_FILE:-docs/krea2-prompt-guide.md}"
|
||||||
|
log_root="${SXCP_EVAL_LOG_DIR:-.sxcp_eval}"
|
||||||
|
run_once=0
|
||||||
|
|
||||||
|
if [ "${1:-}" != "" ] && [ "${1#-}" = "$1" ]; then
|
||||||
|
minutes="$1"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
while [ "$#" -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
-m|--minutes)
|
||||||
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
||||||
|
minutes="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-i|--in)
|
||||||
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
||||||
|
in_channel="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-o|--out)
|
||||||
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
||||||
|
out_channel="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-l|--log)
|
||||||
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
||||||
|
log_channel="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-g|--guide)
|
||||||
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
||||||
|
guide_file="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-d|--dir)
|
||||||
|
[ "$#" -ge 2 ] || die "$1 requires a value"
|
||||||
|
log_root="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
--once)
|
||||||
|
run_once=1
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
-h|--help)
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
minutes="${minutes:-5}"
|
||||||
|
is_positive_number "$minutes" || die "minutes must be a positive number"
|
||||||
|
|
||||||
|
mkdir -p "$log_root"
|
||||||
|
run_id="$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
|
run_dir="$log_root/$run_id"
|
||||||
|
mkdir -p "$run_dir"
|
||||||
|
events_file="$run_dir/events.tsv"
|
||||||
|
summary_file="$run_dir/summary.md"
|
||||||
|
|
||||||
|
cat > "$summary_file" <<EOF
|
||||||
|
# SxCP Eval Loop $run_id
|
||||||
|
|
||||||
|
- Interval: ${minutes} minute(s)
|
||||||
|
- Input channel: \`$in_channel\`
|
||||||
|
- Prompt output channel: \`$out_channel\`
|
||||||
|
- Log channel: \`$log_channel\`
|
||||||
|
- Krea2 prompt guide: \`$guide_file\`
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Tune the SxCP generator so its default Krea2 prompts produce the strongest
|
||||||
|
possible images for the selected scene, camera, subject, outfit, action, and
|
||||||
|
style. Every cycle should turn visual evidence into one of:
|
||||||
|
|
||||||
|
- a prompt-only A/B edit,
|
||||||
|
- a durable rule for \`$guide_file\`,
|
||||||
|
- a generator code/data change with focused test coverage.
|
||||||
|
|
||||||
|
## Protocol
|
||||||
|
|
||||||
|
1. Pull the latest prompt/image from \`$in_channel\`.
|
||||||
|
2. Record the emitted seed. If it is missing, mark the image as uncontrolled.
|
||||||
|
3. Compare the image against the prompt and previous edited prompt.
|
||||||
|
4. Identify concrete Krea2 mismatches and likely generator path.
|
||||||
|
5. Classify the next step: prompt-only edit, guide rule, or generator patch.
|
||||||
|
6. Push only the next test prompt text to \`$out_channel\`. Preserve the same
|
||||||
|
seed through the MCP signal when available; never write the seed into the
|
||||||
|
prompt text.
|
||||||
|
7. Keep analysis in chat or \`$log_channel\`, not in \`$out_channel\`.
|
||||||
|
8. Edit generator code/data only when the issue is systemic.
|
||||||
|
9. Update \`$guide_file\` when a wording rule is confirmed.
|
||||||
|
10. Run focused smoke tests after generator edits.
|
||||||
|
|
||||||
|
## Cycles
|
||||||
|
|
||||||
|
EOF
|
||||||
|
|
||||||
|
printf 'cycle\tutc_time\trequest_file\tstatus\n' > "$events_file"
|
||||||
|
|
||||||
|
cycle=0
|
||||||
|
while :; do
|
||||||
|
cycle=$((cycle + 1))
|
||||||
|
echo "sxcp_eval_loop: cycle $cycle waiting ${minutes} minute(s) before requesting evaluation..."
|
||||||
|
sleep "${minutes}m"
|
||||||
|
|
||||||
|
stamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||||||
|
cycle_dir="$run_dir/cycle_$(printf '%03d' "$cycle")"
|
||||||
|
mkdir -p "$cycle_dir"
|
||||||
|
request_file="$cycle_dir/request.md"
|
||||||
|
|
||||||
|
cat > "$request_file" <<EOF
|
||||||
|
Please run SxCP eval cycle $cycle now.
|
||||||
|
|
||||||
|
Primary goal:
|
||||||
|
- Tune the generator for better Krea2 images, not just one isolated image.
|
||||||
|
- Maintain/update the durable Krea2 prompt guide at: $guide_file
|
||||||
|
|
||||||
|
Channels:
|
||||||
|
- Pull latest graph output from: $in_channel
|
||||||
|
- Push prompt-only replacement to: $out_channel
|
||||||
|
- Put analysis/log text in chat or: $log_channel
|
||||||
|
|
||||||
|
Evaluation steps:
|
||||||
|
1. Pull the latest payload from $in_channel.
|
||||||
|
2. Record payload.seed if present. Keep the same seed for prompt-only A/B tests.
|
||||||
|
3. Inspect image_path and compare it to the prompt text.
|
||||||
|
4. Score these Krea2 axes: identity, outfit continuity, pose/action, camera compliance, location coherence, crop/framing, prompt noise, model confusion tokens, seed control, and overall image usefulness.
|
||||||
|
5. Identify the smallest concrete mismatch that should be tested next.
|
||||||
|
6. Classify the finding:
|
||||||
|
- prompt-only: push exactly one edited prompt to $out_channel and preserve payload.seed through the MCP signal when the tool supports it.
|
||||||
|
- guide-rule: update $guide_file with the confirmed Krea2 wording rule.
|
||||||
|
- generator-fix: edit the responsible generator path, add/adjust focused smoke coverage, run tests, and summarize the change.
|
||||||
|
7. Keep a clear link between the image evidence, seed, prompt wording, and generator path.
|
||||||
|
8. Append the finding to the eval log with: seed, original issue, changed wording/path, expected improvement, test result, guide update, generator update, and next hypothesis.
|
||||||
|
|
||||||
|
Current run:
|
||||||
|
- run_id: $run_id
|
||||||
|
- cycle: $cycle
|
||||||
|
- generated_at_utc: $stamp
|
||||||
|
- request_file: $request_file
|
||||||
|
- guide_file: $guide_file
|
||||||
|
EOF
|
||||||
|
|
||||||
|
{
|
||||||
|
echo
|
||||||
|
echo "### Cycle $cycle - $stamp"
|
||||||
|
echo
|
||||||
|
echo "- Request: \`$request_file\`"
|
||||||
|
echo "- Status: pending evaluation"
|
||||||
|
} >> "$summary_file"
|
||||||
|
printf '%s\t%s\t%s\t%s\n' "$cycle" "$stamp" "$request_file" "pending" >> "$events_file"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "================ SxCP Eval Request ================"
|
||||||
|
cat "$request_file"
|
||||||
|
echo "==================================================="
|
||||||
|
echo
|
||||||
|
|
||||||
|
if [ "${SXCP_EVAL_CODEX_CMD:-}" != "" ]; then
|
||||||
|
echo "sxcp_eval_loop: piping request to SXCP_EVAL_CODEX_CMD"
|
||||||
|
SXCP_EVAL_IN_CHANNEL="$in_channel" \
|
||||||
|
SXCP_EVAL_OUT_CHANNEL="$out_channel" \
|
||||||
|
SXCP_EVAL_LOG_CHANNEL="$log_channel" \
|
||||||
|
SXCP_EVAL_GUIDE_FILE="$guide_file" \
|
||||||
|
SXCP_EVAL_REQUEST_FILE="$request_file" \
|
||||||
|
SXCP_EVAL_CYCLE_DIR="$cycle_dir" \
|
||||||
|
SXCP_EVAL_CYCLE="$cycle" \
|
||||||
|
sh -c "$SXCP_EVAL_CODEX_CMD" < "$request_file"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$run_once" -eq 1 ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "sxcp_eval_loop: log written to $run_dir"
|
||||||
@@ -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())
|
||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user