diff --git a/ab_batches/atlas_refine_baseline_deck_analysis.md b/ab_batches/atlas_refine_baseline_deck_analysis.md new file mode 100644 index 0000000..de568c6 --- /dev/null +++ b/ab_batches/atlas_refine_baseline_deck_analysis.md @@ -0,0 +1,71 @@ +# Atlas Refine Baseline Deck Analysis + +Date: 2026-07-01 + +Folder: +`/media/unraid/comfyui/output/CodexMCP-Atlas-Refine` + +Subject id: +`atlas_refine_same_woman_001` + +## Folder State + +The folder contains 16 complete `.png` / `.txt` atlas prompt pairs. + +Coverage report: + +- 10 clean `baseline_only` entries +- 6 `needs_prompt_cleanup` entries +- 0 sidecar prompt variants +- 0 seedable catalog cue candidates +- 0 missing image/text pairs + +The deck is useful as a controlled same-woman baseline set, not yet as a +seed/cue system. Reviewed sidecar variants still need to be authored from +actual prompt/image evidence. + +## Prompt Cleanup Queue + +These baseline prompts still contain positive-channel negative wording through +`without` and should be rewritten before visual scoring or seed promotion: + +- `pov_cowgirl_alt_low_squat_penetration` +- `pov_cowgirl_frontal_straddle_penetration` +- `pov_ejaculation_aftermath_open_thigh_candidate` +- `pov_handjob_upright_centered` +- `pov_reverse_cowgirl_alt_upright_back_facing_penetration` +- `pov_reverse_cowgirl_back_facing_penetration` + +## Visual Deck Read + +The deck is strong for same-subject comparison: + +- the woman identity is stable across poses; +- the coworking lounge style is consistent; +- desk rows, laptops, chair wheels, plants, glass partitions, and warm window + depth are repeated enough to support workspace-continuity scoring; +- most poses place the action as the primary foreground subject. + +Current limitations: + +- all entries are single baseline images, so they do not test seedable + alternatives yet; +- several prompts still use cleanup-needed wording; +- clothing/control is not represented across most poses; +- workspace interaction is mostly background context, not varied physical use + of the lounge furniture; +- exact top-view oral remains weak under text-only prompting even after the + second MCP batch. + +## Next Evidence Priorities + +1. Clean the six `without` prompts before using them for cue seeds. +2. Build prompt-variant sidecars only from tested cues, not invented wording. +3. For each pose family, collect same-subject fixed-seed variants before + touching generator defaults. +4. Score scene/pose height from background cues: floor plane, desk height, + chair wheels, table bases, and whether the viewer/partner appears standing, + seated, kneeling, reclined, or supported by furniture. +5. Keep clothing restore as woman-owned visible detail, and only for garments + that the pose crop can actually show. + diff --git a/ab_batches/blowjob_top_down_vertical_shaft_axis_analysis.md b/ab_batches/blowjob_top_down_vertical_shaft_axis_analysis.md new file mode 100644 index 0000000..4230d41 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_axis_analysis.md @@ -0,0 +1,68 @@ +# Blowjob Top-Down Vertical Shaft A/B Pass + +Date: 2026-07-01 +Batch: `ab_batches/blowjob_top_down_vertical_shaft_axis_batch.json` +Results: `ab_batches/blowjob_top_down_vertical_shaft_axis_results.json` +Sampler seed: `238365845574312` +Pose target: `pov_blowjob_top_down_vertical_shaft` + +## Reference Read + +Atlas examples `27_blowjob_top_view.png` and `2_blowjob_top_view.png` read flatter and more vertical than most generator outputs: the viewer appears standing or high over the woman, the floor/surface plane dominates, and the woman is directly below the camera axis. The generated coworking baseline already has useful office anchors, but it still reads like an elevated forward-looking POV rather than a pure top-down/nadir shot. + +## Strongest Variants + +- `axis_mouth_directly_below_torso` -> `/media/unraid/comfyui/output/agent_bridge/img_75cd8e71ddad45f4a4b2aa9e00ea6127.png` + - Best overall single-seed improvement. Contact is preserved, mouth is centered below torso, both hands hold the base, and the office/floor read remains coherent. + - Still not as vertically flat as atlas `27` or `2`. +- `axis_floor_plane_priority` -> `/media/unraid/comfyui/output/agent_bridge/img_867f5eea66354fb4beb5df586e56bfbc.png` + - Good floor-plane/workspace read, contact preserved, strong centered column. + - Slightly less direct mouth alignment than the best candidate, but useful wording. +- `axis_wide_floor_coworking_rows` -> `/media/unraid/comfyui/output/agent_bridge/img_696bb9f3163049f5b696316db97b46f2.png` + - Good workspace continuity and repeated desk/chair floor grid. Contact preserved. + - Still camera-forward rather than true nadir. +- `axis_chair_wheel_floor` -> `/media/unraid/comfyui/output/agent_bridge/img_5265517ff9544c149277a8711461da17.png` + - Contact preserved with strong chair-wheel/floor anchors. + - Similar axis to baseline, but cleaner workspace evidence. +- `axis_eye_contact_vertical` -> `/media/unraid/comfyui/output/agent_bridge/img_acfdcfda262849eea33dd5b31985751c.png` + - Useful expression/eye-control probe. Preserves contact and subject look. + - Does not solve the flat vertical camera read. +- `axis_clothed_top_visible` -> `/media/unraid/comfyui/output/agent_bridge/img_13516b4bed1a4a69a8542b446822836e.png` + - Woman-owned clothing worked: tank top stayed on the woman and contact remained. + - Useful for later clothing-restore rules, not the main vertical-axis fix. + +## Weak Or Broken Variants + +- `axis_standing_feet_close` broke oral contact. +- `axis_carpet_seam_centerline` produced a centerline artifact and broke contact. +- `axis_glass_partition_floor` broke contact. +- `axis_knees_visible_below_head` broke contact. +- `axis_compact_anatomy` broke contact and did not improve axis. +- `axis_bralette_open_shirt` preserved woman-owned clothing but broke oral contact. +- `axis_low_lounge_table` preserved workspace interaction but broke oral contact. +- `axis_tight_vertical_crop` kept contact but made anatomy too tall/large and did not improve atlas verticality enough. + +## Wording Takeaways + +- Useful wording: + - `mouth directly below the viewer's torso` + - `floor-plane-priority` + - `rows of desk legs, chair wheels, table corners, and carpet seams extend across the floor` + - `chair wheels, caster bases, desk legs, and carpet texture surround the kneeling woman from above` + - `face tilted upward, dark almond eyes looking up into the camera` + - woman-owned clothing: `the woman wears a fitted white ribbed tank top` +- Risky wording: + - `centerline` can create literal artifacts. + - `glass partition floor rail` competes with the action. + - `full kneeling posture visible` pulls the mouth away from contact. + - `foreshortened compact cylinder` did not improve anatomy; it weakened contact. + - Low table / desk edge interactions can improve workspace but often move the action away from the mouth. + +## Next Batch + +Do not promote to generator/catalog from this single-seed run. The next batch should narrow around the top candidates: + +- Keep `axis_mouth_directly_below_torso` as the center prompt. +- Hybridize it with `floor-plane-priority`, `wide coworking rows`, and `chair-wheel floor` anchors. +- Add 12-16 variants that push more verticality using concrete camera/surface language while preserving the exact mouth-contact hierarchy. +- Run at least two sampler seeds before sidecar promotion; use more if the first two disagree. diff --git a/ab_batches/blowjob_top_down_vertical_shaft_axis_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_axis_batch.json new file mode 100644 index 0000000..e6aff4b --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_axis_batch.json @@ -0,0 +1,428 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "source_prompt_sha256": "1d0b95d9865d1a502fb91bc856b3ff4baf00da90248c47d45631fb512f58a463", + "probes": [ + { + "id": "baseline_top_down_vertical_shaft", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. 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. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "baseline_nadir_standing", + "workspace_surface": "coworking_carpet_floor", + "body_angle": "woman_kneels_below_viewer", + "hand_position": "one_hand_base" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 0 + }, + "notes": "control prompt from same-subject atlas refine deck" + }, + { + "id": "axis_floor_fills_frame", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. True nadir standing male POV oral position. the camera points straight down from the viewer's abdomen to the carpet. carpet texture and floor seams fill the frame behind the woman. viewer shorts, abdomen, thighs, knees, and bare feet form a tight lower border. the large penis rises as a compact vertical column in the exact center. the woman kneels directly between the viewer's feet with both knees on the carpet. her hair crown, forehead, shoulders, hands, and knees read from above. her mouth seals around the centered large penis while her right hand wraps the base. desk legs and chair wheels appear as top-down office marks around the carpet plane. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "true_nadir_floor_fills_frame", + "workspace_surface": "carpet_floor_plane", + "body_angle": "kneeling_between_feet", + "hand_position": "right_hand_base" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 101 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_floor_fills_frame" + }, + "notes": "maximize floor-plane dominance and near-vertical camera" + }, + { + "id": "axis_standing_feet_close", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position from a steep overhead angle. viewer stands on carpet with both bare feet close to the woman's knees. viewer abdomen and open shorts sit along the lower edge, thighs descend to the lower corners. the large penis stands upright as a centered foreshortened column. the woman kneels low under the viewer's torso, directly below the camera axis. her face tilts up, dark almond eyes visible, mouth sealed around the centered large penis. her left hand wraps the base while the other hand rests on the viewer's thigh. carpet seams, chair wheels, and desk legs surround the kneeling pose from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "standing_feet_close", + "expression_eye_detail": "eyes_visible_upward", + "hand_position": "one_hand_base_one_hand_thigh", + "workspace_surface": "carpet_with_chair_wheels" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 102 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_standing_feet_close" + }, + "notes": "tests explicit standing feet and upward eye contact" + }, + { + "id": "axis_desk_leg_grid", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep top-down standing male POV oral position inside a coworking desk row. the viewer looks down past his abdomen, open shorts, thighs, and feet. the floor plane dominates the entire image. black desk legs form straight vertical rods around the woman's shoulders and knees. rolling chair bases and carpet seams mark the overhead office geometry. the large penis is centered as a compact vertical cylinder. the woman kneels directly below the viewer, her head centered between his feet, mouth sealed around the tip, both hands stacked at the base. hair crown, brow, shoulders, hands, and knees stay readable from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "steep_top_down", + "workspace_surface": "desk_leg_grid", + "hand_position": "both_hands_stacked_base", + "body_angle": "head_centered_between_feet" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 103 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_desk_leg_grid" + }, + "notes": "workspace anchors should prove top-down geometry" + }, + { + "id": "axis_carpet_seam_centerline", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position with a strong carpet seam centerline. viewer abdomen, open shorts, thighs, and feet frame the lower edge. a long carpet seam runs away from the viewer through the center behind the woman's head. the large penis rises from the lower center as a compact vertical column aligned with that seam. the woman kneels directly below, knees on both sides of the seam, mouth sealed around the centered large penis. one hand wraps the base, the other hand rests flat on the carpet. her hair crown, eyes, shoulders, hands, and knees are seen from above. desk legs and chair wheels sit along the side edges. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "nadir_centerline", + "workspace_surface": "carpet_seam_centerline", + "hand_position": "one_hand_base_one_hand_floor", + "expression_eye_detail": "eyes_visible" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 104 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_carpet_seam_centerline" + }, + "notes": "tests centerline depth cue and avoids horizon wording" + }, + { + "id": "axis_chair_wheel_floor", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside rolling office chairs. the camera points down from the viewer's torso to the carpet. viewer abdomen, shorts, thighs, knees, and feet make the lower foreground border. chair wheels, caster bases, desk legs, and carpet texture surround the kneeling woman from above. the large penis is a centered vertical cylinder rising from the lower middle. the woman kneels below the viewer between his feet with shoulders tucked under his torso line. her mouth seals around the centered large penis. both hands wrap low at the base. her hair crown and dark eyes angle upward toward the camera. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "standing_top_view", + "workspace_surface": "chair_wheel_floor", + "hand_position": "both_hands_low_base", + "expression_eye_detail": "dark_eyes_upward" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 105 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_chair_wheel_floor" + }, + "notes": "tests chair wheel overhead anchors" + }, + { + "id": "axis_under_table_edge", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV top-view oral position beside a coworking desk edge. viewer stands at the desk edge looking down past his abdomen, shorts, thighs, and feet. the wooden tabletop corner appears as a flat rectangle along the upper side of the frame. desk legs descend beside the woman's shoulders. carpet fills the space beneath her knees. the large penis appears as a centered foreshortened vertical column. the woman kneels below the viewer, mouth sealed around the centered large penis, both hands clasping the base. hair crown, forehead, shoulders, hands, and knees are visible from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "steep_standing", + "workspace_surface": "desk_edge_topdown", + "hand_position": "both_hands_clasp_base", + "body_angle": "knees_under_desk_edge" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 106 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_under_table_edge" + }, + "notes": "tests workspace interaction through a visible desk edge" + }, + { + "id": "axis_glass_partition_floor", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. True top-down standing male POV oral position beside a glass partition base. the viewer looks straight down from his abdomen toward the carpet. viewer shorts, thighs, knees, and bare feet frame the lower foreground. the glass partition seam and black floor rail run along one side of the carpet plane. the large penis rises as a compact vertical column at the center. the woman kneels directly below the viewer between his feet with her knees parallel to the rail. her mouth seals around the centered large penis. one hand wraps the base and one hand touches the floor rail side of the carpet. her eyes glance upward from below the hairline. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "true_top_down", + "workspace_surface": "glass_partition_floor_rail", + "hand_position": "base_and_floor", + "expression_eye_detail": "upward_glance" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 107 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_glass_partition_floor" + }, + "notes": "tests glass partition as top-down side anchor" + }, + { + "id": "axis_mouth_directly_below_torso", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position with the woman's mouth directly below the viewer's torso. viewer abdomen and open shorts occupy the lower edge, thighs and feet bracket the lower corners. the large penis rises from the lower center as a compact vertical column. the woman's face sits centered directly under the shaft, mouth sealed around it, hair crown and forehead close to the middle of the frame. both shoulders slope downward toward her knees on the carpet. both hands hold the base with fingers visible from above. surrounding desk legs, chair wheels, carpet seams, and floor texture stay beside the bodies. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "nadir_direct_mouth_alignment", + "contact_depth": "mouth_directly_below_torso", + "hand_position": "both_hands_base_visible", + "workspace_surface": "surrounding_floor_anchors" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 108 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_mouth_directly_below_torso" + }, + "notes": "tests contact alignment rather than background depth" + }, + { + "id": "axis_knees_visible_below_head", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with the woman's full kneeling posture visible from above. viewer abdomen, shorts, thighs, and bare feet frame the lower foreground. her head is centered between the viewer's feet, her shoulders sit below the shaft line, and her knees appear behind her elbows on the carpet. the large penis is a compact centered vertical column. her mouth seals around the centered large penis. one hand wraps the base while the other hand rests on her own thigh. carpet weave, floor seams, chair wheels, and desk legs create a top-down office grid around her knees. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "top_view_full_kneel", + "body_angle": "knees_visible_below_head", + "hand_position": "base_and_own_thigh", + "workspace_surface": "office_grid_floor" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 109 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_knees_visible_below_head" + }, + "notes": "tests full kneeling silhouette" + }, + { + "id": "axis_compact_anatomy", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Near-vertical standing male POV oral position. viewer looks down from his abdomen toward the carpet between his feet. viewer shorts, thighs, knees, and bare feet anchor the lower frame. the large penis appears foreshortened by the top-down angle as a compact centered cylinder with rounded tip at the woman's lips. the woman kneels directly below the viewer, shoulders narrow under the camera, mouth sealed around the centered tip. her right hand grips the base, left hand rests on the viewer's thigh. carpet seams, desk legs, rolling chair bases, and floor texture remain flat behind her. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "near_vertical", + "anatomy_shape_detail": "foreshortened_compact_cylinder", + "hand_position": "base_and_viewer_thigh", + "workspace_surface": "flat_floor_background" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 110 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_compact_anatomy" + }, + "notes": "tests anatomy length control for top-down angle" + }, + { + "id": "axis_eye_contact_vertical", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep overhead standing male POV oral position. viewer looks down past abdomen, open shorts, thighs, and feet onto the carpet. the woman kneels directly between his feet, face tilted upward, dark almond eyes looking up into the camera from below her brow. the large penis is a centered compact vertical column leading from the lower edge to her mouth. her lips seal around the centered tip. both hands wrap the base with long fingers visible from above. carpet texture fills the frame, desk legs and chair wheels appear as overhead office anchors around her shoulders and knees. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "steep_overhead", + "expression_eye_detail": "direct_upward_eye_contact", + "hand_position": "both_hands_base", + "workspace_surface": "carpet_texture_fill" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 111 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_eye_contact_vertical" + }, + "notes": "tests eyes while preserving vertical axis" + }, + { + "id": "axis_soft_expression", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. True nadir standing male POV top-view oral position. viewer abdomen, open shorts, thighs, knees, and bare feet create the lower foreground frame. carpet plane and desk-leg shadows fill the rest of the image. the large penis rises at the exact center as a compact vertical column. the woman kneels directly below the viewer, mouth sealed around the centered large penis, eyes lifted upward with a calm focused expression. her hair crown, forehead, lashes, shoulders, hands, and knees are visible from above. one hand wraps the base, one hand rests on the carpet beside her knee. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "true_nadir", + "expression_eye_detail": "calm_focused_upward_expression", + "hand_position": "base_and_carpet", + "workspace_surface": "desk_leg_shadows_on_carpet" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 112 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_soft_expression" + }, + "notes": "tests expression control under strict axis" + }, + { + "id": "axis_clothed_top_visible", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position from directly above. viewer abdomen, open shorts, thighs, and bare feet frame the lower foreground. the woman kneels between his feet on the coworking carpet. the woman wears a fitted white ribbed tank top visible across her shoulders and chest. the large penis is a centered foreshortened vertical column leading to her mouth. her mouth seals around the centered large penis, one hand wrapped at the base and one hand on her own chest. hair crown, brow, shoulders, hands, tank top straps, and knees read from above. desk legs, chair wheels, carpet seams, and floor texture surround the kneeling pose. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "directly_above", + "clothing_visibility": "woman_white_tank_top_visible", + "hand_position": "base_and_own_chest", + "workspace_surface": "coworking_carpet" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 113 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_clothed_top_visible" + }, + "notes": "tests woman-owned clothing visibility" + }, + { + "id": "axis_bralette_open_shirt", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position on coworking carpet. viewer abdomen, open shorts, thighs, knees, and feet form the lower border. the woman kneels directly below the viewer between his feet. the woman wears a fitted dark bralette and an open light button-down shirt draped from her shoulders. the large penis is centered as a compact vertical cylinder. her mouth seals around the centered large penis, both hands wrapped low at the base. hair crown, eyes, shoulders, bralette edge, open shirt collar, hands, and knees are visible from above. desk legs and chair wheels frame the carpet plane. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "nadir", + "clothing_visibility": "woman_bralette_open_button_down", + "hand_position": "both_hands_low_base", + "workspace_surface": "carpet_desk_chairs" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 114 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_bralette_open_shirt" + }, + "notes": "tests woman-owned clothing with office outfit" + }, + { + "id": "axis_low_lounge_table", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside a low coworking lounge table. viewer looks down past abdomen, shorts, thighs, knees, and feet. the carpet floor plane dominates the frame. a low table corner and table legs appear as flat top-down workspace shapes near the woman's shoulder. the large penis rises from the lower center as a compact vertical column. the woman kneels directly below the viewer, mouth sealed around the centered large penis, both hands cupping the base. her hair crown, eyes, shoulders, hands, and knees are visible from above. chair wheels and carpet seams continue the overhead office read around the table. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "standing_top_view", + "workspace_surface": "low_lounge_table_edge", + "hand_position": "both_hands_cupping_base", + "body_angle": "kneeling_beside_low_table" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 115 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_low_lounge_table" + }, + "notes": "tests workspace interaction with low table" + }, + { + "id": "axis_wide_floor_coworking_rows", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High top-down standing male POV oral position with a wider coworking floor read. viewer abdomen, open shorts, thighs, knees, and bare feet stay in the lower foreground. the woman kneels between his feet on the carpet, smaller under the higher downward camera. rows of desk legs, chair wheels, table corners, and carpet seams extend across the floor around her. the large penis remains a centered compact vertical column from the lower edge to her mouth. her mouth seals around it and one hand wraps the base. hair crown, shoulders, hands, knees, and the floor grid are all visible from above. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "higher_top_down_wide_floor", + "workspace_surface": "coworking_rows_floor_grid", + "contact_depth": "compact_centered_contact", + "hand_position": "one_hand_base" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 116 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_wide_floor_coworking_rows" + }, + "notes": "tests a wider floor-dominant frame" + }, + { + "id": "axis_tight_vertical_crop", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Tight nadir standing male POV oral position. viewer abdomen and open shorts fill the bottom edge, thighs and feet appear close at the lower sides. the large penis is a compact centered vertical column occupying the middle. the woman's mouth seals around the centered tip directly below the viewer's torso. her hair crown, forehead, eyelashes, hands, shoulders, and knees appear around the shaft from above. both hands hold the base with fingers stacked. carpet texture fills every background gap, with only nearby desk legs and chair wheels at the side edges. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "tight_nadir_crop", + "contact_depth": "mouth_directly_below_tip", + "hand_position": "stacked_fingers_base", + "workspace_surface": "carpet_background_gaps" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 117 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_tight_vertical_crop" + }, + "notes": "tests tight crop and vertical shaft dominance" + }, + { + "id": "axis_viewer_feet_frame_head", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with the viewer's bare feet framing the woman's head. the camera looks down from the viewer's torso to the carpet. viewer abdomen, shorts, thighs, knees, and feet form a clear lower frame. the woman's head sits centered between the feet, shoulders below the feet line, knees farther into the carpet plane. the large penis is a compact centered vertical column entering her mouth. her mouth seals around it, left hand at the base, right hand on the viewer's shin. desk legs, chair wheels, carpet seams, and floor texture show the same overhead office angle around the body. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "standing_feet_frame_head", + "body_angle": "head_between_feet_shoulders_below", + "hand_position": "base_and_viewer_shin", + "workspace_surface": "overhead_office_angle" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 118 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_viewer_feet_frame_head" + }, + "notes": "tests viewer standing geometry through feet/head relation" + }, + { + "id": "axis_phone_like_top_snapshot", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Phone-like standing male POV top snapshot from above the viewer's torso. viewer abdomen, open shorts, thighs, knees, and bare feet anchor the lower part of the frame. the camera points almost straight down to the carpet. the large penis appears as a centered foreshortened vertical column. the woman kneels directly below between the viewer's feet, looking upward with dark almond eyes, mouth sealed around the centered tip. one hand wraps the base and the other hand holds the viewer's thigh. desk legs, chair wheels, carpet seams, laptop tables, and glass partition floor rails appear as top-down coworking details around her. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "phone_like_top_snapshot", + "expression_eye_detail": "upward_eyes", + "hand_position": "base_and_viewer_thigh", + "workspace_surface": "laptop_tables_floor_rails" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 119 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_phone_like_top_snapshot" + }, + "notes": "tests phone-like top snapshot wording" + }, + { + "id": "axis_floor_plane_priority", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Floor-plane-priority standing male POV oral position. viewer stands above the kneeling woman and looks down from his abdomen. open shorts, thighs, feet, and lower abdomen frame the bottom. the carpet plane is the main background surface, filled with woven texture, floor seams, desk-leg feet, and caster wheels. the large penis rises from the lower center as a foreshortened vertical cylinder. the woman kneels in the middle of the carpet plane, mouth sealed around the centered large penis. both hands wrap around the base, shoulders and knees visible below her hair crown. her dark eyes angle up toward the viewer. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "cue_axes": { + "camera_height": "floor_plane_priority", + "workspace_surface": "woven_carpet_seams_wheels", + "hand_position": "both_hands_wrap_base", + "expression_eye_detail": "eyes_angle_up" + }, + "seed_metadata": { + "sampler_seed": 238365845574312, + "atlas_cue_seed": 120 + }, + "prompt_source": { + "kind": "text", + "prompt_variant_id": "axis_floor_plane_priority" + }, + "notes": "tests floor-priority wording as a strong axis cue" + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_axis_results.json b/ab_batches/blowjob_top_down_vertical_shaft_axis_results.json new file mode 100644 index 0000000..b19703a --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_axis_results.json @@ -0,0 +1,153 @@ +{ + "seed": 238365845574312, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "baseline_top_down_vertical_shaft", + "prompt_order": "subject_first", + "turn": 6, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_8c3ae3ebaff74bee91cdcdb600163c79.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_floor_fills_frame", + "prompt_order": "subject_first", + "turn": 7, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_a89acdeb80f641c59fef7352b6c20661.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_standing_feet_close", + "prompt_order": "subject_first", + "turn": 8, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_57ffe9aa6c7d4dcda6d49937b1dcdc5c.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_desk_leg_grid", + "prompt_order": "subject_first", + "turn": 9, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_e85bd81bef114bf1b741dc816254881a.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_carpet_seam_centerline", + "prompt_order": "subject_first", + "turn": 10, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_c13ee92586ed4310bf1bd23626eee5f0.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_chair_wheel_floor", + "prompt_order": "subject_first", + "turn": 11, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_5265517ff9544c149277a8711461da17.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_under_table_edge", + "prompt_order": "subject_first", + "turn": 12, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_396bd047524b4c92b46d5860fec682da.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_glass_partition_floor", + "prompt_order": "subject_first", + "turn": 13, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_69a268a1cf3142f39d99d159853e92df.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_mouth_directly_below_torso", + "prompt_order": "subject_first", + "turn": 14, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_75cd8e71ddad45f4a4b2aa9e00ea6127.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_knees_visible_below_head", + "prompt_order": "subject_first", + "turn": 15, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_22bfc679e02b4b9b9d3ee21a9a302d03.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_compact_anatomy", + "prompt_order": "subject_first", + "turn": 16, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_cef16a8cebd649c4a53191f9f74f21e7.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_eye_contact_vertical", + "prompt_order": "subject_first", + "turn": 17, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_acfdcfda262849eea33dd5b31985751c.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_soft_expression", + "prompt_order": "subject_first", + "turn": 18, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_3f455403ee98473e8d30ef34d93ac9d1.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_clothed_top_visible", + "prompt_order": "subject_first", + "turn": 19, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_13516b4bed1a4a69a8542b446822836e.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_bralette_open_shirt", + "prompt_order": "subject_first", + "turn": 20, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_e4f7ec25182b4acfa4ecce291a0a5097.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_low_lounge_table", + "prompt_order": "subject_first", + "turn": 21, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_eda27adbb98648b492444867f764d4c9.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_wide_floor_coworking_rows", + "prompt_order": "subject_first", + "turn": 22, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_696bb9f3163049f5b696316db97b46f2.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_tight_vertical_crop", + "prompt_order": "subject_first", + "turn": 23, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_9840c47c74f7444ab5a7b8afd24189f4.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_viewer_feet_frame_head", + "prompt_order": "subject_first", + "turn": 24, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_11315ff26c78428fb5e630d9c4fd7ac5.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_phone_like_top_snapshot", + "prompt_order": "subject_first", + "turn": 25, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_da68aff4f3234e51a4dca2196dc8d9ae.png", + "returned_seed": 238365845574312 + }, + { + "id": "axis_floor_plane_priority", + "prompt_order": "subject_first", + "turn": 26, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_867f5eea66354fb4beb5df586e56bfbc.png", + "returned_seed": 238365845574312 + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_floorplan_salvage_analysis.md b/ab_batches/blowjob_top_down_vertical_shaft_floorplan_salvage_analysis.md new file mode 100644 index 0000000..7a2aaa9 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_floorplan_salvage_analysis.md @@ -0,0 +1,92 @@ +# Blowjob Top-Down Vertical Shaft Floor-Plan Salvage + +Date: 2026-07-01 + +Pose target: `pov_blowjob_top_down_vertical_shaft` + +Reference atlas examples: + +- `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/27_blowjob_top_view.png` +- `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/2_blowjob_top_view.png` + +Key manual evidence: + +- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.png` +- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00079_.png` + +MCP batches: + +- `ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_batch.json` +- `ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_results.json` +- `ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_batch.json` +- `ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_results.json` + +Sampler seed: `238365845574312` + +## Atlas Comparison + +The atlas frames do not mainly solve the pose by saying “top view” many times. +They solve it by making the environment match the camera axis: + +- the background is a floor or ground plane, not a deep room; +- environmental evidence is flat to the camera, such as floor boards, turf, net, + or cropped edge objects; +- the partner is below the camera, and her head/crown/shoulders stack downward + in the image; +- foreground viewer body cues sit at the lower edge, but they do not require a + broad rendered room behind the partner. + +The original coworking scene tail fought that geometry because `tall windows`, +`repeated desk rows`, and `soft shared-office depth` invite a forward-looking +room render. More vertical synonyms did not fully fix that conflict. + +## Working Prompt Principle + +For this pose, translate the coworking lounge into top-down floor evidence: + +```text +Set in a coworking lounge seen as a top-down floor plan: carpet texture, +carpet tile seams +``` + +This is not a generic “remove background” rule. It is a pose-specific scene +translation. A different atlas family may need more environment detail if the +reference angle actually shows a room, desk surface, sofa, wall, bed, or other +support. + +## Strong Current Candidates + +- `manual_00079_minimal_tail` + - image: `/media/unraid/comfyui/output/agent_bridge/img_c44d156d05614e00858dbe659be7ebc0.png` + - prompt keeps the manual `img_00079` minimal scene tail. + - Strong overhead read, carpet-dominant, still plausible coworking floor. +- `minimal_tail_floor_plane_background` + - image: `/media/unraid/comfyui/output/agent_bridge/img_726db4d0591647c2a2ed39a93f37ccb9.png` + - Strong overhead and clean floor-plane background. + - Slightly more generic floor material than the coworking carpet-tail version. +- `minimal_tail_tight_crop` + - image: `/media/unraid/comfyui/output/agent_bridge/img_4764ba4774974d9789df7fdaa12b1f65.png` + - Strong contact priority and verticality. + - More cropped; needs user verification against atlas preference. + +## Weaker Add-Backs + +Adding explicit office objects too early can reintroduce room-depth pressure. +Single or double anchors such as `one cropped chair caster`, `one cropped desk +foot`, or both may be useful for scene identity, but they should be added only +after the vertical floor-plane read is preserved across seeds. + +## Current Decision + +This pose is no longer a text-weak case. The failure was mostly a scene/camera +conflict: + +- bad: overhead pose wording plus deep coworking room-depth tail; +- better: overhead pose wording plus floor-plan coworking tail; +- best current hypothesis: overhead pose wording plus minimal carpet/tile-seam + coworking floor tail. + +Next gate: user visual verification of the strongest candidate, then repeat +the selected minimal-tail candidate on at least one second sampler seed before +sidecar/catalog cue promotion. + diff --git a/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_analysis.md b/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_analysis.md new file mode 100644 index 0000000..7cab076 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_analysis.md @@ -0,0 +1,78 @@ +# Blowjob Top-Down Vertical Shaft Image-To-Prompt Calibration + +Date: 2026-07-01 + +Pose target: `pov_blowjob_top_down_vertical_shaft` + +Primary atlas reference: + +- `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/22_blowjob_top_view.png` + +Manual calibration evidence: + +- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00100_.png` +- `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00101_.png` + +MCP image-to-prompt batch: + +- `ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_batch.json` +- `ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_results.json` + +Sampler seed: `238365845574312` + +## What Improved + +The strongest verticality came from describing the camera and scene as a single +top-down plane, not from repeating more top-view synonyms. The working manual +prompt removed the deep coworking-room tail and used a flat floor/support read: + +```text +Background reads as a flat pale floor and cropped white lounge chair surface, +with very little room depth. +``` + +This confirms the earlier conflict analysis: for this atlas family, deep room +phrases such as repeated desk rows, tall windows, and soft background depth +fight the vertical camera axis. + +## Remaining Miss + +The calibration images have the best verticality so far, but compared with atlas +22 the viewer foreground is still too dominant. The atlas reference gives more +visual weight to the woman's face, hair crown, shoulders, upper chest, and hand +stack. The next prompt rule should push that positive hierarchy, rather than +adding room detail or relying on negative-style body suppression. + +## Prompt Rule + +Use a positive visibility order: + +```text +Straight-down male POV oral close-up. The camera looks almost vertically down +from the man's upper abdomen. The woman's face, eyelids, hair crown, shoulders, +upper chest, and one hand stack directly below the camera. Centered mouth +contact aligns with the vertical shaft. One hand wraps the base near the man's +lower abdomen. The man's lower abdomen and small thigh edges anchor only the +bottom foreground. Tucked knees remain small side shapes on the floor. The +background reads as a flat pale floor and one cropped white lounge chair +surface, with shallow top-down room depth. +``` + +For clothed atlas variants, use subject-owned clothing as a geometry anchor: + +```text +The woman wears a fitted white ribbed tank top; the tank-top neckline and +shoulders remain visible from above. +``` + +Avoid carrying the manual scoring phrase `hips and ass stay visually secondary, +mostly hidden` into final positive conditioning. Keep that as a human rejection +criterion, and express the render prompt through the visible upper-body stack. + +## Current Decision + +This pose is prompt-responsive. The route should not be promoted yet from these +two manual calibrations alone, but the next batch should keep the successful +floor/support-plane camera tail fixed and compare upper-body-stack variants, +including the fitted-tank-top anchor that previously matched atlas 22 more +closely than the nude minimal-floor candidates. diff --git a/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_batch.json new file mode 100644 index 0000000..e7a3084 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_batch.json @@ -0,0 +1,67 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "selection": { + "purpose": "image-to-prompt inversion from clothed generated atlas-like result and atlas_22 body geometry", + "sampler_seed_role": "fixed sampler seed for comparison against minimal_tail_woman_tank_top" + }, + "probes": [ + { + "id": "clothed_minimal_tail_control", + "prompt_order": "subject_first", + "cue_axes": ["control", "owned_clothing", "minimal_scene_tail"], + "evidence": { + "prior_image": "/media/unraid/comfyui/output/agent_bridge/img_556bc9272211426cb6e0139c4ec0dd0a.png", + "atlas_reference": "/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view/22_blowjob_top_view.png" + }, + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "upper_body_stack_tank_top", + "prompt_order": "subject_first", + "cue_axes": ["image_to_prompt", "upper_body_stack", "owned_clothing"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. her hair crown, forehead, face, white tank-top neckline, shoulders, hands, and tucked knees stack vertically below the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base directly under her lips. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "atlas_22_torso_neckline", + "prompt_order": "subject_first", + "cue_axes": ["image_to_prompt", "atlas_22_body_geometry", "torso_neckline"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the woman's face, tank-top neckline, shoulders, and upper torso are the main partner shapes below the camera. her knees tuck under her body on the carpet behind the shoulder line. her mouth seals around the centered foreshortened large penis. both hands wrap the base. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "upright_kneeling_torso", + "prompt_order": "subject_first", + "cue_axes": ["image_to_prompt", "upright_kneeling", "body_proportion"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman kneels upright below the viewer with shoulders and tank-top chest centered under her head. her knees are tucked on the carpet at the sides of the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base under her lips. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "shoulders_primary_knees_small", + "prompt_order": "subject_first", + "cue_axes": ["image_to_prompt", "shoulder_priority", "knee_scale"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. her head, face, shoulders, and white tank-top upper torso dominate the partner silhouette. her tucked knees remain smaller side shapes on the carpet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and sit between the viewer's feet. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "tank_top_eye_contact", + "prompt_order": "subject_first", + "cue_axes": ["image_to_prompt", "eye_contact", "owned_clothing"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. her face tilts upward, dark almond eyes looking into the camera, with the white tank-top neckline and shoulders visible below her face. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her tucked knees remain low on the carpet. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "tank_top_one_caster_edge", + "prompt_order": "subject_first", + "cue_axes": ["image_to_prompt", "owned_clothing", "minimal_workspace_anchor"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. her face, white tank-top neckline, shoulders, hands, and tucked knees stack below the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped chair caster at the far edge" + }, + { + "id": "tight_tank_top_stack", + "prompt_order": "subject_first", + "cue_axes": ["image_to_prompt", "tight_crop", "upper_body_stack"], + "text": "Tight straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen. her face, shoulders, white tank-top upper torso, hands, and tucked knees fill the center of the carpet floor. her mouth seals around the centered foreshortened large penis. both hands wrap the base directly under her lips. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_results.json b/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_results.json new file mode 100644 index 0000000..c2a57c7 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_image_to_prompt_seed_238365845574312_results.json @@ -0,0 +1,62 @@ +{ + "seed": 238365845574312, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "clothed_minimal_tail_control", + "prompt_order": "subject_first", + "turn": 78, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_d5264d677f954800aaf5d71b11249e32.png", + "returned_seed": 238365845574312 + }, + { + "id": "upper_body_stack_tank_top", + "prompt_order": "subject_first", + "turn": 79, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_907d592ed7b44e6dacf6a3d22a7e48f1.png", + "returned_seed": 238365845574312 + }, + { + "id": "atlas_22_torso_neckline", + "prompt_order": "subject_first", + "turn": 80, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_9dfec0d52f8d4e468bfe639e94e0f382.png", + "returned_seed": 238365845574312 + }, + { + "id": "upright_kneeling_torso", + "prompt_order": "subject_first", + "turn": 81, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_5ebc5a243b6247bf990769bb902d7563.png", + "returned_seed": 238365845574312 + }, + { + "id": "shoulders_primary_knees_small", + "prompt_order": "subject_first", + "turn": 82, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_b39a8cfe8f9c4819b12d472f931a0e55.png", + "returned_seed": 238365845574312 + }, + { + "id": "tank_top_eye_contact", + "prompt_order": "subject_first", + "turn": 83, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_ec875770f68144c3a495b186da589848.png", + "returned_seed": 238365845574312 + }, + { + "id": "tank_top_one_caster_edge", + "prompt_order": "subject_first", + "turn": 84, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_aa78fceb96c14f17a585c0adcc9c0ddf.png", + "returned_seed": 238365845574312 + }, + { + "id": "tight_tank_top_stack", + "prompt_order": "subject_first", + "turn": 85, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_32c138249203481290985c29c48960fe.png", + "returned_seed": 238365845574312 + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_batch.json new file mode 100644 index 0000000..9974c86 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_batch.json @@ -0,0 +1,67 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "selection": { + "purpose": "minimal coworking floor-tail probes from manual img_00079 conflict-analysis evidence", + "sampler_seed_role": "fixed sampler seed for direct comparison against sparse manual evidence" + }, + "probes": [ + { + "id": "manual_00079_minimal_tail", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "minimal_scene_tail", "carpet_only"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00079_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00079_.txt" + }, + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "minimal_tail_with_one_caster", + "prompt_order": "subject_first", + "cue_axes": ["minimal_scene_tail", "single_office_anchor", "sparse_floor"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped chair caster at the far edge" + }, + { + "id": "minimal_tail_with_edge_desk_foot", + "prompt_order": "subject_first", + "cue_axes": ["minimal_scene_tail", "single_office_anchor", "desk_foot_edge"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped desk foot at the frame edge" + }, + { + "id": "minimal_tail_with_caster_and_desk_foot", + "prompt_order": "subject_first", + "cue_axes": ["minimal_scene_tail", "two_office_anchors", "sparse_floor"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, one cropped chair caster, one cropped desk foot" + }, + { + "id": "carpet_only_no_lounge_word", + "prompt_order": "subject_first", + "cue_axes": ["scene_tail_removed", "carpet_only", "conflict_reduction"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Carpet texture and carpet tile seams fill the background floor plane." + }, + { + "id": "minimal_tail_floor_plane_background", + "prompt_order": "subject_first", + "cue_axes": ["floor_as_background", "minimal_scene_tail", "top_view"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the carpet floor plane is the background. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen from above: carpet texture and carpet tile seams." + }, + { + "id": "minimal_tail_tight_crop", + "prompt_order": "subject_first", + "cue_axes": ["tight_crop", "minimal_scene_tail", "contact_priority"], + "text": "Tight straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + }, + { + "id": "minimal_tail_woman_tank_top", + "prompt_order": "subject_first", + "cue_axes": ["owned_clothing", "minimal_scene_tail", "pose_preservation"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams" + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_results.json b/ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_results.json new file mode 100644 index 0000000..a8d4a87 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_minimal_floor_tail_seed_238365845574312_results.json @@ -0,0 +1,62 @@ +{ + "seed": 238365845574312, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "manual_00079_minimal_tail", + "prompt_order": "subject_first", + "turn": 70, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_c44d156d05614e00858dbe659be7ebc0.png", + "returned_seed": 238365845574312 + }, + { + "id": "minimal_tail_with_one_caster", + "prompt_order": "subject_first", + "turn": 71, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_a75439104c734857ae2f1cc39cbd54a5.png", + "returned_seed": 238365845574312 + }, + { + "id": "minimal_tail_with_edge_desk_foot", + "prompt_order": "subject_first", + "turn": 72, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_9798ccfcef9e47d3bce59465bc236d15.png", + "returned_seed": 238365845574312 + }, + { + "id": "minimal_tail_with_caster_and_desk_foot", + "prompt_order": "subject_first", + "turn": 73, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_c98adad644254d81b69e7d52bf076003.png", + "returned_seed": 238365845574312 + }, + { + "id": "carpet_only_no_lounge_word", + "prompt_order": "subject_first", + "turn": 74, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_fa66fc003bbb499887572e7becc68bbc.png", + "returned_seed": 238365845574312 + }, + { + "id": "minimal_tail_floor_plane_background", + "prompt_order": "subject_first", + "turn": 75, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_726db4d0591647c2a2ed39a93f37ccb9.png", + "returned_seed": 238365845574312 + }, + { + "id": "minimal_tail_tight_crop", + "prompt_order": "subject_first", + "turn": 76, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_4764ba4774974d9789df7fdaa12b1f65.png", + "returned_seed": 238365845574312 + }, + { + "id": "minimal_tail_woman_tank_top", + "prompt_order": "subject_first", + "turn": 77, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_556bc9272211426cb6e0139c4ec0dd0a.png", + "returned_seed": 238365845574312 + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_batch.json new file mode 100644 index 0000000..08c449b --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_batch.json @@ -0,0 +1,91 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "selection": { + "purpose": "top-view oral salvage with overhead camera words and floor-plan coworking scene wording", + "sampler_seed_role": "fixed sampler seed for direct comparison against prior MCP and manual overhead probes" + }, + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "cue_axes": ["baseline", "folder_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. 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. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "manual_00062_original_scene_control", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "overhead_prefix", "original_scene_tail"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.txt" + }, + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "overhead_floorplan_scene_tail", + "prompt_order": "subject_first", + "cue_axes": ["overhead_prefix", "straight_down", "floorplan_scene_tail"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "perfectly_vertical_floorplan", + "prompt_order": "subject_first", + "cue_axes": ["perfectly_vertical", "floorplan_scene_tail", "strong_camera_axis"], + "text": "Perfectly vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "straight_down_floorplan", + "prompt_order": "subject_first", + "cue_axes": ["straight_down_prefix", "floorplan_scene_tail", "camera_axis"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "floor_plan_view", + "prompt_order": "subject_first", + "cue_axes": ["floor_plan_view", "scene_reconciliation", "workspace_floor"], + "text": "Top-down floor-plan view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV oral position. viewer looks straight down from his abdomen onto the carpet floor. the woman kneels below the viewer in the middle of the floor-plan composition. her mouth seals around the centered foreshortened large penis. both hands wrap the base. her hair crown, forehead, shoulders, hands, and knees read from above. Coworking lounge objects read as floor-plan anchors: carpet seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots around her." + }, + { + "id": "perpendicular_camera_axis", + "prompt_order": "subject_first", + "cue_axes": ["camera_axis", "technical_verticality", "floorplan_scene_tail"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. the camera axis is perpendicular to the carpet floor and points straight down from the viewer's abdomen. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "ceiling_to_floor_alignment", + "prompt_order": "subject_first", + "cue_axes": ["plumb_alignment", "floorplan_scene_tail", "overhead_prefix"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Plumb vertical standing male POV oral position. viewer looks straight down from his abdomen to the carpet floor. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "floor_fills_frame", + "prompt_order": "subject_first", + "cue_axes": ["floor_ratio", "floorplan_scene_tail", "workspace_floor"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. carpet floor fills the frame around the kneeling woman. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge floor details surround her: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots." + }, + { + "id": "woman_tank_top_floorplan", + "prompt_order": "subject_first", + "cue_axes": ["owned_clothing", "floorplan_scene_tail", "pose_preservation"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "eye_contact_floorplan", + "prompt_order": "subject_first", + "cue_axes": ["eye_control", "floorplan_scene_tail", "pose_preservation"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her face tilts upward and her dark almond eyes look up into the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "compact_floorplan_no_camera_tail", + "prompt_order": "subject_first", + "cue_axes": ["short_prompt", "floorplan_scene_tail", "noise_reduction"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. the woman kneels in the center of the floor below him. her mouth seals around the centered foreshortened large penis. both hands wrap the base. her hair crown, forehead, shoulders, hands, and knees read from above. Coworking lounge floor-plan anchors surround her: carpet seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots." + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_results.json b/ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_results.json new file mode 100644 index 0000000..94ab143 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_overhead_floorplan_seed_238365845574312_results.json @@ -0,0 +1,90 @@ +{ + "seed": 238365845574312, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "turn": 58, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_7c307b0667b04c9190b657ec0c59eace.png", + "returned_seed": 238365845574312 + }, + { + "id": "manual_00062_original_scene_control", + "prompt_order": "subject_first", + "turn": 59, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_21686bf41ca74e16a689bcb58b818213.png", + "returned_seed": 238365845574312 + }, + { + "id": "overhead_floorplan_scene_tail", + "prompt_order": "subject_first", + "turn": 60, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_498ada9c2dab4adfb03cd1e2e5655a39.png", + "returned_seed": 238365845574312 + }, + { + "id": "perfectly_vertical_floorplan", + "prompt_order": "subject_first", + "turn": 61, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_a677480fbf3241d1b912630e1c659dc4.png", + "returned_seed": 238365845574312 + }, + { + "id": "straight_down_floorplan", + "prompt_order": "subject_first", + "turn": 62, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_02410cd85ef14ceaa160cca11116f5cb.png", + "returned_seed": 238365845574312 + }, + { + "id": "floor_plan_view", + "prompt_order": "subject_first", + "turn": 63, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_6383798de55c40e68373061e0d2d4890.png", + "returned_seed": 238365845574312 + }, + { + "id": "perpendicular_camera_axis", + "prompt_order": "subject_first", + "turn": 64, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_2b649968429e4a7d97ea2e08ecc20484.png", + "returned_seed": 238365845574312 + }, + { + "id": "ceiling_to_floor_alignment", + "prompt_order": "subject_first", + "turn": 65, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_cc7c76a2226f4949897d20e80108dc55.png", + "returned_seed": 238365845574312 + }, + { + "id": "floor_fills_frame", + "prompt_order": "subject_first", + "turn": 66, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_ce49b6650d2845f6a297703af02fdcf5.png", + "returned_seed": 238365845574312 + }, + { + "id": "woman_tank_top_floorplan", + "prompt_order": "subject_first", + "turn": 67, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_cdb2eb93082d4369990b52c33d0254f6.png", + "returned_seed": 238365845574312 + }, + { + "id": "eye_contact_floorplan", + "prompt_order": "subject_first", + "turn": 68, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_89b27cdbb89a4d999c52f6eba8a23cd2.png", + "returned_seed": 238365845574312 + }, + { + "id": "compact_floorplan_no_camera_tail", + "prompt_order": "subject_first", + "turn": 69, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_c86cc56829b14a7eb30d4fd30f95c0fd.png", + "returned_seed": 238365845574312 + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_overhead_salvage_seed_238365845574312_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_overhead_salvage_seed_238365845574312_batch.json new file mode 100644 index 0000000..c00aeed --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_overhead_salvage_seed_238365845574312_batch.json @@ -0,0 +1,99 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "selection": { + "purpose": "salvage top-view oral using user manual overhead prompt evidence from sxcp_accumulator/bwave_2", + "sampler_seed_role": "fixed sampler seed for direct comparison with previous MCP probes" + }, + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "cue_axes": ["baseline", "folder_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. 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. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "prior_low_head_high_floor_ratio", + "prompt_order": "subject_first", + "cue_axes": ["previous_probe_control", "floor_ratio", "no_overhead_prefix"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "manual_00057_vertical_overhead", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "vertical_overhead_prefix", "full_lower_body_sentence"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00057_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00057_.txt" + }, + "text": "Vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "manual_00059_top_vertical_truncated", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "top_vertical_overhead_prefix", "truncated_lower_body_sentence"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00059_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00059_.txt" + }, + "text": "Top Vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "manual_00060_completely_vertical", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "completely_vertical_overhead_prefix", "truncated_lower_body_sentence"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00060_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00060_.txt" + }, + "text": "Completely Vertical overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "manual_00061_vertical_ceiling", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "vertical_ceiling_prefix", "truncated_lower_body_sentence"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00061_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00061_.txt" + }, + "text": "Vertical ceiling view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "manual_00062_overhead_straight_down", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "overhead_prefix", "straight_down", "minimal_lower_body_sentence"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00062_.txt" + }, + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "manual_00063_overhead_camera_vertical", + "prompt_order": "subject_first", + "cue_axes": ["manual_evidence", "overhead_prefix", "straight_down", "camera_vertical_phrase"], + "evidence": { + "manual_image": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00063_.png", + "manual_prompt": "/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00063_.txt" + }, + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is overhead vertical male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "overhead_straight_down_woman_tank_top", + "prompt_order": "subject_first", + "cue_axes": ["owned_clothing", "overhead_prefix", "pose_preservation"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. tank top shoulders and neckline remain visible from above. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "overhead_straight_down_eye_contact", + "prompt_order": "subject_first", + "cue_axes": ["facial_expression", "eye_control", "overhead_prefix", "pose_preservation"], + "text": "Overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her face tilts upward and her dark almond eyes look up into the camera. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_refine_analysis.md b/ab_batches/blowjob_top_down_vertical_shaft_refine_analysis.md new file mode 100644 index 0000000..ebeb2f6 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_refine_analysis.md @@ -0,0 +1,100 @@ +# Blowjob Top-Down Vertical Shaft Refine Pass + +Date: 2026-07-01 + +Pose target: `pov_blowjob_top_down_vertical_shaft` + +Reference folder baseline: +`/media/unraid/comfyui/output/CodexMCP-Atlas-Refine/pov_blowjob_top_down_vertical_shaft_00001_.png` + +Prior batch: +`ab_batches/blowjob_top_down_vertical_shaft_axis_batch.json` + +Second-pass batches: + +- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_batch.json` +- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_batch.json` + +Second-pass results: + +- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_results.json` +- `ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_results.json` + +Sampler seeds: + +- `238365845574312` +- `238365845574313` + +Total text-only attempts counted for this pose: 51 prompt/seed outcomes +across the first and second MCP batches. + +## Result + +The second pass is stable but does not solve the atlas top-view geometry. Most +variants converge to a front-facing kneeling or seated oral composition: contact +is often preserved, subject identity stays strong, and the coworking floor grid +is coherent, but the camera still reads forward/downward rather than true nadir. + +This means the exact atlas family shown by `blowjob_top_view` references is a +weak text-only case for this model under subject-first prompting. Treat it as a +candidate for stronger image/control guidance if a flatter atlas match is +required. + +## Best Repeatable Partial + +The best repeatable partial remains the first-pass direction: + +- `axis_mouth_directly_below_torso` +- `axis_floor_plane_priority` + +The refined `floor_plane_mouth_under_torso` prompt preserved contact on both +sampler seeds and produced stable office-floor framing: + +- seed `238365845574312`: `/media/unraid/comfyui/output/agent_bridge/img_b6525cf541d44c34828c7fe19068425b.png` +- seed `238365845574313`: `/media/unraid/comfyui/output/agent_bridge/img_fe09b3d132004f26ab848f3edb21d8a0.png` + +It is better than a generic seated oral prompt because it anchors the partner +below the viewer, keeps the mouth/contact centered, and uses floor/caster/desk +anchors. It is not a proven exact top-view atlas reproduction. + +## Stable But Insufficient + +These cues mostly improved consistency rather than verticality: + +- `caster_wheel_floor_grid` +- `desk_leg_rows_floor_depth` +- `viewer_feet_gate_head_center` +- `straight_down_carpet_tiles` +- `mouth_below_navel_eye_contact` +- `kneeling_between_shoes_floor_map` +- `short_column_contact_scale` +- `desk_edges_side_floor_anchor` +- `head_crown_forehead_vertical_read` +- `office_lane_floor_perspective` +- `low_head_high_floor_ratio` + +They tend to preserve the coworking deck and subject identity, but they do not +force a flat top-view body plane. Several reduce the action to a conventional +front-facing kneeling oral pose. + +## Weak Or Rejected Axes + +- `phone_snapshot_abdomen_down` often broke mouth contact and produced a staged + kneeling pose. +- `woman_tank_top_owned_visibility` confirmed owned clothing works, but it did + not improve the top-view geometry. +- More repetitions of `nadir`, `straight-down`, `floor plane`, `viewer feet`, + and `desk-leg grid` are unlikely to solve the exact atlas pose alone. + +## Prompt Rules From This Pass + +- Keep as a provisional prompt-guide partial: `mouth directly below the viewer's + torso` plus `floor-plane-priority` office anchors. +- Keep office-floor anchors concrete: `chair caster wheels`, `desk-leg feet`, + `table bases`, `carpet texture`, `floor seams`. +- Owned clothing remains valid only when phrased as `The woman wears ...`. +- Do not promote exact top-view success from this pass. +- For catalog seed variants, this pose needs either a partial seed family + labeled as forward/downward POV oral, or stronger control/image guidance for + the flatter atlas top-view family. + diff --git a/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_batch.json new file mode 100644 index 0000000..27eba7d --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_batch.json @@ -0,0 +1,105 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "selection": { + "purpose": "second-pass top-view axis refinement using folder baseline and first MCP batch evidence", + "sampler_seed_role": "fixed sampler seed for repeatable matrix comparison" + }, + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "cue_axes": ["baseline", "folder_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. 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. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "floor_plane_mouth_under_torso", + "prompt_order": "subject_first", + "cue_axes": ["floor_plane_priority", "mouth_under_torso", "contact_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Floor-plane-priority standing male POV oral position. viewer stands above the kneeling woman and looks down from his abdomen. open shorts, thighs, feet, and lower abdomen frame the bottom. the carpet plane is the main background surface, filled with woven texture, floor seams, desk-leg feet, and caster wheels. the large penis rises from the lower center as a foreshortened vertical cylinder. the woman kneels in the middle of the carpet plane, mouth sealed around the centered large penis. both hands wrap around the base, shoulders and knees visible below her hair crown. her dark eyes angle up toward the viewer. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "caster_wheel_floor_grid", + "prompt_order": "subject_first", + "cue_axes": ["caster_wheels", "top_down_grid", "office_floor_anchor"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position on office carpet. the camera looks down past the viewer's abdomen, open shorts, thighs, and feet. chair caster wheels, desk-leg feet, table bases, and carpet seams form a visible grid across the floor. the woman kneels centered inside that floor grid between the viewer's shoes. her hair crown, forehead, shoulders, hands, and knees are seen from above. her mouth seals around the foreshortened centered large penis rising from the lower frame. both hands stack at the base under her lips. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "desk_leg_rows_floor_depth", + "prompt_order": "subject_first", + "cue_axes": ["desk_leg_rows", "vertical_depth", "floor_background"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position between coworking desk rows. viewer looks steeply downward from his torso. open shorts, thighs, feet, and lower abdomen hold the lower edge. repeated desk legs and chair wheels recede across the carpet behind the kneeling woman. the floor plane fills most of the image. the woman kneels directly below the viewer, centered between his feet. her mouth covers the short foreshortened large penis at the exact center. both hands hold the base, elbows tucked close, shoulders visible below her hair crown. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "viewer_feet_gate_head_center", + "prompt_order": "subject_first", + "cue_axes": ["viewer_feet_frame", "head_centering", "standing_height"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position. viewer's bare feet and open shorts create a gate around the bottom of the frame. the camera looks down between his thighs onto the carpet. the woman's head is centered directly below the viewer's pelvis between his feet. her hair crown and forehead face the camera from above while her dark eyes look upward. a short vertical large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. floor seams, chair wheels, and desk legs surround her knees on the carpet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "phone_snapshot_abdomen_down", + "prompt_order": "subject_first", + "cue_axes": ["phone_like_snapshot", "abdomen_down_angle", "floor_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Phone-like standing male POV oral snapshot from above. the viewer looks down from chest and abdomen height. lower abdomen, open shorts, thighs, and feet occupy the near bottom edge. the carpet below is clear and detailed, with chair wheels, desk-leg feet, and table bases placed around the woman. she kneels directly underneath the viewer between his feet. her mouth seals around the centered short large penis. both hands clasp the base and her shoulders sit below her hair crown. her dark almond eyes glance upward into the camera. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "straight_down_carpet_tiles", + "prompt_order": "subject_first", + "cue_axes": ["straight_down_axis", "carpet_tiles", "top_view"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nearly straight-down standing male POV oral position. the viewer's abdomen, shorts, thighs, and feet frame a steep view onto carpet tiles. the carpet tile seams run beneath the kneeling woman and make the camera angle read vertical. she kneels below the viewer with knees spread on the carpet. the short centered large penis points upward from the lower frame into her sealed mouth. both hands hold the base at the viewer's pelvis line. hair crown, forehead, shoulders, hands, and knees are all visible from above. desk legs and chair wheels sit flat on the floor around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "mouth_below_navel_eye_contact", + "prompt_order": "subject_first", + "cue_axes": ["mouth_below_navel", "eye_contact", "contact_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with eye contact. viewer looks down past his navel, open shorts, thighs, and feet. the woman's mouth sits directly below the viewer's navel line on the carpet plane. her lips seal around the centered foreshortened large penis and both hands clasp the base. her face tilts upward, dark almond eyes looking into the camera. the hair crown, forehead, shoulders, hands, and knees remain readable from above. carpet texture, desk legs, chair wheels, and table bases flatten into the background floor grid. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "kneeling_between_shoes_floor_map", + "prompt_order": "subject_first", + "cue_axes": ["kneeling_between_feet", "floor_map", "standing_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position. the viewer's feet sit near the lower left and lower right edges like fixed markers on the carpet. his open shorts, thighs, and lower abdomen occupy the bottom edge. the woman kneels between those feet, directly under the viewer's pelvis. her head, shoulders, hands, and knees fit inside the visible floor map. the short centered large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. office desk legs, caster wheels, carpet seams, and table bases show the flat floor orientation. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "short_column_contact_scale", + "prompt_order": "subject_first", + "cue_axes": ["contact_scale", "foreshortened_column", "anatomy_length_control"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position with foreshortened contact scale. viewer looks down from his abdomen to the carpet. lower abdomen, open shorts, thighs, and feet frame the bottom foreground. the large penis reads as a short centered vertical column because the camera is directly above it. the woman kneels below the viewer and seals her mouth around the centered tip. both hands clasp low at the base near the viewer's pelvis. her hair crown, forehead, eyes, shoulders, and knees are visible from the top angle. floor seams, desk legs, caster wheels, and chair bases stay flat around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "woman_tank_top_owned_visibility", + "prompt_order": "subject_first", + "cue_axes": ["owned_clothing", "visible_top", "pose_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. Standing male POV top-view oral position. viewer looks down from his abdomen with open shorts, thighs, and feet framing the lower edge. the woman kneels directly below the viewer on the carpet between his feet. her tank top shoulders and neckline are visible below her hair crown from above. her mouth seals around the centered short large penis. both hands wrap the base under her lips. carpet texture, floor seams, desk legs, caster wheels, and table bases surround her knees and anchor the top-down office floor. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "desk_edges_side_floor_anchor", + "prompt_order": "subject_first", + "cue_axes": ["desk_edges", "side_background", "floor_angle"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside coworking desks. viewer looks down steeply past his lower abdomen, open shorts, thighs, and feet. desktop edges and table legs appear along the side margins while the carpet fills the center. the woman kneels on the carpet directly below the viewer between his feet. her mouth seals around the short centered large penis. both hands clasp the base under her lips. her hair crown, forehead, shoulders, and knees are visible from above. chair wheels and table bases mark the floor depth around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "head_crown_forehead_vertical_read", + "prompt_order": "subject_first", + "cue_axes": ["head_crown", "forehead_visibility", "vertical_read"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position. viewer looks down over his lower abdomen, open shorts, thighs, and feet to the carpet directly below. the woman's hair crown is the topmost visible part of her head, with forehead, eyes, nose bridge, shoulders, hands, and knees stacked underneath from the steep angle. her mouth seals around the short centered large penis rising from the lower frame. both hands hold the base. the floor plane dominates the image with carpet weave, chair wheels, desk-leg feet, table bases, and seams surrounding her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "office_lane_floor_perspective", + "prompt_order": "subject_first", + "cue_axes": ["office_lane", "floor_perspective", "workspace_continuity"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV oral position in a narrow coworking floor lane. viewer looks steeply down from his abdomen. open shorts, thighs, feet, and lower torso anchor the lower foreground. a lane of carpet, chair caster wheels, table bases, and desk legs extends behind the kneeling woman. the woman is directly below the viewer in the lane, centered between his feet. her mouth seals around the short centered large penis. both hands stack at the base under her lips. hair crown, forehead, shoulders, and knees are readable from above. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "low_head_high_floor_ratio", + "prompt_order": "subject_first", + "cue_axes": ["floor_ratio", "head_below_viewer", "verticality"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_results.json b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_results.json new file mode 100644 index 0000000..0d7971c --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574312_results.json @@ -0,0 +1,111 @@ +{ + "seed": 238365845574312, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "turn": 27, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_40bd9ea31595497eacabd4bf08fc5088.png", + "returned_seed": 238365845574312 + }, + { + "id": "floor_plane_mouth_under_torso", + "prompt_order": "subject_first", + "turn": 28, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_b6525cf541d44c34828c7fe19068425b.png", + "returned_seed": 238365845574312 + }, + { + "id": "caster_wheel_floor_grid", + "prompt_order": "subject_first", + "turn": 29, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_4f1bc1b6a655401d92d883043b7af76a.png", + "returned_seed": 238365845574312 + }, + { + "id": "desk_leg_rows_floor_depth", + "prompt_order": "subject_first", + "turn": 30, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_5606d28cf60e41c7a894b3e4e667cd88.png", + "returned_seed": 238365845574312 + }, + { + "id": "viewer_feet_gate_head_center", + "prompt_order": "subject_first", + "turn": 31, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_15c47d49bd344721a598c3ab024a6278.png", + "returned_seed": 238365845574312 + }, + { + "id": "phone_snapshot_abdomen_down", + "prompt_order": "subject_first", + "turn": 32, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_d4f87fa4e43548d7af12af13a78e1cb3.png", + "returned_seed": 238365845574312 + }, + { + "id": "straight_down_carpet_tiles", + "prompt_order": "subject_first", + "turn": 33, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_3a84cc08518447a0a9b302d9bfb55191.png", + "returned_seed": 238365845574312 + }, + { + "id": "mouth_below_navel_eye_contact", + "prompt_order": "subject_first", + "turn": 34, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_984845bbd30449e3a801b40917eec708.png", + "returned_seed": 238365845574312 + }, + { + "id": "kneeling_between_shoes_floor_map", + "prompt_order": "subject_first", + "turn": 35, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_dfc98e75d16946f89e9b96eed2675993.png", + "returned_seed": 238365845574312 + }, + { + "id": "short_column_contact_scale", + "prompt_order": "subject_first", + "turn": 36, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_6bb47f36d12740a8aa9a815801988b68.png", + "returned_seed": 238365845574312 + }, + { + "id": "woman_tank_top_owned_visibility", + "prompt_order": "subject_first", + "turn": 37, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_9f6fb18a5d574237910a15847cf762ca.png", + "returned_seed": 238365845574312 + }, + { + "id": "desk_edges_side_floor_anchor", + "prompt_order": "subject_first", + "turn": 38, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_7e4432d41e6c4defb44fa8582c127764.png", + "returned_seed": 238365845574312 + }, + { + "id": "head_crown_forehead_vertical_read", + "prompt_order": "subject_first", + "turn": 39, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_31c14beb5cee41a29e15a4d2d48218b3.png", + "returned_seed": 238365845574312 + }, + { + "id": "office_lane_floor_perspective", + "prompt_order": "subject_first", + "turn": 40, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_3eb8c5b4cc6648b7ab0fd5555488d657.png", + "returned_seed": 238365845574312 + }, + { + "id": "low_head_high_floor_ratio", + "prompt_order": "subject_first", + "turn": 41, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_9f12349bc10e4dce908babe161200814.png", + "returned_seed": 238365845574312 + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_batch.json new file mode 100644 index 0000000..d12594f --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_batch.json @@ -0,0 +1,105 @@ +{ + "seed": 238365845574313, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "selection": { + "purpose": "second-pass top-view axis refinement using folder baseline and first MCP batch evidence", + "sampler_seed_role": "second fixed sampler seed for repeatable matrix comparison" + }, + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "cue_axes": ["baseline", "folder_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. 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. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "floor_plane_mouth_under_torso", + "prompt_order": "subject_first", + "cue_axes": ["floor_plane_priority", "mouth_under_torso", "contact_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Floor-plane-priority standing male POV oral position. viewer stands above the kneeling woman and looks down from his abdomen. open shorts, thighs, feet, and lower abdomen frame the bottom. the carpet plane is the main background surface, filled with woven texture, floor seams, desk-leg feet, and caster wheels. the large penis rises from the lower center as a foreshortened vertical cylinder. the woman kneels in the middle of the carpet plane, mouth sealed around the centered large penis. both hands wrap around the base, shoulders and knees visible below her hair crown. her dark eyes angle up toward the viewer. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "caster_wheel_floor_grid", + "prompt_order": "subject_first", + "cue_axes": ["caster_wheels", "top_down_grid", "office_floor_anchor"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position on office carpet. the camera looks down past the viewer's abdomen, open shorts, thighs, and feet. chair caster wheels, desk-leg feet, table bases, and carpet seams form a visible grid across the floor. the woman kneels centered inside that floor grid between the viewer's shoes. her hair crown, forehead, shoulders, hands, and knees are seen from above. her mouth seals around the foreshortened centered large penis rising from the lower frame. both hands stack at the base under her lips. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "desk_leg_rows_floor_depth", + "prompt_order": "subject_first", + "cue_axes": ["desk_leg_rows", "vertical_depth", "floor_background"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position between coworking desk rows. viewer looks steeply downward from his torso. open shorts, thighs, feet, and lower abdomen hold the lower edge. repeated desk legs and chair wheels recede across the carpet behind the kneeling woman. the floor plane fills most of the image. the woman kneels directly below the viewer, centered between his feet. her mouth covers the short foreshortened large penis at the exact center. both hands hold the base, elbows tucked close, shoulders visible below her hair crown. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "viewer_feet_gate_head_center", + "prompt_order": "subject_first", + "cue_axes": ["viewer_feet_frame", "head_centering", "standing_height"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position. viewer's bare feet and open shorts create a gate around the bottom of the frame. the camera looks down between his thighs onto the carpet. the woman's head is centered directly below the viewer's pelvis between his feet. her hair crown and forehead face the camera from above while her dark eyes look upward. a short vertical large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. floor seams, chair wheels, and desk legs surround her knees on the carpet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "phone_snapshot_abdomen_down", + "prompt_order": "subject_first", + "cue_axes": ["phone_like_snapshot", "abdomen_down_angle", "floor_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Phone-like standing male POV oral snapshot from above. the viewer looks down from chest and abdomen height. lower abdomen, open shorts, thighs, and feet occupy the near bottom edge. the carpet below is clear and detailed, with chair wheels, desk-leg feet, and table bases placed around the woman. she kneels directly underneath the viewer between his feet. her mouth seals around the centered short large penis. both hands clasp the base and her shoulders sit below her hair crown. her dark almond eyes glance upward into the camera. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "straight_down_carpet_tiles", + "prompt_order": "subject_first", + "cue_axes": ["straight_down_axis", "carpet_tiles", "top_view"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nearly straight-down standing male POV oral position. the viewer's abdomen, shorts, thighs, and feet frame a steep view onto carpet tiles. the carpet tile seams run beneath the kneeling woman and make the camera angle read vertical. she kneels below the viewer with knees spread on the carpet. the short centered large penis points upward from the lower frame into her sealed mouth. both hands hold the base at the viewer's pelvis line. hair crown, forehead, shoulders, hands, and knees are all visible from above. desk legs and chair wheels sit flat on the floor around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "mouth_below_navel_eye_contact", + "prompt_order": "subject_first", + "cue_axes": ["mouth_below_navel", "eye_contact", "contact_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position with eye contact. viewer looks down past his navel, open shorts, thighs, and feet. the woman's mouth sits directly below the viewer's navel line on the carpet plane. her lips seal around the centered foreshortened large penis and both hands clasp the base. her face tilts upward, dark almond eyes looking into the camera. the hair crown, forehead, shoulders, hands, and knees remain readable from above. carpet texture, desk legs, chair wheels, and table bases flatten into the background floor grid. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "kneeling_between_shoes_floor_map", + "prompt_order": "subject_first", + "cue_axes": ["kneeling_between_feet", "floor_map", "standing_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position. the viewer's feet sit near the lower left and lower right edges like fixed markers on the carpet. his open shorts, thighs, and lower abdomen occupy the bottom edge. the woman kneels between those feet, directly under the viewer's pelvis. her head, shoulders, hands, and knees fit inside the visible floor map. the short centered large penis rises from the lower center into her sealed mouth. both hands wrap the base under her lips. office desk legs, caster wheels, carpet seams, and table bases show the flat floor orientation. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "short_column_contact_scale", + "prompt_order": "subject_first", + "cue_axes": ["contact_scale", "foreshortened_column", "anatomy_length_control"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Steep standing male POV oral position with foreshortened contact scale. viewer looks down from his abdomen to the carpet. lower abdomen, open shorts, thighs, and feet frame the bottom foreground. the large penis reads as a short centered vertical column because the camera is directly above it. the woman kneels below the viewer and seals her mouth around the centered tip. both hands clasp low at the base near the viewer's pelvis. her hair crown, forehead, eyes, shoulders, and knees are visible from the top angle. floor seams, desk legs, caster wheels, and chair bases stay flat around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "woman_tank_top_owned_visibility", + "prompt_order": "subject_first", + "cue_axes": ["owned_clothing", "visible_top", "pose_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top. Standing male POV top-view oral position. viewer looks down from his abdomen with open shorts, thighs, and feet framing the lower edge. the woman kneels directly below the viewer on the carpet between his feet. her tank top shoulders and neckline are visible below her hair crown from above. her mouth seals around the centered short large penis. both hands wrap the base under her lips. carpet texture, floor seams, desk legs, caster wheels, and table bases surround her knees and anchor the top-down office floor. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "desk_edges_side_floor_anchor", + "prompt_order": "subject_first", + "cue_axes": ["desk_edges", "side_background", "floor_angle"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV top-view oral position beside coworking desks. viewer looks down steeply past his lower abdomen, open shorts, thighs, and feet. desktop edges and table legs appear along the side margins while the carpet fills the center. the woman kneels on the carpet directly below the viewer between his feet. her mouth seals around the short centered large penis. both hands clasp the base under her lips. her hair crown, forehead, shoulders, and knees are visible from above. chair wheels and table bases mark the floor depth around her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "head_crown_forehead_vertical_read", + "prompt_order": "subject_first", + "cue_axes": ["head_crown", "forehead_visibility", "vertical_read"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Nadir standing male POV oral position. viewer looks down over his lower abdomen, open shorts, thighs, and feet to the carpet directly below. the woman's hair crown is the topmost visible part of her head, with forehead, eyes, nose bridge, shoulders, hands, and knees stacked underneath from the steep angle. her mouth seals around the short centered large penis rising from the lower frame. both hands hold the base. the floor plane dominates the image with carpet weave, chair wheels, desk-leg feet, table bases, and seams surrounding her. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "office_lane_floor_perspective", + "prompt_order": "subject_first", + "cue_axes": ["office_lane", "floor_perspective", "workspace_continuity"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing male POV oral position in a narrow coworking floor lane. viewer looks steeply down from his abdomen. open shorts, thighs, feet, and lower torso anchor the lower foreground. a lane of carpet, chair caster wheels, table bases, and desk legs extends behind the kneeling woman. the woman is directly below the viewer in the lane, centered between his feet. her mouth seals around the short centered large penis. both hands stack at the base under her lips. hair crown, forehead, shoulders, and knees are readable from above. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "low_head_high_floor_ratio", + "prompt_order": "subject_first", + "cue_axes": ["floor_ratio", "head_below_viewer", "verticality"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks down from his abdomen so the floor takes most of the frame. lower abdomen, open shorts, thighs, and feet sit at the bottom edge. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. desk-leg feet, chair wheels, carpet seams, and table bases surround her and show the downward camera angle. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_results.json b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_results.json new file mode 100644 index 0000000..bee0b6c --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_refine_seed_238365845574313_results.json @@ -0,0 +1,111 @@ +{ + "seed": 238365845574313, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "turn": 42, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_ee2c4c4e134441e99050c066ce3e3ccd.png", + "returned_seed": 238365845574313 + }, + { + "id": "floor_plane_mouth_under_torso", + "prompt_order": "subject_first", + "turn": 43, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_fe09b3d132004f26ab848f3edb21d8a0.png", + "returned_seed": 238365845574313 + }, + { + "id": "caster_wheel_floor_grid", + "prompt_order": "subject_first", + "turn": 44, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_15a44ffbaa5647f2aa93f15a5421bf68.png", + "returned_seed": 238365845574313 + }, + { + "id": "desk_leg_rows_floor_depth", + "prompt_order": "subject_first", + "turn": 45, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_f459a958fbfa4cf4b3268b92c3f5ad4e.png", + "returned_seed": 238365845574313 + }, + { + "id": "viewer_feet_gate_head_center", + "prompt_order": "subject_first", + "turn": 46, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_13751f4e0fe54330aceaedb8fa260a16.png", + "returned_seed": 238365845574313 + }, + { + "id": "phone_snapshot_abdomen_down", + "prompt_order": "subject_first", + "turn": 47, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_387428c4386d4de68e95af66d4afc887.png", + "returned_seed": 238365845574313 + }, + { + "id": "straight_down_carpet_tiles", + "prompt_order": "subject_first", + "turn": 48, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_47825cbcc88946ca939b3b3e348a71b1.png", + "returned_seed": 238365845574313 + }, + { + "id": "mouth_below_navel_eye_contact", + "prompt_order": "subject_first", + "turn": 49, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_0d123f63cb214049ac6b1f14b2e41c59.png", + "returned_seed": 238365845574313 + }, + { + "id": "kneeling_between_shoes_floor_map", + "prompt_order": "subject_first", + "turn": 50, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_b6ea951bbd2c4210b38b2f6d1c2bebb7.png", + "returned_seed": 238365845574313 + }, + { + "id": "short_column_contact_scale", + "prompt_order": "subject_first", + "turn": 51, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_ce9b7fdf8d5a49feae653bfec93879f8.png", + "returned_seed": 238365845574313 + }, + { + "id": "woman_tank_top_owned_visibility", + "prompt_order": "subject_first", + "turn": 52, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_20edcebe86424ec0b8fa9e6f027f725f.png", + "returned_seed": 238365845574313 + }, + { + "id": "desk_edges_side_floor_anchor", + "prompt_order": "subject_first", + "turn": 53, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_d8ef57b7ca4c4b04a8c55bbcf2c626a0.png", + "returned_seed": 238365845574313 + }, + { + "id": "head_crown_forehead_vertical_read", + "prompt_order": "subject_first", + "turn": 54, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_90b9792203b74925abc913ddee696809.png", + "returned_seed": 238365845574313 + }, + { + "id": "office_lane_floor_perspective", + "prompt_order": "subject_first", + "turn": 55, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_bd754c0e15514ffc876b6a746d01268b.png", + "returned_seed": 238365845574313 + }, + { + "id": "low_head_high_floor_ratio", + "prompt_order": "subject_first", + "turn": 56, + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_08635c3c3c354716a837d0945b4b70c6.png", + "returned_seed": 238365845574313 + } + ] +} diff --git a/ab_batches/blowjob_top_down_vertical_shaft_sparse_floor_seed_238365845574312_batch.json b/ab_batches/blowjob_top_down_vertical_shaft_sparse_floor_seed_238365845574312_batch.json new file mode 100644 index 0000000..89fc1f5 --- /dev/null +++ b/ab_batches/blowjob_top_down_vertical_shaft_sparse_floor_seed_238365845574312_batch.json @@ -0,0 +1,72 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "selection": { + "purpose": "reduce office-background density while preserving the strongest straight-down floor-plan top-view oral wording", + "sampler_seed_role": "fixed sampler seed for comparison against straight_down_floorplan result" + }, + "probes": [ + { + "id": "straight_down_floorplan_control", + "prompt_order": "subject_first", + "cue_axes": ["control", "straight_down_prefix", "floorplan_scene_tail"], + "evidence": { + "prior_image": "/media/unraid/comfyui/output/agent_bridge/img_02410cd85ef14ceaa160cca11116f5cb.png" + }, + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Set in a coworking lounge seen as a top-down floor plan: carpet texture, carpet tile seams, desk-leg feet, chair caster wheels, table bases, lower desk edges, and small plant pots sit flat around the kneeling woman." + }, + { + "id": "sparse_carpet_plane", + "prompt_order": "subject_first", + "cue_axes": ["sparse_floor", "background_density", "floor_ratio"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet floor takes most of the frame. a broad sparse carpet plane surrounds the kneeling woman. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge evidence appears as carpet texture, carpet tile seams, two cropped chair caster wheels near the far edge, and one cropped table foot at the margin." + }, + { + "id": "empty_carpet_halo", + "prompt_order": "subject_first", + "cue_axes": ["empty_floor_area", "background_density", "top_view"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto a mostly open carpet floor. a large empty carpet halo surrounds the woman from above. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge floor cues are sparse carpet seams and small cropped furniture feet at the outer edge." + }, + { + "id": "edge_cropped_office_marks", + "prompt_order": "subject_first", + "cue_axes": ["edge_crops", "background_density", "workspace_floor"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the carpet floor fills the central image. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Office furniture appears as cropped edge marks: partial desk feet, partial chair caster wheels, and small table-base slivers along the frame margins." + }, + { + "id": "carpet_tile_map", + "prompt_order": "subject_first", + "cue_axes": ["carpet_grid", "floor_map", "reduced_background"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the carpet tile map takes most of the frame. carpet seams form a flat square grid around the kneeling woman. her head stays low in the center of that carpet grid between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking context reads through a few cropped caster wheels and desk feet at the grid edges." + }, + { + "id": "floor_plane_is_background", + "prompt_order": "subject_first", + "cue_axes": ["floor_as_background", "scene_reconciliation", "top_view"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the carpet floor plane is the background. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. The coworking lounge appears through flat floor details: carpet weave, carpet seams, small caster wheels, desk feet, and cropped table-base edges." + }, + { + "id": "minimal_floorplan_compact", + "prompt_order": "subject_first", + "cue_axes": ["short_prompt", "noise_reduction", "sparse_floor"], + "text": "Straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen onto the carpet floor. the woman kneels in the center of a sparse carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base. hair crown, forehead, shoulders, hands, and knees read from above. Sparse coworking floor cues: carpet seams, cropped caster wheels, cropped desk feet." + }, + { + "id": "tight_vertical_subject_sparse_floor", + "prompt_order": "subject_first", + "cue_axes": ["tight_crop", "sparse_floor", "contact_priority"], + "text": "Tight straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen. the kneeling woman and centered contact fill the lower half while sparse carpet fills the upper half. her head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Coworking lounge floor cues stay at the margins as carpet seams, caster wheels, and desk feet." + }, + { + "id": "atlas_like_floor_dominant", + "prompt_order": "subject_first", + "cue_axes": ["atlas_like_floor_ratio", "sparse_floor", "top_view"], + "text": "Atlas-like straight-down overhead view of A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. High standing male POV top-view oral position. viewer looks straight down from his abdomen so the floor plane dominates the composition. the woman's head stays low in the center of the carpet plane between the viewer's feet. her mouth seals around the centered foreshortened large penis. both hands wrap the base and her shoulders and knees remain visible below her head. Sparse coworking floor marks surround her: carpet weave, carpet seams, cropped chair wheels, and cropped table feet." + } + ] +} diff --git a/ab_batches/blowjob_top_view_1024_reference_pool_analysis.md b/ab_batches/blowjob_top_view_1024_reference_pool_analysis.md new file mode 100644 index 0000000..2b81dc5 --- /dev/null +++ b/ab_batches/blowjob_top_view_1024_reference_pool_analysis.md @@ -0,0 +1,183 @@ +# Blowjob Top-View 1024 Reference Pool Cue Expansion + +Date: 2026-07-01 + +Canonical atlas folder: + +```text +/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view +``` + +Supplemental raw cue-expansion folder: + +```text +/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/1.original/blowjob_top_view_1024 +``` + +Folder state: + +- canonical folder: 17 PNG images +- supplemental raw folder: 27 PNG images +- all images are `1024x1024` +- raw-pool contact sheet created for review at `/tmp/blowjob_top_view_1024_contact.jpg` + +Reference-pool report command: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-reference-pool-report --variant-key pov_blowjob_top_down_vertical_shaft --reference-pool-folder 1.original/blowjob_top_view_1024 +``` + +Cue-review sheet command: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-reference-cue-review-sheet --variant-key pov_blowjob_top_down_vertical_shaft --reference-pool-folder 1.original/blowjob_top_view_1024 +``` + +The cue-review sheet currently creates 27 blank review items: 17 canonical +curated references, all matched to raw counterparts by image id, plus 10 +raw-only supplemental extras. Canonical rows include a +`reference_images_template`; raw-only rows leave that template empty until a +human review decides whether the extra frame is only cue-mining evidence or +deserves promotion into the curated catalog. + +Filled cue-review sheet to sidecar-candidate draft command: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-reference-cue-candidate-draft --reference-cue-review-sheet-json /tmp/sxcp-reference-cue-review-filled.json +``` + +The candidate draft emits copyable `prompt_variant` objects only for reviewed +canonical rows with positive cues and a filled prompt-variant id. It skips +raw-only extras, noisy option/meta/negative cue wording, blank rows, and +duplicate ids so the raw pool cannot silently become promoted generator truth. + +Candidate draft to same-stem sidecar authoring draft: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-reference-cue-sidecar-author-draft --reference-cue-candidate-draft-json /tmp/sxcp-reference-cue-candidate-draft.json --variant-key pov_blowjob_top_down_vertical_shaft +``` + +Validate and apply only after confirming the target baseline deck is the one to +test: + +```bash +python tools/krea2_atlas_refine_manifest.py --validate-reference-cue-sidecar-author-draft --reference-cue-sidecar-author-draft-json /tmp/sxcp-reference-cue-sidecar-author-draft.json +python tools/krea2_atlas_refine_manifest.py --apply-reference-cue-sidecar-author-draft --reference-cue-sidecar-author-draft-json /tmp/sxcp-reference-cue-sidecar-author-draft.json --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine +``` + +Apply writes unscored sidecar prompt variants and checks the source prompt hash. +The next required action is fixed-seed MCP rendering and scoring, not catalog +promotion. + +## Why This Pool Matters + +The canonical folder is the preferred source for curated atlas references. The +supplemental raw folder shows the same pose family with more images, so it can +drive cue expansion when repeated visual axes are visible across references. It +should not be treated as proof that our current generated prompt can preserve +the same subject, coworking workspace, clothing ownership, or anatomy behavior. + +Use this pool upstream of sidecar authoring: + +1. Pick the nearest atlas cluster for the target variant, preferring the + canonical `blowjob_top_view/` path when the frame exists there. +2. Extract positive cue axes from repeated visual evidence. +3. Write small `append_cues` against the current generated baseline and store + the nearest atlas targets in `reference_images`. +4. Test with fixed sampler seeds through the MCP batch path. +5. Score generated results against the nearest atlas cluster and the current + same-subject/workspace baseline before promotion. +6. Keep raw-only extras in the cue-review sheet until repeated generated + evidence proves the cue belongs in a sidecar or catalog entry. +7. Convert only reviewed canonical rows to candidate sidecar snippets; then + render and score them before seed selection. +8. Use the sidecar authoring draft to attach reviewed candidates to a baseline + deck with prompt-hash drift protection. + +## Observed Cue Axes + +- `camera_pitch`: straight-down vertical, high oblique top-down, and tilted + top-down variants all appear in the pool. +- `support_plane`: white lounge/chair surfaces, pale floor, carpet/rug, bed or + blanket surfaces, wood floor, tile floor, and outdoor ground appear as visible + plane anchors. +- `viewer_foreground_amount`: some refs use only a lower torso edge, while + others show more thighs, feet, waistband, or abdomen mass. +- `partner_upper_body_stack`: the strongest top-view refs place face, eyes or + eyelids, hair crown, shoulders, upper chest or neckline, and hand contact as + the main partner stack. +- `hand_placement`: one hand at the base, both hands supporting the base, hands + on floor/support, and hands on the viewer/body edge appear as alternate frame + cues. +- `eye_direction`: direct eye contact, eyelids lowered, and open-mouth/gaze-up + variants appear as expression axes. +- `clothing_anchor`: fitted tops, straps, shirts, or visible necklines can help + anchor upper-body geometry when the crop supports them. +- `floor_furniture_evidence`: cropped support edges, floor seams, rugs, chair or + furniture edges, and outdoor objects can carry scene identity without forcing + deep room perspective. + +## Current Top-View Rule From This Pool + +For coworking prompt tests, translate workspace context into floor/support-plane +evidence and keep the shaft/contact line as the first pose anchor. The +user-highlighted atlas-22-style direction is: + +```text +Straight-down male POV oral close-up. The centered shaft and mouth contact form +the main vertical axis from the lower foreground to the woman's face. The +woman's face, eyelids, hair crown, shoulders, upper chest, neckline, and one +hand stack around the shaft-contact axis. Viewer thighs and feet frame the +lower side edges. Tucked knees remain small side shapes on the floor. The +background reads as a flat pale floor and one cropped white lounge chair +surface, with shallow top-down room depth. +``` + +Manual same-seed calibration on 2026-07-01 confirms the anchor order. The +sidecars for `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00135_.png` +through `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00139_.png` +performed best when the prompt started with straight-down POV, then the shaft, +then the partner stack directly below the shaft, then mouth contact, and ended +with a sparse flat-floor background. Treat abdomen and room-depth cues as +secondary evidence; they should follow the shaft-contact axis instead of +leading the composition. + +Because this finding depends on word order and removes the deep coworking-room +tail, test it as exact replacement text instead of appended cues. The validated +dry-run batch is: + +```text +/tmp/sxcp_top_view_oral_shaft_anchor_exact_batch.json +``` + +It keeps the original baseline probe plus two exact-text candidates: + +- `atlas22_shaft_contact_upper_stack_floor_plane` +- `atlas27_shaft_axis_between_feet_floor_anchors` + +For clothed variants, keep clothing subject-owned: + +```text +The woman wears a fitted white ribbed tank top; the tank-top neckline and +shoulders remain visible from above. +``` + +## Promotion Guard + +Do not copy all 27 supplemental references into the live catalog variant. Keep +the catalog reference list curated, then use the raw pool to choose cue axes and +nearest visual targets. A generated candidate still needs fixed-seed image +evidence and visual scores for pose ownership, workspace continuity, clothing +visibility, subject identity, expression/eye control, anatomy/proportion, and +prompt noise before it can become seedable. Sidecar cue variants should carry +nearest visual targets like: + +```json +{ + "id": "shaft_contact_upper_stack_floor_support", + "text": "A same-subject straight-down male POV oral close-up. The centered shaft and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman's face, eyelids, hair crown, shoulders, upper chest, neckline, and one hand stack around the shaft-contact axis. The background reads as a flat pale floor plane with shallow overhead room depth.", + "reference_images": [ + "blowjob_top_view/22_blowjob_top_view.png" + ] +} +``` diff --git a/ab_batches/footjob_frontal_sole_stroke_microposition_seed_238365845574312_batch.json b/ab_batches/footjob_frontal_sole_stroke_microposition_seed_238365845574312_batch.json new file mode 100644 index 0000000..d8d1504 --- /dev/null +++ b/ab_batches/footjob_frontal_sole_stroke_microposition_seed_238365845574312_batch.json @@ -0,0 +1,105 @@ +{ + "seed": 238365845574312, + "channel_out": "sxcp_eval_out", + "channel_in": "sxcp_eval_in", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_footjob_frontal_sole_stroke", + "source_entry_id": "pov_footjob_frontal_sole_stroke_00001", + "source_stem": "pov_footjob_frontal_sole_stroke_00001_", + "selection": { + "purpose": "controlled same-pose micro-position cue exploration for seedable atlas alternatives", + "sampler_seed_role": "fixed sampler seed for first-pass micro-position comparison" + }, + "probes": [ + { + "id": "baseline_ref_folder", + "prompt_order": "subject_first", + "cue_axes": ["baseline", "folder_reference"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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 large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face and torso stay visible behind the large foreground feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "soles_closer_to_lens", + "prompt_order": "subject_first", + "cue_axes": ["foot_distance", "foreground_scale", "sole_dominance"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two large soles push closer to the lens and dominate the lower center foreground. inner arches press inward around the upright large penis. toes curl around both sides near the glans. a narrow centered strip of large penis rises between the compressed feet. woman's face and torso remain visible behind the enlarged foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "soles_lower_more_column_visible", + "prompt_order": "subject_first", + "cue_axes": ["foot_distance", "contact_visibility", "column_visibility"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. both soles press lower on the shaft and leave more of the upright large penis visible above the feet. inner arches squeeze inward at the middle. toes curl around the sides below the glans. woman's face and torso stay visible behind the paired feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "toes_wrap_over_tip", + "prompt_order": "subject_first", + "cue_axes": ["toe_position", "glans_contact", "micro_contact"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. two foreground soles press together around the upright large penis. her toes curl over the top edges near the glans. inner arches squeeze the shaft from both sides. the glans peeks between the curled toes and compressed soles. woman's face and torso stay visible behind the large feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "left_sole_forward_right_sole_back", + "prompt_order": "subject_first", + "cue_axes": ["foot_asymmetry", "same_pose_frame", "depth_variation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her left sole sits slightly closer to the lens while her right sole sits a little farther back. the paired arches still squeeze around the upright large penis. toes curl along both edges. a centered strip of large penis remains visible between the offset feet. woman's face and torso stay visible behind the asymmetrical foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "right_sole_forward_left_sole_back", + "prompt_order": "subject_first", + "cue_axes": ["foot_asymmetry", "same_pose_frame", "depth_variation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her right sole sits slightly closer to the lens while her left sole sits a little farther back. the paired arches squeeze around the upright large penis. toes curl along both edges. a centered strip of large penis remains visible between the offset feet. woman's face and torso stay visible behind the asymmetrical foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "v_angle_arch_press", + "prompt_order": "subject_first", + "cue_axes": ["foot_angle", "arch_pressure", "contact_shape"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her soles form a tight V shape around the upright large penis. inner arches press inward at the center and the toes angle outward along the top edges. a narrow vertical strip of large penis and glans rises between the angled soles. woman's face and torso stay visible behind the foot frame. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "flat_parallel_soles", + "prompt_order": "subject_first", + "cue_axes": ["foot_angle", "parallel_contact", "controlled_variant"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. both soles face the camera almost flat and parallel. the upright large penis is centered between the parallel soles. inner arches press evenly inward from left and right. toes curl around the upper edges while her face and torso stay visible behind the large foreground feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "ankles_wide_soles_squeeze_center", + "prompt_order": "subject_first", + "cue_axes": ["ankle_position", "leg_spread", "center_squeeze"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. her ankles stay wide apart while the soles angle inward to squeeze the upright large penis at the center. toes curl around both sides. the compressed feet frame a narrow visible strip of shaft and glans. woman's face, torso, and spread thighs stay visible behind the large foot foreground. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "knees_higher_foot_frame", + "prompt_order": "subject_first", + "cue_axes": ["knee_position", "foot_frame_height", "body_position"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with knees lifted higher beside her torso. the raised knees frame the large foreground soles. her soles press together around the upright large penis at the center. toes curl around the sides near the glans. woman's face and torso stay visible above the higher knee frame. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "woman_looking_at_viewer", + "prompt_order": "subject_first", + "cue_axes": ["gaze_control", "facial_expression", "pose_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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 large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face and torso stay visible behind the large feet, her dark almond eyes looking toward the viewer with a focused soft expression. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "woman_looking_down_at_feet", + "prompt_order": "subject_first", + "cue_axes": ["gaze_control", "facial_expression", "pose_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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 large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face and torso stay visible behind the large feet, her dark eyes looking down at her own feet with a calm focused expression. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "woman_white_tank_top_visible", + "prompt_order": "subject_first", + "cue_axes": ["owned_clothing", "clothing_visibility", "pose_preservation"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. The woman wears a fitted white ribbed tank top visible behind the raised soles. 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 large penis. toes curl around both edges. narrow visible strip of large penis and glans rises between the compressed feet. woman's face, tank top, and torso stay visible behind the large foreground feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "seat_edge_lounge_support", + "prompt_order": "subject_first", + "cue_axes": ["workspace_interaction", "support_surface", "scene_continuity"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position on a coworking lounge seat. viewer reclines with thighs framing the lower foreground. woman sits opposite on the seat edge facing him with legs open toward the camera. the cushion edge appears below her hips while desk rows, laptop tables, chair wheels, and glass partitions stay in the background. two large overlapping soles dominate the lower center foreground. inner arches squeeze around the upright large penis and toes curl around both edges. woman's face and torso stay visible behind the raised feet. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + }, + { + "id": "low_table_side_anchor", + "prompt_order": "subject_first", + "cue_axes": ["workspace_interaction", "side_anchor", "scene_continuity"], + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. POV footjob position beside a low coworking table. viewer reclines with thighs framing the lower foreground. woman sits opposite facing him with legs open toward the camera. a low laptop table and chair wheels sit along the side of the frame while the action stays centered. two large overlapping soles dominate the lower center foreground. inner arches squeeze around the upright large penis. toes curl around both edges and a narrow visible strip of shaft and glans rises between the feet. woman's face and torso stay visible behind the foreground soles. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth." + } + ] +} diff --git a/ab_batches/top_view_oral_reference_cue_review_filled_v1.json b/ab_batches/top_view_oral_reference_cue_review_filled_v1.json new file mode 100644 index 0000000..dca69b9 --- /dev/null +++ b/ab_batches/top_view_oral_reference_cue_review_filled_v1.json @@ -0,0 +1,116 @@ +{ + "schema": "sxcp_atlas_reference_cue_review_sheet_v1", + "variant_key": "pov_blowjob_top_down_vertical_shaft", + "review_items": [ + { + "id": "22", + "role": "catalog_reference", + "canonical_image": "blowjob_top_view/22_blowjob_top_view.png", + "supplemental_image": "1.original/blowjob_top_view_1024/22.png", + "reference_images_template": [ + "blowjob_top_view/22_blowjob_top_view.png" + ], + "cue_axes": { + "contact_depth": "shaft_contact_axis_centered_from_lower_foreground_to_mouth", + "hand_position": "one_hand_base_contact", + "foot_position": "", + "body_angle": "compact_kneeling_upper_body_stack_below_shaft", + "camera_height": "straight_down_overhead_shaft_first", + "workspace_surface": "flat_pale_floor_with_cropped_support_edge_shallow_depth", + "clothing_visibility": "upper_body_neckline_anchor", + "expression_eye_detail": "upward_face_or_lowered_eyelids_visible_from_above", + "anatomy_shape_detail": "small_tucked_knees_beside_upper_body_stack" + }, + "observed_positive_cues": [ + "the centered shaft and mouth contact form the main vertical axis from the lower foreground to the woman's face", + "the woman's face, eyelids, hair crown, shoulders, upper chest, neckline, and one hand stack around the shaft-contact axis", + "a flat pale floor plane and one cropped support edge fill the background as shallow overhead room evidence" + ], + "rejected_cues": [], + "review_notes": "Atlas 22 anchors the strongest shaft-contact axis, upper-body stack, and shallow top-down support-plane read.", + "prompt_variant_template": { + "id": "atlas22_shaft_contact_upper_stack_floor_plane", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Straight-down male POV oral close-up. The centered large penis and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman kneels directly below the penis. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, upper chest, neckline, and one hand stack around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Tucked knees sit small on the floor beside the upper-body stack. Background reads as a flat pale floor plane with one cropped support edge and shallow overhead room depth.", + "append_cues": [], + "reference_images": [ + "blowjob_top_view/22_blowjob_top_view.png" + ], + "cue_axes": { + "contact_depth": null, + "hand_position": null, + "foot_position": null, + "body_angle": null, + "camera_height": null, + "workspace_surface": null, + "clothing_visibility": null, + "expression_eye_detail": null, + "anatomy_shape_detail": null + }, + "seed_metadata": { + "sampler_seed": null, + "generator_seed": null, + "atlas_cue_seed": null, + "micro_position_seed": null, + "workspace_seed": null + }, + "notes": "Reviewed from canonical atlas 22; test as shaft-contact axis, upper-body stack, and sparse floor-plane cue." + } + }, + { + "id": "27", + "role": "catalog_reference", + "canonical_image": "blowjob_top_view/27_blowjob_top_view.png", + "supplemental_image": "1.original/blowjob_top_view_1024/27.png", + "reference_images_template": [ + "blowjob_top_view/27_blowjob_top_view.png" + ], + "cue_axes": { + "contact_depth": "shaft_contact_axis_centered_between_viewer_feet", + "hand_position": "one_hand_base_contact", + "foot_position": "viewer_feet_lower_side_anchors", + "body_angle": "directly_below_viewer_between_feet", + "camera_height": "standing_nadir_overhead_shaft_first", + "workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors", + "clothing_visibility": "upper_body_clothing_visible_from_above", + "expression_eye_detail": "face_looking_up_from_below_camera", + "anatomy_shape_detail": "head_and_shoulders_closer_than_knees" + }, + "observed_positive_cues": [ + "the centered shaft rises from the lower foreground and points directly to the woman's mouth between the viewer's feet", + "viewer thighs and feet frame the lower side edges while the shaft-contact line stays centered", + "cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors" + ], + "rejected_cues": [], + "review_notes": "Atlas 27 carries stronger standing-nadir verticality, shaft-first alignment, and edge furniture anchors.", + "prompt_variant_template": { + "id": "atlas27_shaft_axis_between_feet_floor_anchors", + "prompt_order": "subject_first", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors.", + "append_cues": [], + "reference_images": [ + "blowjob_top_view/27_blowjob_top_view.png" + ], + "cue_axes": { + "contact_depth": null, + "hand_position": null, + "foot_position": null, + "body_angle": null, + "camera_height": null, + "workspace_surface": null, + "clothing_visibility": null, + "expression_eye_detail": null, + "anatomy_shape_detail": null + }, + "seed_metadata": { + "sampler_seed": null, + "generator_seed": null, + "atlas_cue_seed": null, + "micro_position_seed": null, + "workspace_seed": null + }, + "notes": "Reviewed from canonical atlas 27; test as standing-nadir shaft axis between the viewer's feet." + } + } + ] +} diff --git a/ab_batches/top_view_oral_shaft_anchor_exact_promotion_report_v1.json b/ab_batches/top_view_oral_shaft_anchor_exact_promotion_report_v1.json new file mode 100644 index 0000000..5a6c3ca --- /dev/null +++ b/ab_batches/top_view_oral_shaft_anchor_exact_promotion_report_v1.json @@ -0,0 +1,133 @@ +{ + "baseline_probe_id": "pov_blowjob_top_down_vertical_shaft_00001__baseline", + "blocked_count": 1, + "candidate_count": 2, + "candidates": [ + { + "analysis_notes": "Turn 90 strongly improves shaft/contact verticality and upper-body stack over baseline. It loses most coworking workspace identity, so keep it as shaft-anchor evidence rather than seedable workspace-continuity evidence.", + "blockers": [ + "workspace_continuity=partial" + ], + "cue_axes": { + "anatomy_shape_detail": "small_tucked_knees_beside_upper_body_stack", + "body_angle": "compact_kneeling_upper_body_stack_below_shaft", + "camera_height": "straight_down_overhead_shaft_first", + "clothing_visibility": "upper_body_neckline_anchor", + "contact_depth": "shaft_contact_axis_centered_from_lower_foreground_to_mouth", + "expression_eye_detail": "upward_face_or_lowered_eyelids_visible_from_above", + "foot_position": null, + "hand_position": "one_hand_base_contact", + "workspace_surface": "flat_pale_floor_with_cropped_support_edge_shallow_depth" + }, + "decision": "rejected", + "id": "pov_blowjob_top_down_vertical_shaft_00001__atlas22_shaft_contact_upper_stack_floor_plane", + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_cca7cd5c69894cbb899f94b172faa5d1.png", + "prompt_order": "subject_first", + "prompt_source": { + "kind": "text", + "prompt_variant_id": "atlas22_shaft_contact_upper_stack_floor_plane", + "tested_text_sha256": "49495cc80d3ea87a49b46b20aa716b4e07ebc3179617b6252c310bcde7788848" + }, + "prompt_variant_id": "atlas22_shaft_contact_upper_stack_floor_plane", + "reference_images": [ + "blowjob_top_view/22_blowjob_top_view.png" + ], + "score": { + "anatomy_proportion": "pass", + "atlas_pose_match": "pass", + "clothing_visibility": "pass", + "contact_match": "pass", + "expression_eye_control": "partial", + "pose_ownership": "pass", + "prompt_noise": "pass", + "subject_identity": "pass", + "workspace_continuity": "partial" + }, + "seed": 238365845574312, + "seed_metadata": { + "atlas_cue_seed": null, + "generator_seed": null, + "micro_position_seed": null, + "sampler_seed": 238365845574312, + "workspace_seed": null + }, + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Straight-down male POV oral close-up. The centered large penis and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman kneels directly below the penis. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, upper chest, neckline, and one hand stack around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Tucked knees sit small on the floor beside the upper-body stack. Background reads as a flat pale floor plane with one cropped support edge and shallow overhead room depth.", + "turn": 90, + "variant_key": "pov_blowjob_top_down_vertical_shaft" + }, + { + "analysis_notes": "Turn 91 is the best same-seed controlled candidate in this batch: shaft/contact axis holds, the woman remains between the viewer legs, and cropped desk/chair/carpet anchors preserve top-down coworking evidence without returning to deep lounge perspective.", + "blockers": [], + "cue_axes": { + "anatomy_shape_detail": "head_and_shoulders_closer_than_knees", + "body_angle": "directly_below_viewer_between_feet", + "camera_height": "standing_nadir_overhead_shaft_first", + "clothing_visibility": "upper_body_clothing_visible_from_above", + "contact_depth": "shaft_contact_axis_centered_between_viewer_feet", + "expression_eye_detail": "face_looking_up_from_below_camera", + "foot_position": "viewer_feet_lower_side_anchors", + "hand_position": "one_hand_base_contact", + "workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors" + }, + "decision": "seedable_candidate", + "id": "pov_blowjob_top_down_vertical_shaft_00001__atlas27_shaft_axis_between_feet_floor_anchors", + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_a2341f39a45e41ed96b93dcc5a89d372.png", + "prompt_order": "subject_first", + "prompt_source": { + "kind": "text", + "prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors", + "tested_text_sha256": "041e07abe9389df875267f5aa76d94bb9387f9d232ca4331e1e2efcadf2de497" + }, + "prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors", + "reference_images": [ + "blowjob_top_view/27_blowjob_top_view.png" + ], + "score": { + "anatomy_proportion": "pass", + "atlas_pose_match": "pass", + "clothing_visibility": "pass", + "contact_match": "pass", + "expression_eye_control": "partial", + "pose_ownership": "pass", + "prompt_noise": "pass", + "subject_identity": "pass", + "workspace_continuity": "pass" + }, + "seed": 238365845574312, + "seed_metadata": { + "atlas_cue_seed": null, + "generator_seed": null, + "micro_position_seed": null, + "sampler_seed": 238365845574312, + "workspace_seed": null + }, + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors.", + "turn": 91, + "variant_key": "pov_blowjob_top_down_vertical_shaft" + } + ], + "promotion_ready_count": 1, + "required_pass_keys": [ + "pose_ownership", + "workspace_continuity", + "clothing_visibility", + "subject_identity", + "prompt_noise" + ], + "required_progress_keys": [ + "atlas_pose_match", + "contact_match", + "expression_eye_control", + "anatomy_proportion" + ], + "schema": "sxcp_atlas_refine_promotion_report_v1", + "seed": 238365845574312, + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft" +} diff --git a/ab_batches/top_view_oral_shaft_anchor_exact_result_sheet_scored_v1.json b/ab_batches/top_view_oral_shaft_anchor_exact_result_sheet_scored_v1.json new file mode 100644 index 0000000..a5d23f8 --- /dev/null +++ b/ab_batches/top_view_oral_shaft_anchor_exact_result_sheet_scored_v1.json @@ -0,0 +1,173 @@ +{ + "baseline_probe_id": "pov_blowjob_top_down_vertical_shaft_00001__baseline", + "channel_in": "sxcp_eval_in", + "notes": "Same-seed shaft-first exact-text top-view oral calibration. Baseline turn 89 keeps deep coworking perspective; candidate turn 90 improves shaft-axis but loses workspace anchors; candidate turn 91 best preserves shaft-axis and top-down workspace edge anchors.", + "probe_count": 3, + "probes": [ + { + "analysis_notes": "Baseline turn 89 keeps subject and contact, but deep coworking-room perspective and abdomen-first foreground make it read less like the atlas top-view family.", + "cue_axes": { + "anatomy_shape_detail": null, + "body_angle": null, + "camera_height": null, + "clothing_visibility": null, + "contact_depth": null, + "expression_eye_detail": null, + "foot_position": null, + "hand_position": null, + "workspace_surface": null + }, + "id": "pov_blowjob_top_down_vertical_shaft_00001__baseline", + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_24af2d34756248cca358567f2a692891.png", + "prompt_order": "subject_first", + "prompt_source": { + "kind": "baseline", + "tested_text_sha256": "1d0b95d9865d1a502fb91bc856b3ff4baf00da90248c47d45631fb512f58a463" + }, + "returned_seed": 238365845574312, + "score": { + "anatomy_proportion": null, + "atlas_pose_match": null, + "clothing_visibility": null, + "contact_match": null, + "expression_eye_control": null, + "pose_ownership": null, + "prompt_noise": null, + "subject_identity": null, + "workspace_continuity": null + }, + "seed_metadata": { + "atlas_cue_seed": null, + "generator_seed": null, + "micro_position_seed": null, + "sampler_seed": 238365845574312, + "workspace_seed": null + }, + "selection": {}, + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. 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. large penis is a short centered vertical column. the woman kneels directly below the viewer between his feet. her mouth seals around the centered large penis. one hand wraps the base. 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. Camera is the male participant's first-person view in one continuous frame, with foreground body cues anchoring the lower frame. Set in coworking lounge with tall windows, warm desks, laptop tables, glass partition seams, repeated desk rows, plants, and soft shared-office depth.", + "turn": 89, + "variant_key": "pov_blowjob_top_down_vertical_shaft" + }, + { + "analysis_notes": "Turn 90 strongly improves shaft/contact verticality and upper-body stack over baseline. It loses most coworking workspace identity, so keep it as shaft-anchor evidence rather than seedable workspace-continuity evidence.", + "cue_axes": { + "anatomy_shape_detail": "small_tucked_knees_beside_upper_body_stack", + "body_angle": "compact_kneeling_upper_body_stack_below_shaft", + "camera_height": "straight_down_overhead_shaft_first", + "clothing_visibility": "upper_body_neckline_anchor", + "contact_depth": "shaft_contact_axis_centered_from_lower_foreground_to_mouth", + "expression_eye_detail": "upward_face_or_lowered_eyelids_visible_from_above", + "foot_position": null, + "hand_position": "one_hand_base_contact", + "workspace_surface": "flat_pale_floor_with_cropped_support_edge_shallow_depth" + }, + "id": "pov_blowjob_top_down_vertical_shaft_00001__atlas22_shaft_contact_upper_stack_floor_plane", + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_cca7cd5c69894cbb899f94b172faa5d1.png", + "prompt_order": "subject_first", + "prompt_source": { + "kind": "text", + "prompt_variant_id": "atlas22_shaft_contact_upper_stack_floor_plane", + "tested_text_sha256": "49495cc80d3ea87a49b46b20aa716b4e07ebc3179617b6252c310bcde7788848" + }, + "reference_images": [ + "blowjob_top_view/22_blowjob_top_view.png" + ], + "returned_seed": 238365845574312, + "score": { + "pose_ownership": "pass", + "workspace_continuity": "partial", + "clothing_visibility": "pass", + "subject_identity": "pass", + "prompt_noise": "pass", + "atlas_pose_match": "pass", + "contact_match": "pass", + "expression_eye_control": "partial", + "anatomy_proportion": "pass" + }, + "seed_metadata": { + "atlas_cue_seed": null, + "generator_seed": null, + "micro_position_seed": null, + "sampler_seed": 238365845574312, + "workspace_seed": null + }, + "selection": {}, + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Straight-down male POV oral close-up. The centered large penis and mouth contact form the main vertical axis from the lower foreground to the woman's face. The woman kneels directly below the penis. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, upper chest, neckline, and one hand stack around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Tucked knees sit small on the floor beside the upper-body stack. Background reads as a flat pale floor plane with one cropped support edge and shallow overhead room depth.", + "turn": 90, + "variant_key": "pov_blowjob_top_down_vertical_shaft" + }, + { + "analysis_notes": "Turn 91 is the best same-seed controlled candidate in this batch: shaft/contact axis holds, the woman remains between the viewer legs, and cropped desk/chair/carpet anchors preserve top-down coworking evidence without returning to deep lounge perspective.", + "cue_axes": { + "anatomy_shape_detail": "head_and_shoulders_closer_than_knees", + "body_angle": "directly_below_viewer_between_feet", + "camera_height": "standing_nadir_overhead_shaft_first", + "clothing_visibility": "upper_body_clothing_visible_from_above", + "contact_depth": "shaft_contact_axis_centered_between_viewer_feet", + "expression_eye_detail": "face_looking_up_from_below_camera", + "foot_position": "viewer_feet_lower_side_anchors", + "hand_position": "one_hand_base_contact", + "workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors" + }, + "id": "pov_blowjob_top_down_vertical_shaft_00001__atlas27_shaft_axis_between_feet_floor_anchors", + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_a2341f39a45e41ed96b93dcc5a89d372.png", + "prompt_order": "subject_first", + "prompt_source": { + "kind": "text", + "prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors", + "tested_text_sha256": "041e07abe9389df875267f5aa76d94bb9387f9d232ca4331e1e2efcadf2de497" + }, + "reference_images": [ + "blowjob_top_view/27_blowjob_top_view.png" + ], + "returned_seed": 238365845574312, + "score": { + "pose_ownership": "pass", + "workspace_continuity": "pass", + "clothing_visibility": "pass", + "subject_identity": "pass", + "prompt_noise": "pass", + "atlas_pose_match": "pass", + "contact_match": "pass", + "expression_eye_control": "partial", + "anatomy_proportion": "pass" + }, + "seed_metadata": { + "atlas_cue_seed": null, + "generator_seed": null, + "micro_position_seed": null, + "sampler_seed": 238365845574312, + "workspace_seed": null + }, + "selection": {}, + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors.", + "turn": 91, + "variant_key": "pov_blowjob_top_down_vertical_shaft" + } + ], + "schema": "sxcp_atlas_refine_result_sheet_v1", + "score_keys": [ + "atlas_pose_match", + "contact_match", + "pose_ownership", + "workspace_continuity", + "clothing_visibility", + "subject_identity", + "expression_eye_control", + "anatomy_proportion", + "prompt_noise" + ], + "seed": 238365845574312, + "selection": {}, + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_prompt_sha256": "1d0b95d9865d1a502fb91bc856b3ff4baf00da90248c47d45631fb512f58a463", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "subject_id": "atlas_refine_same_woman_001", + "variant_key": "pov_blowjob_top_down_vertical_shaft" +} diff --git a/ab_batches/top_view_oral_shaft_anchor_exact_sidecar_update_draft_v1.json b/ab_batches/top_view_oral_shaft_anchor_exact_sidecar_update_draft_v1.json new file mode 100644 index 0000000..9dc17b4 --- /dev/null +++ b/ab_batches/top_view_oral_shaft_anchor_exact_sidecar_update_draft_v1.json @@ -0,0 +1,70 @@ +{ + "ready_candidate_count": 1, + "schema": "sxcp_atlas_refine_sidecar_update_draft_v1", + "seed": 238365845574312, + "skipped_candidate_count": 1, + "subject_id": "atlas_refine_same_woman_001", + "update_count": 1, + "updates": [ + { + "prompt_variants": [ + { + "cue_axes": { + "anatomy_shape_detail": "head_and_shoulders_closer_than_knees", + "body_angle": "directly_below_viewer_between_feet", + "camera_height": "standing_nadir_overhead_shaft_first", + "clothing_visibility": "upper_body_clothing_visible_from_above", + "contact_depth": "shaft_contact_axis_centered_between_viewer_feet", + "expression_eye_detail": "face_looking_up_from_below_camera", + "foot_position": "viewer_feet_lower_side_anchors", + "hand_position": "one_hand_base_contact", + "workspace_surface": "floor_texture_with_cropped_furniture_edge_anchors" + }, + "evidence": { + "image_path": "/media/unraid/comfyui/output/agent_bridge/img_a2341f39a45e41ed96b93dcc5a89d372.png", + "reference_images": [ + "blowjob_top_view/27_blowjob_top_view.png" + ], + "score": { + "anatomy_proportion": "pass", + "atlas_pose_match": "pass", + "clothing_visibility": "pass", + "contact_match": "pass", + "expression_eye_control": "partial", + "pose_ownership": "pass", + "prompt_noise": "pass", + "subject_identity": "pass", + "workspace_continuity": "pass" + }, + "seed": 238365845574312, + "turn": 91 + }, + "id": "atlas27_shaft_axis_between_feet_floor_anchors", + "notes": "Turn 91 is the best same-seed controlled candidate in this batch: shaft/contact axis holds, the woman remains between the viewer legs, and cropped desk/chair/carpet anchors preserve top-down coworking evidence without returning to deep lounge perspective.", + "prompt_order": "subject_first", + "prompt_source": { + "kind": "text", + "prompt_variant_id": "atlas27_shaft_axis_between_feet_floor_anchors", + "tested_text_sha256": "041e07abe9389df875267f5aa76d94bb9387f9d232ca4331e1e2efcadf2de497" + }, + "reference_images": [ + "blowjob_top_view/27_blowjob_top_view.png" + ], + "seed_metadata": { + "atlas_cue_seed": null, + "generator_seed": null, + "micro_position_seed": null, + "sampler_seed": 238365845574312, + "workspace_seed": null + }, + "text": "A 23-year-old adult woman, slim busty figure with full, sculpted hips and a small waist, light Korean East Asian-European mixed skin, soft black bob, dark almond eyes. Standing nadir male POV oral close-up. The centered large penis rises from the lower foreground and points directly to the woman's mouth between the viewer's feet. The woman kneels directly below the viewer between his feet. Her mouth seals around the centered large penis. Her face, eyes, hair crown, shoulders, hands, and knees are visible from above around the penis-contact axis. One hand wraps the base. Viewer thighs and feet frame the lower side edges. Cropped desk legs, chair wheels, carpet texture, and floor seams sit near the frame edges as top-down workspace anchors." + } + ], + "sidecar_filename": "pov_blowjob_top_down_vertical_shaft_00001_.json", + "source_entry_id": "pov_blowjob_top_down_vertical_shaft_00001", + "source_stem": "pov_blowjob_top_down_vertical_shaft_00001_", + "variant_key": "pov_blowjob_top_down_vertical_shaft" + } + ], + "variant_key": "pov_blowjob_top_down_vertical_shaft" +} diff --git a/categories/krea2_pov_pose_variants.json b/categories/krea2_pov_pose_variants.json index 5449819..2ede589 100644 --- a/categories/krea2_pov_pose_variants.json +++ b/categories/krea2_pov_pose_variants.json @@ -413,6 +413,8 @@ "literal plumb-line or map wording that renders as drawn graphics" ], "reference_images": [ + "blowjob_top_view/22_blowjob_top_view.png", + "blowjob_top_view/27_blowjob_top_view.png", "blowjob_top_view/102_blowjob_top_view.png", "blowjob_top_view/2_blowjob_top_view.png", "blowjob_top_view/85_blowjob_top_view.png" diff --git a/docs/krea2-ab-methodology.md b/docs/krea2-ab-methodology.md index 4b0c848..3734092 100644 --- a/docs/krea2-ab-methodology.md +++ b/docs/krea2-ab-methodology.md @@ -5,7 +5,7 @@ Update it whenever the testing method improves. ## Current Method -Version: `2026-06-30-generated-route-validation-positive-channel-cleanup` +Version: `2026-07-01-top-view-shaft-anchor-calibration` 1. Pull or construct the baseline from an actual SxCP/CodexMCPTest source case. 2. Keep the sampler seed fixed across the baseline and candidate. @@ -69,6 +69,102 @@ Version: `2026-06-30-generated-route-validation-positive-channel-cleanup` changes, collect prompt/image evidence across multiple women, source cases, and seeds when feasible, then patch only the repeated generator-safe hierarchy. Keep early wins as prompt-guide or provisional evidence. +18. Treat atlas prompt restore as a constrained final-prompt operation, not a + free re-add of removed generator axes. Restored detail must support the + atlas hierarchy without adding a second body/camera cue, hidden garment, or + ambiguous subject. For clothing restore, keep softcore-continuity ownership + explicit: use woman-owned wording such as `the woman wears ...` for visible + clothes, keep partial-removal state when useful, and describe hidden lower + garments as out of frame instead of naming visible shorts/pants that the + atlas pose should not show. Strip raw `POV foreground clothing cue` or + `POV foreground body cue` text from strict atlas prompts because it can make + Krea2 assign clothing or body ownership to the viewer/man. +19. Use same-subject atlas refine decks before broad generator edits whenever + possible. A deck such as + `/media/unraid/comfyui/output/CodexMCP-Atlas-Refine` keeps the visible woman + constant across atlas variants, so prompt/cue changes can be scored against + pose ownership, workspace continuity, clothing visibility, and anatomy + behavior without confusing subject drift for prompt behavior. +20. After the hard-pose exploration budget is met, separate repeatable partials + from exact atlas hits. For `pov_blowjob_top_down_vertical_shaft`, 51 + text-only prompt/seed outcomes preserved contact and coworking continuity + but repeatedly collapsed toward a forward/downward kneeling oral frame + instead of the flatter atlas top view. Keep `mouth directly below the + viewer's torso` plus `floor-plane-priority` as a provisional partial, and + mark the exact flatter atlas family as needing stronger control/image + guidance before more synonym-only prompt probes. +21. Before adding more target-pose words, do conflict analysis on clauses that + fight the pose. A working prompt can still be dragged away by scene, + clothing, foreground-body, camera-layout, or background-depth clauses that + imply the wrong geometry. For overhead/top-view oral in a coworking lounge, + the generic lounge tail with windows, repeated desk rows, and soft depth + fights the atlas angle; rewrite the scene as sparse floor-plan evidence + such as carpet texture and carpet tile seams before adding more `vertical` + synonyms. The refinement loop is: keep the winning action/pose hierarchy + fixed, remove or compress conflicting clauses, render, then only add back + the smallest visible scene detail that still supports the target camera. +22. Do not generalize conflict analysis into a blanket “remove details” rule. + Compare the atlas frame first and ask what it does better. If the atlas + shows a room, support surface, furniture interaction, wall, or body-contact + prop, add or rewrite those details. If the atlas shows a flat floor/ground + plane, translate the scene into floor-plane evidence. For top-view oral, the + working coworking translation is a minimal floor-plan tail such as + `carpet texture, carpet tile seams`; adding desk rows and window depth + fights the camera axis, while one cropped caster or desk foot should be + tested only after the sparse floor-plane read is stable. +23. For `pov_blowjob_top_down_vertical_shaft`, floor-plane evidence alone is + not the final rule. The atlas-22-style calibration needs the shaft/contact + line as the first visual anchor: the centered shaft runs from the lower + foreground to mouth contact, and the woman's face, eyelids, hair crown, + shoulders, upper chest, neckline or bare upper torso, and one hand stack + around that same axis. Viewer abdomen, thighs, and feet are lower-edge + frame evidence after the shaft axis is established; they should not be the + prompt's primary anchor. Translate body-proportion control into positive + visibility hierarchy, for example `the centered shaft and mouth contact + form the main vertical axis from the lower foreground to the woman's face` + plus `the woman's face, hair crown, shoulders, upper chest, and one hand + stack around the shaft-contact axis`. Avoid final-prompt phrasing such as + `hips and ass stay visually secondary` or `mostly hidden`; that is useful + as a human scoring note, but it is negative-style hierarchy inside positive + conditioning. +24. Use large image-only atlas folders as cue-expansion pools, not as automatic + generator truth. For top-view oral, the canonical curated atlas references + live under + `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view`, + while + `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/1.original/blowjob_top_view_1024` + is a supplemental raw pool with the same family and more images. The larger + pool is useful for defining repeated micro-axes: camera pitch, support-plane + type, viewer foreground amount, partner upper-body stack, hand placement, + eye direction, clothing/neckline anchors, and floor/furniture evidence. Keep + the live catalog `reference_images` curated to a small stable set, and when a + curated reference exists, prefer that canonical path in sidecar + `reference_images`. Use the supplemental raw pool before authoring sidecar + `append_cues`, especially for extra axes not present in the curated set. A + cue should either repeat across several atlas images or be tied to a specific + nearest reference image. Do not invent cue wording from a single mental model + when the atlas folders can show the allowed variation directly. +25. When a manual calibration render succeeds after several failed top-view + oral probes, compare the exact sidecar text before writing generator or + sidecar memory. The 2026-07-01 manual renders + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00135_.png` + through + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00139_.png` + improved verticality by ordering the prompt as straight-down POV, shaft + visibility, partner stack directly below the shaft, mouth contact, sparse + floor plane. This is a word-order and anchor finding, not just a background + removal finding. Future top-view oral variants should test shaft-first + cue order against any abdomen-first or room-depth wording before patching + the generator. +26. Choose sidecar prompt source by what the experiment is testing. Use + `append_cues` for small micro-position alternates that can safely sit after + the baseline prompt. Use exact `text` replacement when the winning evidence + depends on word order, removes a conflicting baseline clause, or translates + the scene tail into a different camera-compatible surface. For + `pov_blowjob_top_down_vertical_shaft`, shaft-first calibration must be an + exact-text candidate because appending shaft-axis cues after abdomen-first + and deep coworking-room wording does not faithfully simulate the manual + prompt win. ## Promotion Gates @@ -123,6 +219,595 @@ For location-specific wins, split the implementation: - 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. +## Atlas Detail Restore Hygiene + +When re-enabling removed atlas prompt details such as clothing, face expression, +body touch, or camera-presentation text, audit the final Krea prompt before +judging the rendered image. The restore node should not undo the reason the +atlas route was strict in the first place. + +- Restore details must be subordinate to the pose sentence. If a restored clause + adds another POV foreground, body-owner, camera-layout, or optional/policy + instruction, remove or rewrite it before rendering. +- Do not append raw category-axis detail into positive conditioning when a + structured route already has a safer representation. Clothing restore should + flow through the pair clothing continuity path, then through Krea cleanup, not + through raw `clothing_detail`. +- For POV clothing, separate viewer body cues from partner clothing. The normal + non-atlas foreground-clothing cue can help generic POV prompts, but strict + atlas prompts already own the foreground through the atlas pose. Strip + `POV foreground clothing cue` and `POV foreground body cue` from strict atlas + final prompts. +- Visible partner clothing needs explicit subject ownership. Prefer + `the woman wears ...` over bare fragments such as `button-down shirt ...`, + because bare clothing fragments can be assigned to the viewer/man. +- If the atlas crop hides a lower garment, keep the partial-removal semantics + without making the garment visible. For side-profile oral body-line, the useful + wording is `The woman's lower garments are pulled aside out of frame; the + woman wears the button-down shirt tied at the waist and a fitted bralette from + the same outfit`. +- For side-profile oral body-line specifically, restored clothing must not steal + the camera-owner body plane. The adult male viewer's abdomen/navel/pelvis/near + thigh remain the foreground ownership cues; clothing is a partner detail only. + +## Same-Subject Atlas Refine Decks + +Use same-subject generated reference folders as controlled prompt decks before +building seedable atlas cue variants. The current first deck is: + +```text +/media/unraid/comfyui/output/CodexMCP-Atlas-Refine +``` + +Build a manifest before analysis: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-manifest +``` + +Print coverage before choosing the next pose to test: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-coverage-report +``` + +The coverage report classifies each atlas entry as `needs_prompt_cleanup`, +`baseline_only`, `needs_visual_score`, `ready_for_seed_selection`, +`ready_for_catalog_review`, `rejected_only`, or `unknown_variant`. +`needs_prompt_cleanup` means the prompt-noise audit found option/meta/negative +wording in the baseline prompt or sidecar variants; clean that text before +scoring, authoring more alternates, or selecting cue seeds. `baseline_only` +means the prompt/image pair is clean but has no sidecar prompt variants yet; run +MCP probes or add reviewed sidecar candidates before seed selection. +`ready_for_catalog_review` means at least one seedable append-cue variant exists +and can be exported with `--print-catalog-cue-draft`. + +Print prompt-noise findings before scoring or promoting a deck: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-prompt-noise-report +``` + +The prompt-noise report is read-only. It flags option-list words such as `or`, +`may`, `optionally`, and `either`, meta/policy fragments such as +`keep the visible partner`, `context stays`, `camera layout`, and leaked +`POV foreground ... cue` text, plus positive-channel negative-conditioning +phrases such as `no`, `without`, or `do not`. It also flags exact repeated +direct phrases because duplicated pose clauses often make Krea2 weight the wrong +axis. Treat findings as prompt cleanup tasks before fixed-seed evidence is +promoted. Do not auto-rewrite them into new cues; rewrite the source prompt or +sidecar text manually, then rebuild the manifest and rerun the audit. Coverage +carries the same issue counts, so a noisy entry stays `needs_prompt_cleanup` +even if it also has unscored or seedable sidecar variants. + +For a file-oriented cleanup queue, print a prompt-cleanup sheet: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-prompt-cleanup-sheet +``` + +The cleanup sheet groups prompt-noise issues by editable source text. Baseline +issues point at the `.txt` prompt file; sidecar variant issues point at the +same-stem `.json` sidecar, including the `prompt_variant_id` and append-cue +index when relevant. The sheet preserves `current_text`, +`current_text_sha256`, `source_prompt_sha256`, issue excerpts, and blank +`replacement_text` / `cleanup_notes` fields for manual review. Do not apply the +sheet mechanically; use it as a checklist, edit the prompt or sidecar source by +hand, rebuild the manifest, and confirm coverage no longer reports +`needs_prompt_cleanup`. + +When using the sheet as an apply artifact, fill `replacement_text` manually and +validate it first: + +```bash +python tools/krea2_atlas_refine_manifest.py --validate-prompt-cleanup-sheet --prompt-cleanup-sheet-json /tmp/sxcp-prompt-cleanup-sheet-filled.json +``` + +Validation rejects blank replacements, replacements that still contain +option/meta/negative prompt noise, unsupported contexts, missing target +metadata, stale `current_text_sha256`, stale baseline `source_prompt_sha256`, +and replacements identical to the original text. After validation, apply only +against the source folder that produced the sheet: + +```bash +python tools/krea2_atlas_refine_manifest.py --apply-prompt-cleanup-sheet --prompt-cleanup-sheet-json /tmp/sxcp-prompt-cleanup-sheet-filled.json --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine +``` + +Apply is validation-first and drift-aware. It updates prompt files or the +targeted sidecar prompt-variant field, preserves unrelated sidecar metadata, +and allows a repeated apply when the target already equals the reviewed +replacement. Rebuild the manifest and rerun coverage after applying. + +For baseline-only entries, print sidecar scaffolds before hand-authoring cue +variants: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-sidecar-scaffold +``` + +The scaffold is read-only and does not write sidecar files. It only includes +known catalog entries with no existing prompt variants, preserves the same-stem +sidecar filename, baseline seed/cue/score slots, source prompt hash, and a blank +`prompt_variant_template`. Fill the template with user-authored `append_cues` or +exact `text`; do not let the scaffold invent cue wording. + +Before authoring alternates for a baseline-only pose, print a baseline score +sheet and grade the existing image/prompt pair: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-baseline-score-sheet +``` + +The baseline score sheet is also read-only. It preserves prompt/image paths, +prompt hashes, seed metadata, cue axes, and score slots for each baseline entry. +`needs_visual_score` means no score fields are filled yet; `partially_scored` +means some baseline gates are filled but the preservation assessment is not +complete. Use the sheet to decide whether a baseline is already valid, whether +the first sidecar variants should be small same-scene frame changes, or whether +the pose needs stronger structural prompt/control work before seed alternates. + +After manually filling baseline scores, convert the scored sheet into a +baseline score update draft: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-baseline-score-update-draft --baseline-score-sheet-json /tmp/sxcp-baseline-score-sheet-scored.json +``` + +The draft records only top-level baseline metadata: `seed_metadata`, +`cue_axes`, `score`, `score_state`, source prompt hash, and manual analysis +notes. It skips unknown or fully unscored entries and deliberately does not +carry `prompt_variants`; prompt alternates stay owned by the sidecar +promotion/seed-selection path. + +Validate the baseline score draft before writing sidecars: + +```bash +python tools/krea2_atlas_refine_manifest.py --validate-baseline-score-update-draft --baseline-score-update-draft-json /tmp/sxcp-baseline-score-update-draft.json +``` + +Validation rejects sidecar filename drift, missing prompt hashes, empty score +updates, forbidden negative-conditioning fields, and any accidental +`prompt_variants` contamination. Partial or rejected baseline scores are allowed +as evidence but reported as warnings, so partial progress can be preserved +without pretending it is seedable. + +Only after validation, apply the baseline score draft: + +```bash +python tools/krea2_atlas_refine_manifest.py --apply-baseline-score-update-draft --baseline-score-update-draft-json /tmp/sxcp-baseline-score-update-draft.json --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine +``` + +Baseline score apply is validation-first. It writes top-level sidecar +`seed_metadata`, `cue_axes`, `score`, `baseline_score_state`, +`baseline_source_prompt_sha256`, and `baseline_analysis_notes`, while preserving +any existing `prompt_variants` exactly. Rebuild the manifest after applying; +the baseline score sheet should then rescan the same entries as +`scored_pass`, `partially_scored`, or `scored_rejected` according to the saved +baseline evidence. + +The manifest is the bridge between generated artifacts and the seed/cue system. +It records each prompt/image pair, validates the inferred `variant_key` against +the catalog, preserves prompt text and a prompt hash, records missing pairs, and +reserves seed slots for `sampler_seed`, `generator_seed`, `atlas_cue_seed`, +`micro_position_seed`, and `workspace_seed`. Fill those seed slots when the +source workflow exposes them; leave them null only when the historical artifact +does not contain that metadata. Cue-selection commands may target +`generator_seed`, `atlas_cue_seed`, `micro_position_seed`, or `workspace_seed`, +but never `sampler_seed`; that slot is reserved for the actual render sampler +seed returned by the job. + +Optional same-stem JSON sidecars can enrich scanned entries without changing the +prompt/image filenames. For `pov_example_00001_.txt` and +`pov_example_00001_.png`, add `pov_example_00001_.json` with `seed_metadata`, +`cue_axes`, `score`, and `notes`. The manifest keeps explicit null slots for +missing fields so unfilled seed or score data is visible instead of silently +absent. + +Image-only atlas reference pools sit upstream from sidecars. First make a +labeled contact sheet, cluster the references by visible micro-axis, then choose +the nearest atlas target for each proposed cue. Store those nearest targets in a +sidecar prompt variant's `reference_images` list so the reference provenance +travels through prompt batches, result sheets, promotion reports, sidecar +updates, catalog cue drafts, and later rescans. Prefer canonical curated +references such as `blowjob_top_view/22_blowjob_top_view.png` when a matching +curated frame exists; use `1.original/...` paths for supplemental raw-pool frames +that do not have a curated counterpart. This field is still provenance, not +proof: a generated prompt/image pair must show that the same cue preserves the +current subject, workspace, clothing ownership, and prompt-noise gates before the +cue is eligible for seed selection or catalog promotion. Do not promote an +image-only reference cue directly into the generator. + +Before authoring top-view oral cue variants, print a read-only reference-pool +report: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-reference-pool-report --variant-key pov_blowjob_top_down_vertical_shaft --reference-pool-folder 1.original/blowjob_top_view_1024 +``` + +The report compares the canonical catalog `atlas_folders` against supplemental +raw folders by image id. Use `catalog_reference_images` and matched canonical +paths as preferred `reference_images`; use `supplemental_extra_images` to mine +extra cue axes when a raw frame has no curated counterpart. + +Then print a blank cue-review sheet from the same pool: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-reference-cue-review-sheet --variant-key pov_blowjob_top_down_vertical_shaft --reference-pool-folder 1.original/blowjob_top_view_1024 +``` + +Fill `observed_positive_cues`, `cue_axes`, and `review_notes` only from visual +inspection. The sheet provides `reference_images_template` for canonical +catalog refs, but leaves it blank for raw-only supplemental extras so those +images start as cue-mining evidence rather than automatic sidecar references. + +After filling the review sheet, print a candidate draft: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-reference-cue-candidate-draft --reference-cue-review-sheet-json /tmp/sxcp-reference-cue-review-filled.json +``` + +The candidate draft converts reviewed canonical rows into sidecar-ready +`prompt_variant` objects with `append_cues`, `reference_images`, `cue_axes`, and +seed slots. It skips blank rows, option/meta/negative prompt-noise cues, missing +variant ids, duplicate variant ids, and raw-only supplemental rows without a +canonical reference. Copy a candidate into a same-stem sidecar only after +choosing the baseline deck it should modify, then test it through the normal MCP +batch/result-sheet/promotion path. + +To attach reviewed candidates to a same-subject baseline deck for testing, +print a sidecar authoring draft: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-reference-cue-sidecar-author-draft --reference-cue-candidate-draft-json /tmp/sxcp-reference-cue-candidate-draft.json --variant-key pov_blowjob_top_down_vertical_shaft +``` + +Validate and apply that draft only against the same folder: + +```bash +python tools/krea2_atlas_refine_manifest.py --validate-reference-cue-sidecar-author-draft --reference-cue-sidecar-author-draft-json /tmp/sxcp-reference-cue-sidecar-author-draft.json +python tools/krea2_atlas_refine_manifest.py --apply-reference-cue-sidecar-author-draft --reference-cue-sidecar-author-draft-json /tmp/sxcp-reference-cue-sidecar-author-draft.json --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine +``` + +The author draft is pre-test only. It writes unscored prompt variants into the +same-stem sidecar after checking the baseline prompt hash for drift. Rebuild the +manifest after applying; coverage should move from `baseline_only` to +`needs_visual_score`, and the next step is a fixed-seed MCP batch, not catalog +promotion. + +Sidecars can also define explicit `prompt_variants` for seedable cue probes. +Each variant must provide an `id` and exactly one of `text` or `append_cues`; +the batch builder does not invent cue wording. Variant ids must be unique within +the sidecar because they are the stable identity for cue-seed selection, upsert +apply, and evidence roundtrips. If an explicit `prompt_source.prompt_variant_id` +is present, it must match the enclosing variant `id`. Use `append_cues` for small +micro-position changes that should read like another frame from the same scene, +such as a hand moving higher, feet moving farther forward, a body angle tilting, +or a workspace surface becoming more prominent. Keep every cue positive-only and +send batches through the normal `sxcp_eval_out` path: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-batch --variant-key pov_footjob_frontal_sole_stroke --sampler-seed 123 +``` + +Validate the printed batch with `tools/sxcp_prompt_batch.py` before using +`run-batch --run`. A sidecar prompt variant is candidate evidence only until +its returned image is scored against the atlas reference and the same-subject +baseline. + +After `run-batch --run` writes a result JSON, convert the exact batch/results +pair into a visual scoring sheet before editing sidecars or generator wording: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-result-sheet --batch-json /tmp/sxcp-batch.json --result-json /tmp/sxcp-results.json --notes "visual scoring pending" +``` + +The result sheet is not an automatic judge. It preserves the fixed sampler +seed, probe order, returned image paths, exact prompt text, cue-axis metadata, +and empty score slots for manual atlas comparison. Fill those slots only after +checking the generated images against the atlas reference for pose ownership, +workspace continuity, clothing visibility, subject identity, expression/eye +control, anatomy, and prompt noise. + +After the result sheet is manually scored, print a promotion report before +editing a sidecar or generator route: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-promotion-report --result-sheet-json /tmp/sxcp-result-sheet-scored.json +``` + +The promotion report is intentionally conservative. A probe is only a +`seedable_candidate` when pose ownership, workspace continuity, clothing +visibility, subject identity, and prompt noise are all scored `pass`, and the +remaining visual axes at least show progress rather than failure. Missing scores +stay `needs_visual_score`; failed preservation gates stay `rejected`. The report +also scans candidate text with the prompt-noise audit and rejects noisy text even +if the manual `prompt_noise` score was filled as `pass`. + +For ready candidates, print a sidecar update draft instead of editing the +sidecar directly: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-sidecar-update-draft --promotion-report-json /tmp/sxcp-promotion-report.json +``` + +The draft uses only exact tested prompt text from `seedable_candidate` probes. +It preserves the original same-stem sidecar filename, cue axes, seed metadata, +visual score evidence, and returned image path. Stable `matrix_evidence` carried +by a result-sheet probe is preserved through the promotion report and sidecar +draft. Explicit unstable `matrix_evidence` rejects the probe before promotion, +so the sidecar draft cannot replace a matrix-proven variant with weaker +single-batch evidence. Review the draft before copying anything into a sidecar; +rejected or unscored candidates are skipped. + +Validate the draft before applying any sidecar edit: + +```bash +python tools/krea2_atlas_refine_manifest.py --validate-sidecar-update-draft --sidecar-update-draft-json /tmp/sxcp-sidecar-update-draft.json +``` + +The validation gate rejects drafts with missing image evidence, failed +preservation scores, missing cue-axis movement, duplicate prompt-variant ids, +or forbidden negative-conditioning fields. Treat a failing validation report as +a prompt/evidence issue to resolve before editing sidecars. + +Only after validation passes, apply the draft to the atlas-refine folder: + +```bash +python tools/krea2_atlas_refine_manifest.py --apply-sidecar-update-draft --sidecar-update-draft-json /tmp/sxcp-sidecar-update-draft.json --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine +``` + +Apply is validation-first and idempotent. It upserts `prompt_variants` by id +inside the target same-stem sidecar, preserves unrelated sidecar fields, rejects +ambiguous existing `prompt_variants` lists with missing or duplicate ids, and +does not touch rejected or unscored candidates. + +After applying, rebuild the manifest and batch from the same folder as a +roundtrip check. The applied sidecar should rescan with the exact tested prompt +text, cue axes, seed metadata, visual evidence, and score evidence; the next +`--print-batch` output should regenerate the same tested prompt variant by id. +For matrix-proven sidecar variants, stable `matrix_evidence` stays attached to +the normal batch probe and the result sheet built from that batch. + +For generator-style single-variant selection, use the cue seed selector rather +than relying on sidecar order: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-seed-selection --variant-key pov_footjob_frontal_sole_stroke --selection-seed 202 --seed-slot atlas_cue_seed +``` + +The selector is deterministic for the same seed and only chooses sidecar +variants with seedable visual evidence. Eligible variants are sorted by +`prompt_variant_id` before the seed is applied, so reordering a sidecar JSON file +does not change what a cue seed means. Unscored or rejected variants are listed +as ineligible instead of entering the seed pool. If a selected sidecar variant +has stable `matrix_evidence`, the selector keeps that matrix proof attached to +the selected candidate. Variants with no matrix evidence still use single-image +promotion evidence, but variants with explicit unstable matrix evidence are +ineligible until retested or corrected. Downstream reuse does not trust +`stable: true` by itself: malformed stable matrix evidence, such as duplicated +declared sampler seeds, non-matching matrix jobs, or cue-seed metadata that no +longer matches the matrix selection seed, is treated as unstable and kept out of +the seed pool. + +To render the selected alternate frame through the normal MCP batch helper, use +the selected-batch exporter. It emits the baseline and selected candidate only: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-seed-selected-batch --variant-key pov_footjob_frontal_sole_stroke --selection-seed 202 --sampler-seed 101 --seed-slot atlas_cue_seed +``` + +Then validate/run it with `tools/sxcp_prompt_batch.py` as usual. The candidate +probe carries the selected cue seed in the requested seed slot plus the sidecar +evidence that justified the variant. Probe `seed_metadata.sampler_seed` is the +actual sampler seed for that render job; cue, micro-position, generator, and +workspace seed slots remain prompt-variant provenance. Do not use +`sampler_seed` as `--seed-slot`; the tooling rejects it so the cue seed cannot +overwrite the render seed. Matrix-proven variants also keep their full stable +`matrix_evidence` on the selected probe. + +For repeatability checks across several sampler seeds and cue seeds, print a +seed matrix: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-seed-matrix --variant-key pov_footjob_frontal_sole_stroke --selection-seeds 202,203 --sampler-seeds 101,102 --seed-slot atlas_cue_seed +``` + +The matrix is read-only and sampler-major. Each job embeds a normal +seed-selected prompt batch with the baseline and selected candidate only, plus +the selected prompt variant id, exact candidate prompt text, cue seed, sampler +seed, cue axes, and evidence provenance. Use it to queue controlled repeats +where sampler seed changes image stochasticity and `atlas_cue_seed` changes the +selected alternate frame. Sampler seed lists and cue seed lists must contain +distinct values; duplicate seeds are rejected because they inflate apparent +repeatability without adding new evidence. A matrix with only one unique sampler +seed can still be inspected, but it cannot become stable sidecar evidence; stable +proof requires at least two unique sampler seeds before manual hard-pose +thresholds are considered. + +After matrix jobs return images, create one matrix result sheet instead of +manually merging per-job notes: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-seed-matrix-result-sheet --seed-matrix-json /tmp/sxcp-seed-matrix.json --seed-matrix-results-json /tmp/sxcp-seed-matrix-results.json --notes "matrix scoring pending" +``` + +The matrix result sheet matches returned results by matrix job id, then reuses +the normal result-sheet format inside each job. It preserves sampler seed, cue +seed, selected prompt variant id, exact candidate prompt text, returned image +path, and empty score slots for manual atlas scoring. Missing or extra job ids +are errors because they break matrix comparability. Duplicate matrix job ids are +also rejected before result matching, since one returned image must not stand in +for two sampler/cue slots. + +After manually scoring the matrix result sheet, print the matrix promotion +report: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-seed-matrix-promotion-report --seed-matrix-result-sheet-json /tmp/sxcp-seed-matrix-result-sheet-scored.json +``` + +The promotion report applies the same preservation gates as a single result +sheet, then groups jobs by selected prompt variant and cue seed. A group is +stable only when every declared sampler seed in that group is present, at least +two unique sampler seeds are covered, and every covered job is a +`seedable_candidate`; failed jobs keep their blockers, such as subject identity +or workspace continuity failures, attached to the group, omitted sampler seeds +add `missing_sampler_coverage`, and one-sampler groups add +`insufficient_sampler_coverage`. The declared `sampler_seeds` and +`selection_seeds` lists must not contain duplicates, because repeated +declarations can inflate apparent coverage without adding a new render or cue +seed. If `selection_seeds` is present, every job's `selection_seed` must be in +that declared cue-seed set. If `sampler_seeds` is present, every job's +`sampler_seed` must be in that declared render-seed set, and a selected-variant / +cue-seed group may contain each sampler seed only once. Promotion also rechecks +that each matrix result-sheet job has a unique id, so stale or hand-edited sheets +cannot inflate stable evidence by duplicating a job. The selected prompt variant +id recorded on the matrix job must also match the scored candidate prompt +variant id; a mismatch means the sheet no longer proves the selected alternate. +Every job's `seed_slot` must match the matrix result sheet's `seed_slot`, so +atlas-cue evidence cannot be mixed with workspace, generator, or micro-position +evidence in one stable group. +Jobs in the same selected-variant/cue-seed group must also keep the same +`variant_key`, `source_entry_id`, and `source_stem`, so evidence from another +pose family or atlas artifact cannot be folded into a stable sidecar candidate. +They must also keep the same exact candidate prompt text across sampler jobs; +the promotion report records a prompt-text hash for the group, and prompt-text +drift under the same variant id is rejected because it no longer proves one +repeatable wording. +Treat stable groups as repeatability evidence for a sidecar/catalog cue; treat +unstable groups as wording, coverage, or control work before promotion. + +For stable groups, print a matrix sidecar update draft before editing sidecars: + +```bash +python tools/krea2_atlas_refine_manifest.py --print-matrix-sidecar-update-draft --seed-matrix-promotion-report-json /tmp/sxcp-seed-matrix-promotion-report.json +``` + +The draft emits only stable groups. It preserves the same-stem sidecar filename, +the exact selected prompt variant text, prompt source provenance, representative +single-image evidence for compatibility, and `matrix_evidence` containing all +passing sampler seeds, returned image paths, visual scores, cue seed, and job +ids. Stable groups fail closed if any listed job id is missing from the promotion +report, so a hand-edited report cannot write sidecar evidence for jobs it does +not carry. A stable group may not repeat a `job_id`, because one returned image +must not count as multiple matrix samples. Each referenced job must still belong +to the stable group's identity: same selected prompt variant, cue seed, seed +slot, pose `variant_key`, `source_entry_id`, `source_stem`, and exact candidate +prompt text. Stable groups +also fail closed when their declared `sampler_seeds` do not match the sampler +seeds on their listed `job_ids`, so a draft cannot inflate repeatability +evidence by claiming an unreferenced render seed. Their `job_count`, +`promotion_ready_count`, and `blocked_count` must also match the referenced +jobs; emitted matrix evidence derives these counts from `job_ids` instead of +trusting editable group summaries. Unstable cue groups are listed as skipped +with their blockers and must not be copied into sidecars. +Even if a hand-edited promotion report marks a group stable, matrix sidecar draft +generation rejects groups whose referenced `job_ids` cover fewer than two unique +sampler seeds. + +Validate a matrix draft with the matrix-specific gate before applying it: + +```bash +python tools/krea2_atlas_refine_manifest.py --validate-matrix-sidecar-update-draft --matrix-sidecar-update-draft-json /tmp/sxcp-matrix-sidecar-update-draft.json +``` + +This gate rejects unstable matrix evidence, failed per-sampler visual scores, +missing or insufficient sampler coverage, forbidden negative-conditioning fields, +and mismatched cue seed metadata: the prompt variant's +`seed_metadata[matrix_evidence.seed_slot]` must match +`matrix_evidence.selection_seed`. It also rejects duplicated matrix evidence job +ids or sampler seeds, duplicated declared matrix sampler seeds, stable evidence +with fewer than two unique sampler seeds, and matrix evidence rows whose `turn` +is missing or not an integer. It also +checks the representative single-image evidence used for compatibility with the +normal seed selector, including requiring an integer `evidence.turn`. That +representative evidence must match the +`matrix_evidence.jobs` row for its `evidence.seed`, including image path, turn, +and visual score. + +Only after validation passes, apply the matrix draft to the atlas-refine folder: + +```bash +python tools/krea2_atlas_refine_manifest.py --apply-matrix-sidecar-update-draft --matrix-sidecar-update-draft-json /tmp/sxcp-matrix-sidecar-update-draft.json --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine +``` + +Matrix apply is validation-first and idempotent. It upserts prompt variants by +id, preserves unrelated sidecar fields, and keeps the full `matrix_evidence` so +later rescans still know which cue seed and sampler seeds proved the alternate +frame repeatable. + +When a selected batch returns images, convert it with `--print-result-sheet` +like any other batch. The result sheet preserves the seed-selection report at +the sheet level and the selected prompt variant id on the candidate probe, so +manual visual scoring remains tied to the exact cue seed and sidecar alternate. +If the selected probe carried stable `matrix_evidence`, the result sheet keeps +that matrix proof beside the new image path and empty score slots. + +For the generator node path, `atlas_cue_seed` on `SxCP Krea2 Pose Variant` and +the family-specific `SxCP Krea2 POV ... Filter` nodes selects among explicit +catalog `prompt_variant_cues` for the selected atlas variant. This is not the +sampling seed and it does not invent prompt wording. Use `-1` when the broader +generator pose seed should continue choosing cue-set alternates. Use a fixed +`atlas_cue_seed` when testing the same catalog alternate across subjects, +locations, or sampler seeds. The selected index is stored in +`krea2_prompt_variant_indices`, preserved through row building, and shown in the +node summary as `cue_indices=variant:index`. + +To bridge scored sidecar alternates back toward the generator catalog, preserve +`append_cues` provenance through the refine loop. A full prompt win can remain a +sidecar `text` candidate, but only an explicit append-cue delta should become a +reviewable catalog `prompt_variant_cues` candidate. Print that review draft with: + +```bash +python tools/krea2_atlas_refine_manifest.py --folder /media/unraid/comfyui/output/CodexMCP-Atlas-Refine --subject-id atlas_refine_same_woman_001 --print-catalog-cue-draft --variant-key pov_footjob_frontal_sole_stroke +``` + +The catalog cue draft is read-only. It skips unscored, rejected, or exact-text +only sidecar variants and emits only seedable append-cue candidates with visual +evidence, cue axes, seed metadata, and the exact tested prompt hash. Stable +matrix evidence is preserved on catalog candidates; explicit unstable matrix +or malformed stable matrix evidence is skipped and listed with an +`unstable_matrix_evidence` blocker. Review the draft manually before editing +`categories/krea2_pov_pose_variants.json`; do not infer catalog cue wording from +a whole prompt diff. + +Use the manifest entries as baseline frames, not as proven generator fixes. For +each variant, score the current image/prompt against: + +- atlas pose and contact ownership; +- same-subject identity preservation; +- workspace lounge consistency and surface relationship; +- clothing visibility and subject ownership; +- face/eye/expression retention when the face is visible; +- anatomy/proportion sanity; +- prompt noise, duplicate cues, and ambiguous ownership. + +Only add seedable cue alternates after the baseline frame is understood. Store +alternates by axis, such as contact depth, hand position, foot position, body +angle, camera height, workspace surface, clothing visibility, expression/eye +detail, and anatomy shape detail. A seed change should feel like selecting +another frame from the same scene rather than random prompt drift. + ## MCP Command Memory Use the checked helper instead of ad hoc Python snippets for bridge calls. The @@ -159,7 +844,9 @@ 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. +bulk analysis second. The batch helper validates without stripping atlas +metadata such as cue axes, seed metadata, selection data, evidence, and stable +`matrix_evidence`. Before drafting evidence, compare atlas references and generated images for spatial orientation, not only limb/contact similarity. First decide the @@ -244,6 +931,297 @@ scene. The corrected test pattern keeps the coworking location intact: ## Improvement Log +- `2026-07-01`: Added large image-only atlas folders to the cue-expansion + method after inspecting the canonical + `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/blowjob_top_view` + folder and the 27-image supplemental raw pool at + `/media/unraid/davinci/Qwen_edit_lora/POV/dataset_v2/1.original/blowjob_top_view_1024`. + The curated folder remains the preferred `reference_images` source when a + matching frame exists; the supplemental pool defines extra allowed micro-axes + before sidecar authoring. Cue wording still needs fixed-seed generated + evidence before sidecar, catalog, or generator promotion. +- `2026-07-01`: Added atlas-22 image-to-prompt calibration for + `pov_blowjob_top_down_vertical_shaft` after manual renders + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00100_.png` and + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00101_.png` + produced the strongest verticality so far. The retained rule is not “remove + background”; it is floor/support-plane scene translation plus positive + upper-body-stack hierarchy. Viewer abdomen/thigh cues should remain lower-edge + anchors, while face, hair crown, shoulders, upper chest or neckline, and hand + carry the partner geometry. Keep phrases like `hips and ass stay visually + secondary` as human scoring notes, not final positive prompt text. +- `2026-07-01`: Corrected the top-view oral anchor after manual sidecar renders + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00135_.png`, + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00136_.png`, + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00137_.png`, and + `/media/unraid/comfyui/output/sxcp_accumulator/bwave_2/img_00139_.png`. + The strongest same-seed verticality came from making the centered shaft and + mouth contact the primary axis, then stacking the woman's face, hair crown, + shoulders, upper chest, and hand around it. Abdomen/thigh/feet wording belongs + after that as lower-frame evidence. Sparse floor-plane wording remains useful + because deep coworking-room tails fight the overhead angle. +- `2026-07-01`: Added exact-text reference cue candidates for order-sensitive + atlas tests. The reference cue review path can now carry + `prompt_variant_template.text` through candidate draft, sidecar authoring, and + prompt-batch export as `prompt_source.kind = text`. Use this for + shaft-first/top-view oral calibration and other cases where append-cues would + leave the older conflicting baseline hierarchy in front of the tested wording. +- `2026-07-01`: Added explicit sidecar prompt-variant batches for + same-subject atlas refine decks. `krea2_atlas_refine_manifest.py` now keeps + sidecar `prompt_variants` and can print an `sxcp_prompt_batch`-compatible + positive-only probe batch for one catalog `variant_key`. Cue text must come + from the sidecar as exact `text` or `append_cues`; the batch builder may + combine and preserve seed/cue metadata, but it must not create new pose + wording by itself. +- `2026-07-01`: Added atlas result-sheet generation after prompt batches return + images through the MCP loop. The sheet keeps batch/result order, sampler seed, + prompt text, image paths, cue axes, and unfilled score slots together so + visual analysis can be written against the exact generated artifacts before + sidecar promotion or generator patches. +- `2026-07-01`: Added conservative promotion reports for scored atlas result + sheets. Reports recover the sidecar prompt variant id, keep cue/seed metadata, + and classify candidates as `seedable_candidate`, `needs_visual_score`, or + `rejected` using preservation gates for pose ownership, workspace continuity, + clothing visibility, subject identity, and prompt noise. The report does not + auto-edit sidecars or generator wording. +- `2026-07-01`: Added sidecar update drafts for seedable atlas candidates. The + draft emits reviewable `prompt_variants` grouped by original same-stem sidecar + filename, uses only exact tested prompt text, and carries cue axes, seed + metadata, image evidence, and visual scores forward. It deliberately skips + rejected or unscored candidates and does not write sidecar files. +- `2026-07-01`: Added sidecar update draft validation before any sidecar edit. + The validator rejects drafts with missing cue-axis movement, missing image + evidence, failed preservation scores, duplicate prompt-variant ids, or + forbidden negative-conditioning fields, keeping sidecar promotion tied to + exact scored artifacts rather than hand-cleaned prompt text. +- `2026-07-01`: Added validation-first sidecar draft apply. The apply command + writes reviewed `prompt_variants` into same-stem sidecar JSON files, upserts + by variant id so repeated applies do not duplicate variants, and preserves + unrelated sidecar metadata. +- `2026-07-01`: Added applied-sidecar roundtrip evidence preservation. Manifest + scanning now keeps prompt-variant `evidence` from sidecars and generated + prompt batches carry that evidence forward, so a promoted seedable cue remains + tied to the exact tested prompt, image path, and visual score evidence after + it is written to the sidecar. +- `2026-07-01`: Added deterministic atlas cue-seed selection for applied + sidecar variants. `--print-seed-selection` chooses a stable prompt variant for + a seed slot such as `atlas_cue_seed`, but only from variants whose evidence + passes the seedable-candidate preservation gates. Unproven sidecar variants + are reported as ineligible so seed selection does not silently use weak cues. +- `2026-07-01`: Cue-seed selection now sorts eligible candidates by + `prompt_variant_id` before indexing by seed. Reordering sidecar JSON no longer + changes which alternate frame a given cue seed selects. +- `2026-07-01`: Manifest ingestion now rejects duplicate sidecar + `prompt_variants[].id` values before selection or batching, keeping cue-seed + identity and sidecar upserts unambiguous. +- `2026-07-01`: Manifest ingestion now also rejects sidecar variants whose + explicit `prompt_source.prompt_variant_id` does not match the enclosing + variant `id`, so provenance cannot point at a different cue. +- `2026-07-01`: Sidecar update validation now enforces the same + `prompt_source.prompt_variant_id` identity rule for normal and matrix drafts + before apply writes durable sidecar state. +- `2026-07-01`: Sidecar apply now also rejects ambiguous existing + `prompt_variants` lists before upsert, so apply cannot silently preserve or + rewrite duplicate prompt-variant ids. +- `2026-07-01`: Added seed-selected prompt batch export. The exporter turns a + deterministic cue-seed selection into an `sxcp_prompt_batch`-compatible JSON + with baseline plus the selected candidate only, preserving exact prompt text, + selected seed-slot metadata, and evidence provenance for MCP evaluation. +- `2026-07-01`: Prompt batches now stamp the actual render sampler seed into + every probe's `seed_metadata.sampler_seed`, including matrix jobs with sampler + overrides, while preserving the other seed slots as cue/provenance metadata. +- `2026-07-01`: Cue-selection seed slots now explicitly reject `sampler_seed` + in seed selection and matrix sidecar validation. The sampler seed remains the + render seed; cue, generator, micro-position, and workspace slots carry prompt + alternate provenance. +- `2026-07-01`: Added seed-matrix export for atlas alternates. The matrix builds + normal seed-selected batches for every sampler-seed / cue-seed pair, keeping + sampler stochasticity and `atlas_cue_seed` selection separate while preserving + exact selected prompt text and visual evidence in each job. +- `2026-07-01`: Added seed-matrix result sheets. Completed matrix jobs can now + be converted into one scoring artifact that preserves each job id, sampler + seed, cue seed, selected variant, exact prompt text, returned image path, and + empty score slots, while rejecting duplicate, missing, or extra matrix result + ids. +- `2026-07-01`: Added seed-matrix promotion reports. Scored matrix jobs are + aggregated with the same preservation gates as single-result promotion and + grouped by selected prompt variant plus cue seed, marking groups stable only + when every sampler seed passes. Promotion now rejects duplicate or missing + result-sheet job ids, duplicate declared sampler or cue seeds, jobs outside + declared sampler/cue seed sets, duplicate sampler jobs inside one cue group, + selected/candidate prompt-variant id mismatches, job-level seed-slot drift, and + variant/source-entry/stem drift before grouping stable evidence. +- `2026-07-01`: Added matrix sidecar update drafts for stable cue groups. + `--print-matrix-sidecar-update-draft` skips unstable groups and emits reviewed + sidecar prompt-variant updates with representative evidence plus full + `matrix_evidence` across passing sampler seeds. Stable groups now fail closed + if any referenced promotion-report job id is missing. +- `2026-07-01`: Added matrix sidecar validation and apply. Stable matrix drafts + now have `--validate-matrix-sidecar-update-draft` and + `--apply-matrix-sidecar-update-draft`, preserving full sampler/cue evidence + through idempotent sidecar upserts instead of relying on manual copy-paste. +- `2026-07-01`: Preserved seed-selection metadata through selected-batch result + sheets. After MCP returns images for a selected batch, the result sheet keeps + the sheet-level selection report and candidate-level selected prompt variant + id, preventing later visual scores from losing the cue seed that produced the + frame. +- `2026-07-01`: Propagated stable matrix evidence through seed selection, + selected-batch export, and selected-batch result sheets, so a matrix-proven + sidecar alternate keeps its cue seed, sampler seeds, job ids, image paths, and + score evidence attached during later single-frame retests. +- `2026-07-01`: Added a seed-selection gate for explicit unstable matrix + evidence. Legacy single-image variants remain selectable, but a variant that + carries `matrix_evidence` must have `stable: true` before it can enter the cue + seed pool. +- `2026-07-01`: Extended the same matrix-evidence gate to catalog cue drafts and + coverage. Stable matrix proof is preserved on generator-catalog cue candidates; + explicit unstable matrix evidence blocks catalog-review readiness. +- `2026-07-01`: Preserved stable matrix evidence through normal prompt-batch + exports and their result sheets, so regular all-variant retests keep the same + repeatability proof as seed-selected retests. +- `2026-07-01`: Made `tools/sxcp_prompt_batch.py` metadata-preserving when it + loads batches. Validation and runner paths keep atlas cue axes, seed metadata, + selection data, evidence, and stable matrix evidence available to downstream + scoring tools. +- `2026-07-01`: Preserved stable matrix evidence through normal promotion + reports and sidecar update drafts, preventing regular single-batch retests from + overwriting a matrix-proven sidecar variant with a metadata-poorer copy. +- `2026-07-01`: Blocked explicit unstable matrix evidence in normal promotion + reports. A result-sheet probe carrying `matrix_evidence.stable: false` is + rejected with `unstable_matrix_evidence` and skipped by sidecar update drafts. +- `2026-07-01`: Connected catalog atlas cue seeds to the generator node path. + `atlas_cue_seed` on Krea2 pose/filter nodes now records deterministic + `krea2_prompt_variant_indices` for explicit catalog `prompt_variant_cues`, + and prompt row assembly preserves those indices instead of overwriting them + with the broader pose seed. This makes catalog cue alternates reproducible as + same-scene frame changes while keeping sampler seed and cue seed separate. +- `2026-07-01`: Preserved append-cue provenance through atlas-refine promotion + and added a read-only catalog cue draft. Batch probes, result sheets, + promotion reports, and applied sidecars now keep `prompt_source`, so + `--print-catalog-cue-draft` can propose catalog `prompt_variant_cues` only + from scored seedable append-cue deltas instead of inventing alternates from a + full prompt. +- `2026-07-01`: Added an atlas-refine coverage report for live decks. The report + counts baseline-only entries, unscored sidecar variants, seedable candidates, + rejected variants, and catalog-cue-ready append-cue candidates, making the + next MCP/scoring action explicit before changing generator/catalog wording. +- `2026-07-01`: Added read-only sidecar scaffolds for baseline-only atlas + entries. `--print-sidecar-scaffold` emits same-stem sidecar filenames, + baseline metadata slots, source prompt hashes, and a blank prompt-variant + template so user-authored cue variants can be added without inventing wording + or writing files automatically. +- `2026-07-01`: Added a read-only baseline score sheet for same-subject atlas + decks. `--print-baseline-score-sheet` exports every baseline prompt/image + pair with score slots and score state, separating fully unscored baselines + from partially scored ones before sidecar variants or catalog cue alternates + are promoted. +- `2026-07-01`: Added validation-first baseline score sidecar updates. + Manually scored baseline sheets can now produce a baseline score update draft, + validate it, and apply top-level `score`, seed, cue-axis, prompt-hash, and + analysis-note metadata back into same-stem sidecars without carrying or + modifying `prompt_variants`. Partial baseline progress is preserved as + warning-level evidence instead of being promoted as a seedable alternate. +- `2026-07-01`: Added a read-only atlas prompt-noise report. The report scans + baseline prompts and sidecar prompt-variant text/cues for option-list words, + meta/policy instructions, leaked POV foreground cue labels, and + positive-channel negative-conditioning phrases before those prompts become + fixed-seed evidence. +- `2026-07-01`: Integrated prompt-noise findings into atlas coverage. A known + entry with noisy baseline or sidecar prompt text now reports + `needs_prompt_cleanup` before `baseline_only`, `needs_visual_score`, or + seed-selection states, so noisy prompts cannot silently advance as repeatable + seed/cue evidence. +- `2026-07-01`: Added a manual prompt-cleanup sheet for atlas prompt-noise + findings. `--print-prompt-cleanup-sheet` groups issues by editable source + text, points baseline issues to prompt files and sidecar issues to + same-stem JSON prompt variants, and leaves `replacement_text` blank so cleanup + remains human-reviewed rather than generated by the tooling. +- `2026-07-01`: Added validation-first prompt-cleanup apply. Filled cleanup + sheets can now be validated for nonblank/noise-free manual replacements and + applied to prompt files or targeted sidecar variant text/cues while preserving + unrelated sidecar metadata and rejecting drift. +- `2026-07-01`: Added `current_text_sha256` to prompt-cleanup sheet items and + validation. Manual cleanup artifacts now prove their editable source text was + not altered inside the sheet before replacement text is applied. +- `2026-07-01`: Added `source_prompt_sha256` to prompt-cleanup sheet items and + validation. Manual cleanup artifacts now stay tied to the exact atlas baseline + prompt identity used by batch, sidecar, and promotion evidence. +- `2026-07-01`: Seed matrices now reject duplicate sampler or cue seeds before + jobs are emitted, so stable matrix evidence cannot be inflated by repeated + copies of the same generated condition. +- `2026-07-01`: Seed-matrix promotion now requires each stable cue group to cover + every declared sampler seed. Edited or incomplete matrix result sheets report + `missing_sampler_coverage` instead of promoting partial evidence. +- `2026-07-01`: Stable matrix evidence now requires at least two unique sampler + seeds. One-sampler matrices remain inspectable but report + `insufficient_sampler_coverage`, and sidecar validation/draft generation reject + hand-edited stable evidence below that repeatability floor. +- `2026-07-01`: Matrix sidecar drafts now verify stable groups' declared + `sampler_seeds` against their referenced `job_ids`, so hand-edited promotion + reports cannot write sidecar evidence that claims unreferenced render seeds. +- `2026-07-01`: Matrix sidecar drafts now reject stable groups whose + `job_count`, `promotion_ready_count`, or `blocked_count` drift from their + referenced jobs, and emitted matrix evidence uses job-derived counts. +- `2026-07-01`: Matrix sidecar drafts now reject stable groups with duplicated + `job_ids`, so one returned matrix image cannot be counted as multiple + repeatability samples. +- `2026-07-01`: Matrix sidecar drafts now reject stable groups whose referenced + jobs drift from the group's selected prompt variant, cue seed, seed slot, pose + variant, or source sidecar identity. +- `2026-07-01`: Matrix sidecar validation now rejects duplicated + `matrix_evidence.jobs` ids and duplicated per-job sampler seeds, so a manually + edited sidecar draft cannot count one evidence row twice. +- `2026-07-01`: Matrix sidecar validation now rejects duplicated declared + `matrix_evidence.sampler_seeds`, keeping declared render-seed coverage aligned + with the unique matrix evidence rows. +- `2026-07-01`: Matrix sidecar validation now rejects cue seed drift between + `seed_metadata[matrix_evidence.seed_slot]` and + `matrix_evidence.selection_seed`. +- `2026-07-01`: Matrix sidecar validation now requires representative + single-image `evidence` to match the `matrix_evidence.jobs` row for its + sampler seed, keeping normal seed-selector compatibility evidence tied to the + matrix proof. +- `2026-07-01`: Downstream seed selection and catalog cue drafts now treat + malformed stable `matrix_evidence` as unstable, so `stable: true` alone cannot + reintroduce hand-edited matrix proof after sidecar rescan. +- `2026-07-01`: Downstream matrix evidence reuse now also requires + `seed_metadata[matrix_evidence.seed_slot]` to match + `matrix_evidence.selection_seed`, so cue-seed metadata drift cannot enter seed + selection after sidecar rescan. +- `2026-07-01`: Matrix sidecar validation and downstream stable-evidence reuse + now reject matrix evidence job rows whose `turn` is missing or not an integer. +- `2026-07-01`: Matrix sidecar validation now rejects representative + single-image `evidence.turn` values that are missing or not integers before + comparing them to matrix evidence rows. +- `2026-07-01`: Extended prompt-noise detection to exact repeated direct + phrases. Duplicate pose clauses now surface as `duplicate_phrase` cleanup + issues before they can be scored, promoted, or used for seed selection. +- `2026-07-01`: Added a promotion-time prompt-noise gate. Result-sheet + candidates carrying option/meta/negative/duplicate prompt noise are rejected + with `prompt_noise_issue` even when manual visual scores mark prompt noise as + pass. +- `2026-07-01`: Added same-subject atlas refine deck ingestion after + `/media/unraid/comfyui/output/CodexMCP-Atlas-Refine` was prepared with one + prompt/image pair per atlas variant for a stable subject. Future seed/cue + tuning should first build a manifest with + `tools/krea2_atlas_refine_manifest.py`, confirm every prompt/image pair maps + to a catalog `variant_key`, and use the manifest's seed slots to distinguish + sampler, generator, atlas-cue, micro-position, and workspace-surface changes. + This makes cue seeds behave like alternate frames from the same scene rather + than uncontrolled prompt drift. +- `2026-07-01`: Added explicit `--print-manifest` support to the atlas-refine + CLI. The default no-mode output still prints the manifest, but scripts and + notes can now request that artifact by name like the other report modes. +- `2026-07-01`: Added atlas detail-restore hygiene after side-profile oral + clothing restore preserved the shirt/bralette but emitted ownerless wording + and earlier leaked `POV foreground clothing cue` into strict atlas prompts. + Future atlas restores must audit the final Krea prompt, strip raw foreground + clothing/body cue clauses, keep restored clothing explicitly partner-owned, + and preserve partial-removal semantics without making hidden lower garments + visible. For side-profile oral body-line, use `the woman wears ...` for + visible clothes and keep lower garments `pulled aside out of frame` so the + adult male viewer's abdomen/navel/pelvis/near thigh remain the only + foreground body-owner cues. - `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 diff --git a/krea2_atlas_refine_manifest.py b/krea2_atlas_refine_manifest.py new file mode 100644 index 0000000..4f852ba --- /dev/null +++ b/krea2_atlas_refine_manifest.py @@ -0,0 +1,4316 @@ +from __future__ import annotations + +import argparse +import hashlib +import json +import re +from pathlib import Path +from typing import Any + +try: + from . import krea2_pose_variant_catalog +except ImportError: # Allows local CLI/tests from the repository root. + import krea2_pose_variant_catalog + + +SCHEMA = "sxcp_krea2_atlas_refine_manifest_v1" +BATCH_SCHEMA = "sxcp_atlas_refine_prompt_batch_v1" +RESULT_SHEET_SCHEMA = "sxcp_atlas_refine_result_sheet_v1" +PROMOTION_REPORT_SCHEMA = "sxcp_atlas_refine_promotion_report_v1" +SIDECAR_UPDATE_DRAFT_SCHEMA = "sxcp_atlas_refine_sidecar_update_draft_v1" +SIDECAR_UPDATE_VALIDATION_SCHEMA = "sxcp_atlas_refine_sidecar_update_validation_v1" +SIDECAR_APPLY_REPORT_SCHEMA = "sxcp_atlas_refine_sidecar_apply_report_v1" +MATRIX_SIDECAR_UPDATE_DRAFT_SCHEMA = "sxcp_atlas_refine_matrix_sidecar_update_draft_v1" +MATRIX_SIDECAR_UPDATE_VALIDATION_SCHEMA = "sxcp_atlas_refine_matrix_sidecar_update_validation_v1" +MATRIX_SIDECAR_APPLY_REPORT_SCHEMA = "sxcp_atlas_refine_matrix_sidecar_apply_report_v1" +SEED_SELECTION_SCHEMA = "sxcp_atlas_refine_seed_selection_v1" +SEED_MATRIX_SCHEMA = "sxcp_atlas_refine_seed_matrix_v1" +SEED_MATRIX_RESULT_SHEET_SCHEMA = "sxcp_atlas_refine_seed_matrix_result_sheet_v1" +SEED_MATRIX_PROMOTION_REPORT_SCHEMA = "sxcp_atlas_refine_seed_matrix_promotion_report_v1" +CATALOG_CUE_DRAFT_SCHEMA = "sxcp_atlas_refine_catalog_cue_draft_v1" +COVERAGE_REPORT_SCHEMA = "sxcp_atlas_refine_coverage_report_v1" +REFERENCE_POOL_REPORT_SCHEMA = "sxcp_atlas_reference_pool_report_v1" +REFERENCE_CUE_REVIEW_SHEET_SCHEMA = "sxcp_atlas_reference_cue_review_sheet_v1" +REFERENCE_CUE_CANDIDATE_DRAFT_SCHEMA = "sxcp_atlas_reference_cue_candidate_draft_v1" +REFERENCE_CUE_SIDECAR_AUTHOR_DRAFT_SCHEMA = "sxcp_atlas_reference_cue_sidecar_author_draft_v1" +REFERENCE_CUE_SIDECAR_AUTHOR_VALIDATION_SCHEMA = "sxcp_atlas_reference_cue_sidecar_author_validation_v1" +REFERENCE_CUE_SIDECAR_AUTHOR_APPLY_REPORT_SCHEMA = "sxcp_atlas_reference_cue_sidecar_author_apply_report_v1" +SIDECAR_SCAFFOLD_SCHEMA = "sxcp_atlas_refine_sidecar_scaffold_v1" +BASELINE_SCORE_SHEET_SCHEMA = "sxcp_atlas_refine_baseline_score_sheet_v1" +BASELINE_SCORE_UPDATE_DRAFT_SCHEMA = "sxcp_atlas_refine_baseline_score_update_draft_v1" +BASELINE_SCORE_UPDATE_VALIDATION_SCHEMA = "sxcp_atlas_refine_baseline_score_update_validation_v1" +BASELINE_SCORE_APPLY_REPORT_SCHEMA = "sxcp_atlas_refine_baseline_score_apply_report_v1" +PROMPT_NOISE_REPORT_SCHEMA = "sxcp_atlas_refine_prompt_noise_report_v1" +PROMPT_CLEANUP_SHEET_SCHEMA = "sxcp_atlas_refine_prompt_cleanup_sheet_v1" +PROMPT_CLEANUP_VALIDATION_SCHEMA = "sxcp_atlas_refine_prompt_cleanup_validation_v1" +PROMPT_CLEANUP_APPLY_REPORT_SCHEMA = "sxcp_atlas_refine_prompt_cleanup_apply_report_v1" +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"} +PROMPT_SUFFIXES = {".txt", ".prompt"} +IMAGE_SUFFIXES = {".png"} +SIDECAR_SUFFIX = ".json" +SEED_METADATA_KEYS = ( + "sampler_seed", + "generator_seed", + "atlas_cue_seed", + "micro_position_seed", + "workspace_seed", +) +SEED_SELECTION_SLOT_KEYS = tuple(key for key in SEED_METADATA_KEYS if key != "sampler_seed") +CUE_AXIS_KEYS = ( + "contact_depth", + "hand_position", + "foot_position", + "body_angle", + "camera_height", + "workspace_surface", + "clothing_visibility", + "expression_eye_detail", + "anatomy_shape_detail", +) +SCORE_KEYS = ( + "atlas_pose_match", + "contact_match", + "pose_ownership", + "workspace_continuity", + "clothing_visibility", + "subject_identity", + "expression_eye_control", + "anatomy_proportion", + "prompt_noise", +) +PROMOTION_PASS_VALUES = {"pass"} +PROMOTION_PROGRESS_VALUES = {"pass", "partial", "baseline"} +PROMOTION_REQUIRED_PASS_KEYS = ( + "pose_ownership", + "workspace_continuity", + "clothing_visibility", + "subject_identity", + "prompt_noise", +) +PROMOTION_REQUIRED_PROGRESS_KEYS = ( + "atlas_pose_match", + "contact_match", + "expression_eye_control", + "anatomy_proportion", +) +FORBIDDEN_PROMPT_FIELDS = ( + "negative", + "negative_prompt", + "negative_text", + "negative_channel", +) +PROMPT_OPTION_WORD_RE = re.compile(r"\b(?:either|or|may|optionally)\b", re.IGNORECASE) +PROMPT_NEGATIVE_CONDITIONING_RE = re.compile( + r"\b(?:do not|must not|should not|never|without|no)\b", + re.IGNORECASE, +) +PROMPT_META_PHRASES = ( + "keep the visible partner", + "visible partner and the action primary", + "context stays", + "camera layout", + "pov foreground clothing cue", + "pov foreground body cue", + "beside or behind the bodies", +) +PROMPT_DUPLICATE_PHRASE_RE = re.compile(r"[^.!?;]+(?:[.!?;]|$)") +PROMPT_DUPLICATE_MIN_WORDS = 6 +MIN_STABLE_MATRIX_SAMPLER_SEEDS = 2 +PROMPT_NOISE_CODES = ( + "option_word", + "negative_conditioning", + "meta_instruction", + "duplicate_phrase", +) + + +def _sha256_text(text: str) -> str: + return hashlib.sha256(text.encode("utf-8")).hexdigest() + + +def _known_variant_keys() -> list[str]: + return sorted(krea2_pose_variant_catalog.variant_keys(), key=len, reverse=True) + + +def _variant_key_from_stem(stem: str, known_keys: list[str]) -> str: + for key in known_keys: + if stem == key or stem.startswith(f"{key}_"): + return key + match = re.match(r"^(?P.+?)_\d+_?$", stem) + return match.group("key") if match else stem + + +def _files_by_stem(folder: Path, suffixes: set[str]) -> dict[str, Path]: + files: dict[str, Path] = {} + for path in sorted(folder.iterdir(), key=lambda item: item.name.lower()): + if path.is_file() and path.suffix.lower() in suffixes: + files[path.stem] = path + return files + + +def _seed_metadata() -> dict[str, None]: + return {key: None for key in SEED_METADATA_KEYS} + + +def _cue_axes() -> dict[str, None]: + return {key: None for key in CUE_AXIS_KEYS} + + +def _score_template() -> dict[str, None]: + return {key: None for key in SCORE_KEYS} + + +def _merge_known_values(defaults: dict[str, Any], raw: Any) -> dict[str, Any]: + merged = dict(defaults) + if not isinstance(raw, dict): + return merged + for key in merged: + if key in raw: + merged[key] = raw[key] + return merged + + +def _merge_non_null_known_values(defaults: dict[str, Any], raw: Any) -> dict[str, Any]: + merged = dict(defaults) + if not isinstance(raw, dict): + return merged + for key in merged: + value = raw.get(key) + if value is not None: + merged[key] = value + return merged + + +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 ValueError(f"{field} must not use {NEGATIVE_OUT_CHANNEL}") + if NEGATIVE_OUT_CHANNEL in text: + raise ValueError(f"{field} must not mention {NEGATIVE_OUT_CHANNEL}") + + +def _string_list(value: Any, *, field: str) -> list[str]: + if value is None: + return [] + if not isinstance(value, list): + raise ValueError(f"{field} must be a list of strings") + items: list[str] = [] + for index, item in enumerate(value): + text = _text(item) + if not text: + raise ValueError(f"{field}[{index}] must be a non-empty string") + _validate_no_negative_channel(text, field=f"{field}[{index}]") + items.append(text) + return items + + +def _reference_images(value: Any, *, field: str) -> list[str]: + refs = _string_list(value, field=field) + atlas_root = _atlas_root_path() + for index, ref in enumerate(refs): + path = Path(ref) + if path.is_absolute(): + raise ValueError(f"{field}[{index}] must be relative to the atlas root") + if ".." in path.parts: + raise ValueError(f"{field}[{index}] must not contain .. path segments") + if path.suffix.lower() != ".png": + raise ValueError(f"{field}[{index}] must reference a PNG image") + if atlas_root is not None and not (atlas_root / path).is_file(): + raise ValueError(f"{field}[{index}] missing atlas reference image: {atlas_root / path}") + return refs + + +def _atlas_root_path() -> Path | None: + try: + catalog = krea2_pose_variant_catalog.load_catalog() + except Exception: + return None + root_text = _text(catalog.get("atlas_root") if isinstance(catalog, dict) else "") + if not root_text: + return None + root = Path(root_text) + return root if root.is_dir() else None + + +def _atlas_relative_path(path_value: str | Path, *, atlas_root: Path, field: str) -> Path: + path = Path(path_value) + if path.is_absolute(): + try: + path = path.relative_to(atlas_root) + except ValueError as exc: + raise ValueError(f"{field} must be inside the atlas root {atlas_root}") from exc + if ".." in path.parts: + raise ValueError(f"{field} must not contain .. path segments") + return path + + +def _reference_image_id(path: Path) -> str: + stem = path.stem + return stem.split("_", 1)[0] + + +def _atlas_folder_images(atlas_root: Path, folder: str | Path, *, field: str) -> list[dict[str, Any]]: + relative_folder = _atlas_relative_path(folder, atlas_root=atlas_root, field=field) + folder_path = atlas_root / relative_folder + if not folder_path.is_dir(): + raise ValueError(f"{field} is missing atlas folder: {folder_path}") + images: list[dict[str, Any]] = [] + for path in sorted(folder_path.iterdir(), key=lambda item: item.name.lower()): + if not path.is_file() or path.suffix.lower() != ".png": + continue + relative_path = relative_folder / path.name + images.append( + { + "id": _reference_image_id(path), + "relative_path": relative_path.as_posix(), + "filename": path.name, + "size_bytes": path.stat().st_size, + } + ) + return images + + +def build_reference_pool_report(variant_key: str, *, supplemental_folders: list[str] | None = None) -> dict[str, Any]: + key = _text(variant_key) + if not key: + raise ValueError("variant_key is required") + atlas_root = _atlas_root_path() + if atlas_root is None: + raise ValueError("catalog atlas_root is missing or not readable") + variant = krea2_pose_variant_catalog.get_variant(key) + if not variant: + raise ValueError(f"unknown variant_key {key!r}") + canonical_folders = [str(folder) for folder in variant.get("atlas_folders") or [] if _text(folder)] + if not canonical_folders: + raise ValueError(f"variant {key!r} has no atlas_folders") + supplemental_folder_values = [str(folder) for folder in supplemental_folders or [] if _text(folder)] + + canonical_images: list[dict[str, Any]] = [] + for index, folder in enumerate(canonical_folders): + canonical_images.extend(_atlas_folder_images(atlas_root, folder, field=f"atlas_folders[{index}]")) + supplemental_images: list[dict[str, Any]] = [] + for index, folder in enumerate(supplemental_folder_values): + supplemental_images.extend(_atlas_folder_images(atlas_root, folder, field=f"supplemental_folders[{index}]")) + + canonical_by_id = {image["id"]: image for image in canonical_images} + supplemental_by_id = {image["id"]: image for image in supplemental_images} + matched_ids = sorted(set(canonical_by_id) & set(supplemental_by_id)) + supplemental_extra_ids = sorted(set(supplemental_by_id) - set(canonical_by_id)) + canonical_missing_ids = sorted(set(canonical_by_id) - set(supplemental_by_id)) + catalog_reference_images = _reference_images(variant.get("reference_images"), field=f"{key}.reference_images") + + return { + "schema": REFERENCE_POOL_REPORT_SCHEMA, + "variant_key": key, + "atlas_root": str(atlas_root), + "canonical_folders": canonical_folders, + "supplemental_folders": supplemental_folder_values, + "catalog_reference_images": catalog_reference_images, + "catalog_reference_count": len(catalog_reference_images), + "canonical_image_count": len(canonical_images), + "supplemental_image_count": len(supplemental_images), + "matched_image_count": len(matched_ids), + "supplemental_extra_count": len(supplemental_extra_ids), + "canonical_missing_supplemental_count": len(canonical_missing_ids), + "canonical_images": [image["relative_path"] for image in canonical_images], + "supplemental_images": [image["relative_path"] for image in supplemental_images], + "matched_images": [ + { + "id": image_id, + "canonical_image": canonical_by_id[image_id]["relative_path"], + "supplemental_image": supplemental_by_id[image_id]["relative_path"], + } + for image_id in matched_ids + ], + "supplemental_extra_images": [supplemental_by_id[image_id]["relative_path"] for image_id in supplemental_extra_ids], + "canonical_missing_supplemental_images": [canonical_by_id[image_id]["relative_path"] for image_id in canonical_missing_ids], + } + + +def _blank_review_cue_axes() -> dict[str, str]: + return {key: "" for key in CUE_AXIS_KEYS} + + +def _reference_review_item( + *, + image_id: str, + role: str, + canonical_image: str, + supplemental_image: str, + reference_images_template: list[str], +) -> dict[str, Any]: + return { + "id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "reference_images_template": list(reference_images_template), + "cue_axes": _blank_review_cue_axes(), + "observed_positive_cues": [], + "rejected_cues": [], + "review_notes": "", + "prompt_variant_template": { + "id": "", + "prompt_order": "subject_first", + "append_cues": [], + "reference_images": list(reference_images_template), + "cue_axes": _cue_axes(), + "seed_metadata": _seed_metadata(), + "notes": "", + }, + } + + +def build_reference_cue_review_sheet(variant_key: str, *, supplemental_folders: list[str] | None = None) -> dict[str, Any]: + report = build_reference_pool_report(variant_key, supplemental_folders=supplemental_folders) + catalog_reference_images = set(report.get("catalog_reference_images") or []) + matched_by_canonical = { + _text(item.get("canonical_image")): _text(item.get("supplemental_image")) + for item in report.get("matched_images") or [] + if isinstance(item, dict) + } + + review_items: list[dict[str, Any]] = [] + for canonical_image in report.get("canonical_images") or []: + canonical_text = _text(canonical_image) + if not canonical_text: + continue + role = "catalog_reference" if canonical_text in catalog_reference_images else "canonical_reference" + review_items.append( + _reference_review_item( + image_id=_reference_image_id(Path(canonical_text)), + role=role, + canonical_image=canonical_text, + supplemental_image=matched_by_canonical.get(canonical_text, ""), + reference_images_template=[canonical_text], + ) + ) + for supplemental_image in report.get("supplemental_extra_images") or []: + supplemental_text = _text(supplemental_image) + if not supplemental_text: + continue + review_items.append( + _reference_review_item( + image_id=_reference_image_id(Path(supplemental_text)), + role="supplemental_extra", + canonical_image="", + supplemental_image=supplemental_text, + reference_images_template=[], + ) + ) + + return { + "schema": REFERENCE_CUE_REVIEW_SHEET_SCHEMA, + "variant_key": report["variant_key"], + "atlas_root": report["atlas_root"], + "canonical_folders": report["canonical_folders"], + "supplemental_folders": report["supplemental_folders"], + "catalog_reference_count": report["catalog_reference_count"], + "canonical_image_count": report["canonical_image_count"], + "supplemental_image_count": report["supplemental_image_count"], + "matched_image_count": report["matched_image_count"], + "supplemental_extra_count": report["supplemental_extra_count"], + "review_item_count": len(review_items), + "instructions": ( + "Fill observed_positive_cues and cue_axes from visual review only. " + "Use canonical/catalog items for sidecar reference_images; use supplemental_extra items as cue-mining evidence until promoted." + ), + "review_items": review_items, + } + + +def _review_cue_axes(raw: Any, *, field: str) -> dict[str, Any]: + values = _cue_axes() + if not isinstance(raw, dict): + return values + for key in CUE_AXIS_KEYS: + value = _text(raw.get(key)) + if value: + _validate_no_negative_channel(value, field=f"{field}.{key}") + values[key] = value + return values + + +def _prompt_variant_id_from_review_item(item: dict[str, Any], *, field: str) -> str: + variant_id = _text(item.get("prompt_variant_id")) + template = item.get("prompt_variant_template") + if not variant_id and isinstance(template, dict): + variant_id = _text(template.get("id")) + if variant_id: + _validate_no_negative_channel(variant_id, field=f"{field}.prompt_variant_id") + return variant_id + + +def build_reference_cue_candidate_draft(reference_cue_review_sheet: dict[str, Any]) -> dict[str, Any]: + if not isinstance(reference_cue_review_sheet, dict): + raise ValueError("reference cue review sheet must be an object") + schema = _text(reference_cue_review_sheet.get("schema")) + if schema and schema != REFERENCE_CUE_REVIEW_SHEET_SCHEMA: + raise ValueError(f"reference cue review sheet schema must be {REFERENCE_CUE_REVIEW_SHEET_SCHEMA}") + review_items = reference_cue_review_sheet.get("review_items") + if not isinstance(review_items, list): + raise ValueError("reference cue review sheet review_items must be a list") + + variant_key = _text(reference_cue_review_sheet.get("variant_key")) + candidates: list[dict[str, Any]] = [] + skipped: list[dict[str, Any]] = [] + seen_variant_ids: set[str] = set() + + for index, item in enumerate(review_items): + if not isinstance(item, dict): + skipped.append({"index": index, "id": "", "reason": "invalid_review_item"}) + continue + field = f"review_items[{index}]" + image_id = _text(item.get("id")) + role = _text(item.get("role")) + canonical_image = _text(item.get("canonical_image")) + supplemental_image = _text(item.get("supplemental_image")) + cues = _string_list(item.get("observed_positive_cues"), field=f"{field}.observed_positive_cues") + if not cues: + skipped.append( + { + "index": index, + "id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "reason": "no_observed_positive_cues", + } + ) + continue + + variant_id = _prompt_variant_id_from_review_item(item, field=field) + template = item.get("prompt_variant_template") + template = template if isinstance(template, dict) else {} + exact_text = _text(template.get("text")) + prompt_noise_issues: list[dict[str, Any]] = [] + for cue_index, cue in enumerate(cues): + prompt_noise_issues.extend( + _prompt_noise_issues( + cue, + context="reference_cue_observed_positive_cue", + prompt_variant_id=variant_id, + cue_index=cue_index, + ) + ) + if exact_text: + prompt_noise_issues.extend( + _prompt_noise_issues( + exact_text, + context="reference_cue_exact_text", + prompt_variant_id=variant_id, + ) + ) + if prompt_noise_issues: + skipped.append( + { + "index": index, + "id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "reason": "prompt_noise_issue", + "prompt_noise_issues": prompt_noise_issues, + "prompt_noise_code_counts": _prompt_noise_code_counts(prompt_noise_issues), + } + ) + continue + + reference_images_template = _reference_images( + item.get("reference_images_template"), + field=f"{field}.reference_images_template", + ) + if role == "supplemental_extra" or not canonical_image: + skipped.append( + { + "index": index, + "id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "reason": "supplemental_extra_needs_canonical_reference", + "observed_positive_cues": cues, + "cue_axes": _review_cue_axes(item.get("cue_axes"), field=f"{field}.cue_axes"), + } + ) + continue + if not reference_images_template: + skipped.append( + { + "index": index, + "id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "reason": "missing_reference_images_template", + "observed_positive_cues": cues, + } + ) + continue + if not variant_id: + skipped.append( + { + "index": index, + "id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "reason": "missing_prompt_variant_id", + "observed_positive_cues": cues, + } + ) + continue + if variant_id in seen_variant_ids: + skipped.append( + { + "index": index, + "id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "prompt_variant_id": variant_id, + "reason": "duplicate_prompt_variant_id", + "observed_positive_cues": cues, + } + ) + continue + seen_variant_ids.add(variant_id) + + prompt_order = _text(template.get("prompt_order") or "subject_first") + if prompt_order not in PROMPT_ORDERS: + raise ValueError(f"{field}.prompt_variant_template.prompt_order must be one of {sorted(PROMPT_ORDERS)}") + cue_axes = _review_cue_axes(item.get("cue_axes"), field=f"{field}.cue_axes") + seed_metadata = _merge_known_values(_seed_metadata(), template.get("seed_metadata")) + notes = _text(template.get("notes") or item.get("review_notes")) + _validate_no_negative_channel(notes, field=f"{field}.notes") + prompt_variant = { + "id": variant_id, + "prompt_order": prompt_order, + "reference_images": reference_images_template, + "cue_axes": cue_axes, + "seed_metadata": seed_metadata, + "notes": notes, + } + if exact_text: + _validate_no_negative_channel(exact_text, field=f"{field}.prompt_variant_template.text") + prompt_variant["text"] = exact_text + else: + prompt_variant["append_cues"] = cues + candidates.append( + { + "variant_key": variant_key, + "reference_item_id": image_id, + "role": role, + "canonical_image": canonical_image, + "supplemental_image": supplemental_image, + "prompt_variant_id": variant_id, + "reference_images": reference_images_template, + "observed_positive_cues": cues, + "cue_axes": cue_axes, + "review_notes": _text(item.get("review_notes")), + "prompt_variant": prompt_variant, + } + ) + + return { + "schema": REFERENCE_CUE_CANDIDATE_DRAFT_SCHEMA, + "variant_key": variant_key, + "ready_candidate_count": len(candidates), + "skipped_count": len(skipped), + "instructions": ( + "Copy reviewed prompt_variant objects into same-stem sidecars only after choosing the matching baseline deck; " + "raw-only supplemental rows remain cue-mining evidence until paired with a canonical reference." + ), + "candidates": candidates, + "skipped": skipped, + } + + +def build_reference_cue_sidecar_author_draft( + manifest: dict[str, Any], + reference_cue_candidate_draft: dict[str, Any], + *, + variant_key: str = "", +) -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + schema = _text(reference_cue_candidate_draft.get("schema")) + if schema and schema != REFERENCE_CUE_CANDIDATE_DRAFT_SCHEMA: + raise ValueError(f"reference cue candidate draft schema must be {REFERENCE_CUE_CANDIDATE_DRAFT_SCHEMA}") + requested_variant_key = _text(variant_key or reference_cue_candidate_draft.get("variant_key")) + if not requested_variant_key: + raise ValueError("variant_key is required") + + candidate_variants: list[dict[str, Any]] = [] + skipped: list[dict[str, Any]] = [] + for candidate_index, candidate in enumerate(reference_cue_candidate_draft.get("candidates") or []): + if not isinstance(candidate, dict): + skipped.append({"candidate_index": candidate_index, "reason": "invalid_candidate"}) + continue + candidate_variant_key = _text(candidate.get("variant_key") or reference_cue_candidate_draft.get("variant_key")) + if candidate_variant_key and candidate_variant_key != requested_variant_key: + skipped.append( + { + "candidate_index": candidate_index, + "prompt_variant_id": _text(candidate.get("prompt_variant_id")), + "variant_key": candidate_variant_key, + "reason": "variant_key_mismatch", + } + ) + continue + prompt_variant = candidate.get("prompt_variant") + if not isinstance(prompt_variant, dict): + skipped.append( + { + "candidate_index": candidate_index, + "prompt_variant_id": _text(candidate.get("prompt_variant_id")), + "reason": "missing_prompt_variant", + } + ) + continue + variant_copy = dict(prompt_variant) + variant_id = _text(variant_copy.get("id")) + append_cues = _string_list(variant_copy.get("append_cues"), field=f"candidate prompt_variant {variant_id}.append_cues") + exact_text = _text(variant_copy.get("text")) + if variant_id and append_cues: + variant_copy.setdefault( + "prompt_source", + { + "kind": "append_cues", + "prompt_variant_id": variant_id, + "append_cues": list(append_cues), + }, + ) + elif variant_id and exact_text: + variant_copy.setdefault( + "prompt_source", + { + "kind": "text", + "prompt_variant_id": variant_id, + "tested_text_sha256": _sha256_text(exact_text), + }, + ) + candidate_variants.append(variant_copy) + + updates: list[dict[str, Any]] = [] + matching_entry_count = 0 + for entry in entries: + if not isinstance(entry, dict): + continue + entry_variant_key = _text(entry.get("variant_key")) + if entry_variant_key != requested_variant_key: + continue + matching_entry_count += 1 + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + if not bool(entry.get("known_variant")): + skipped.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "reason": "unknown_variant", + } + ) + continue + if not candidate_variants: + skipped.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "reason": "no_ready_candidates", + } + ) + continue + updates.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "sidecar_filename": f"{source_stem}{SIDECAR_SUFFIX}", + "source_prompt_sha256": _text(entry.get("prompt_sha256")), + "prompt_path": _text(entry.get("prompt_path")), + "image_path": _text(entry.get("image_path")), + "prompt_variants": [dict(variant) for variant in candidate_variants], + "notes": "Pre-test sidecar variants from reviewed atlas reference cue candidates.", + } + ) + if matching_entry_count == 0: + skipped.append( + { + "variant_key": requested_variant_key, + "reason": "no_matching_manifest_entry", + } + ) + + return { + "schema": REFERENCE_CUE_SIDECAR_AUTHOR_DRAFT_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": requested_variant_key, + "candidate_count": len(candidate_variants), + "update_count": len(updates), + "skipped_count": len(skipped), + "instructions": ( + "Validate, apply to the same manifest folder, then rebuild the manifest and run MCP fixed-seed prompt batches before promotion." + ), + "updates": updates, + "skipped": skipped, + } + + +def _prompt_variant_evidence(raw: Any, *, field: str) -> dict[str, Any]: + if raw is None: + return {} + if not isinstance(raw, dict): + raise ValueError(f"{field} must be an object") + evidence: dict[str, Any] = {} + if "seed" in raw: + evidence["seed"] = _int_seed(raw.get("seed"), field=f"{field}.seed") + if "turn" in raw: + turn = raw.get("turn") + if turn is not None and (not isinstance(turn, int) or isinstance(turn, bool)): + raise ValueError(f"{field}.turn must be an integer when present") + evidence["turn"] = turn + if "image_path" in raw: + evidence["image_path"] = _image_path(raw.get("image_path"), field=f"{field}.image_path") + if "score" in raw: + evidence["score"] = _merge_known_values(_score_template(), raw.get("score")) + reference_images = _reference_images(raw.get("reference_images"), field=f"{field}.reference_images") + if reference_images: + evidence["reference_images"] = reference_images + return evidence + + +def _stable_matrix_evidence(raw: Any) -> dict[str, Any]: + if not isinstance(raw, dict) or raw.get("stable") is not True: + return {} + try: + selection_seed = _int_seed(raw.get("selection_seed"), field="matrix_evidence.selection_seed") + seed_slot = _text(raw.get("seed_slot")) + if seed_slot not in SEED_SELECTION_SLOT_KEYS: + return {} + sampler_seeds_raw = raw.get("sampler_seeds") + if not isinstance(sampler_seeds_raw, list) or not sampler_seeds_raw: + return {} + sampler_seeds = [ + _int_seed(seed, field=f"matrix_evidence.sampler_seeds[{index}]") + for index, seed in enumerate(sampler_seeds_raw) + ] + if len(set(sampler_seeds)) != len(sampler_seeds): + return {} + if len(sampler_seeds) < MIN_STABLE_MATRIX_SAMPLER_SEEDS: + return {} + jobs_raw = raw.get("jobs") + if not isinstance(jobs_raw, list) or not jobs_raw: + return {} + if raw.get("job_count") != len(jobs_raw) or raw.get("promotion_ready_count") != len(jobs_raw) or raw.get("blocked_count") != 0: + return {} + seen_job_ids: set[str] = set() + job_sampler_seeds: list[int] = [] + for job_index, job in enumerate(jobs_raw): + if not isinstance(job, dict): + return {} + job_id = _text(job.get("id")) + if not job_id or job_id in seen_job_ids: + return {} + seen_job_ids.add(job_id) + if _text(job.get("decision")) != "seedable_candidate": + return {} + job_sampler_seed = _int_seed(job.get("sampler_seed"), field=f"matrix_evidence.jobs[{job_index}].sampler_seed") + if job_sampler_seed in job_sampler_seeds: + return {} + job_sampler_seeds.append(job_sampler_seed) + if _int_seed(job.get("selection_seed"), field=f"matrix_evidence.jobs[{job_index}].selection_seed") != selection_seed: + return {} + _image_path(job.get("image_path"), field=f"matrix_evidence.jobs[{job_index}].image_path") + turn = job.get("turn") + if not isinstance(turn, int) or isinstance(turn, bool): + return {} + decision, _blockers = _promotion_blockers(_merge_known_values(_score_template(), job.get("score"))) + if decision != "seedable_candidate": + return {} + if sorted(job_sampler_seeds) != sorted(sampler_seeds): + return {} + except ValueError: + return {} + return dict(raw) + + +def _stable_matrix_evidence_for_variant(variant: dict[str, Any], *, field: str) -> dict[str, Any]: + matrix_evidence = _stable_matrix_evidence(variant.get("matrix_evidence")) + if not matrix_evidence: + return {} + try: + seed_slot = _text(matrix_evidence.get("seed_slot")) + selection_seed = _int_seed(matrix_evidence.get("selection_seed"), field=f"{field}.matrix_evidence.selection_seed") + seed_metadata = _merge_known_values(_seed_metadata(), variant.get("seed_metadata")) + if _int_seed(seed_metadata.get(seed_slot), field=f"{field}.seed_metadata.{seed_slot}") != selection_seed: + return {} + except ValueError: + return {} + return matrix_evidence + + +def _prompt_source(raw: Any, *, field: str) -> dict[str, Any]: + if raw is None: + return {} + if not isinstance(raw, dict): + raise ValueError(f"{field} must be an object") + kind = _text(raw.get("kind")) + if kind not in {"baseline", "text", "append_cues"}: + raise ValueError(f"{field}.kind must be baseline, text, or append_cues") + source: dict[str, Any] = {"kind": kind} + prompt_variant_id = _text(raw.get("prompt_variant_id")) + if prompt_variant_id: + _validate_no_negative_channel(prompt_variant_id, field=f"{field}.prompt_variant_id") + source["prompt_variant_id"] = prompt_variant_id + append_cues = _string_list(raw.get("append_cues"), field=f"{field}.append_cues") + if kind == "append_cues": + if not append_cues: + raise ValueError(f"{field}.append_cues is required when kind is append_cues") + source["append_cues"] = append_cues + elif append_cues: + source["append_cues"] = append_cues + tested_hash = _text(raw.get("tested_text_sha256")) + if tested_hash: + source["tested_text_sha256"] = tested_hash + return source + + +def _prompt_source_for_variant(variant: dict[str, Any], *, variant_id: str, text: str, append_cues: list[str]) -> dict[str, Any]: + source = _prompt_source(variant.get("prompt_source"), field=f"prompt variant {variant_id}.prompt_source") + if source: + source.setdefault("prompt_variant_id", variant_id) + source.setdefault("tested_text_sha256", _sha256_text(text)) + return source + if append_cues: + return { + "kind": "append_cues", + "prompt_variant_id": variant_id, + "append_cues": list(append_cues), + "tested_text_sha256": _sha256_text(text), + } + return { + "kind": "text", + "prompt_variant_id": variant_id, + "tested_text_sha256": _sha256_text(text), + } + + +def _prompt_variants(raw: Any) -> list[dict[str, Any]]: + if raw is None: + return [] + if not isinstance(raw, list): + raise ValueError("prompt_variants must be a list") + + variants: list[dict[str, Any]] = [] + seen_variant_ids: set[str] = set() + for index, item in enumerate(raw): + if not isinstance(item, dict): + raise ValueError(f"prompt_variants[{index}] must be an object") + for forbidden in FORBIDDEN_PROMPT_FIELDS: + if forbidden in item: + raise ValueError(f"prompt_variants[{index}] must not contain {forbidden}") + + variant_id = _text(item.get("id")) + if not variant_id: + raise ValueError(f"prompt_variants[{index}].id is required") + _validate_no_negative_channel(variant_id, field=f"prompt_variants[{index}].id") + if variant_id in seen_variant_ids: + raise ValueError(f"prompt_variants[{index}].id {variant_id!r} is duplicated") + seen_variant_ids.add(variant_id) + + prompt_order = _text(item.get("prompt_order") or "subject_first") + if prompt_order not in PROMPT_ORDERS: + raise ValueError(f"prompt_variants[{index}].prompt_order must be one of {sorted(PROMPT_ORDERS)}") + + text = _text(item.get("text")) + append_cues = _string_list(item.get("append_cues"), field=f"prompt_variants[{index}].append_cues") + if text: + _validate_no_negative_channel(text, field=f"prompt_variants[{index}].text") + if bool(text) == bool(append_cues): + raise ValueError(f"prompt_variants[{index}] must provide exactly one of text or append_cues") + + notes = _text(item.get("notes")) + _validate_no_negative_channel(notes, field=f"prompt_variants[{index}].notes") + variant: dict[str, Any] = { + "id": variant_id, + "prompt_order": prompt_order, + "cue_axes": _merge_known_values(_cue_axes(), item.get("cue_axes")), + "seed_metadata": _merge_known_values(_seed_metadata(), item.get("seed_metadata")), + "notes": notes, + } + evidence = _prompt_variant_evidence(item.get("evidence"), field=f"prompt_variants[{index}].evidence") + if evidence: + variant["evidence"] = evidence + reference_images = _reference_images(item.get("reference_images"), field=f"prompt_variants[{index}].reference_images") + if reference_images: + variant["reference_images"] = reference_images + matrix_evidence = item.get("matrix_evidence") + if isinstance(matrix_evidence, dict): + variant["matrix_evidence"] = dict(matrix_evidence) + prompt_source = _prompt_source(item.get("prompt_source"), field=f"prompt_variants[{index}].prompt_source") + if prompt_source: + source_variant_id = _text(prompt_source.get("prompt_variant_id")) + if source_variant_id and source_variant_id != variant_id: + raise ValueError( + f"prompt_variants[{index}].prompt_source.prompt_variant_id {source_variant_id!r} must match id {variant_id!r}" + ) + variant["prompt_source"] = prompt_source + if text: + variant["text"] = text + else: + variant["append_cues"] = append_cues + variants.append(variant) + return variants + + +def _sidecar_for_stem(folder: Path, stem: str) -> dict[str, Any]: + path = folder / f"{stem}{SIDECAR_SUFFIX}" + if not path.is_file(): + return {} + with path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + return data if isinstance(data, dict) else {} + + +def build_manifest(folder: str | Path, *, subject_id: str = "") -> dict[str, Any]: + root = Path(folder).resolve() + if not root.is_dir(): + raise FileNotFoundError(f"atlas refine folder does not exist: {root}") + prompt_files = _files_by_stem(root, PROMPT_SUFFIXES) + image_files = _files_by_stem(root, IMAGE_SUFFIXES) + known_keys = _known_variant_keys() + known_key_set = set(known_keys) + paired_stems = sorted(set(prompt_files) & set(image_files)) + missing_stems = sorted(set(prompt_files) ^ set(image_files)) + + entries: list[dict[str, Any]] = [] + for stem in paired_stems: + prompt_path = prompt_files[stem].resolve() + image_path = image_files[stem].resolve() + prompt_text = prompt_path.read_text(encoding="utf-8").strip() + variant_key = _variant_key_from_stem(stem, known_keys) + sidecar = _sidecar_for_stem(root, stem) + entries.append( + { + "id": stem.rstrip("_"), + "source_stem": stem, + "variant_key": variant_key, + "known_variant": variant_key in known_key_set, + "prompt_path": str(prompt_path), + "image_path": str(image_path), + "prompt_text": prompt_text, + "prompt_sha256": _sha256_text(prompt_text), + "image_size_bytes": image_path.stat().st_size, + "seed_metadata": _merge_known_values(_seed_metadata(), sidecar.get("seed_metadata")), + "cue_axes": _merge_known_values(_cue_axes(), sidecar.get("cue_axes")), + "score": _merge_known_values(_score_template(), sidecar.get("score")), + "prompt_variants": _prompt_variants(sidecar.get("prompt_variants")), + "notes": str(sidecar.get("notes") or ""), + } + ) + + missing_pairs: list[dict[str, str]] = [] + for stem in missing_stems: + prompt_path = prompt_files.get(stem) + image_path = image_files.get(stem) + missing_pairs.append( + { + "stem": stem, + "prompt_path": str(prompt_path.resolve()) if prompt_path else "", + "image_path": str(image_path.resolve()) if image_path else "", + } + ) + + return { + "schema": SCHEMA, + "root": str(root), + "subject_id": subject_id or root.name, + "entry_count": len(entries), + "missing_pair_count": len(missing_pairs), + "unknown_variant_count": sum(1 for entry in entries if not entry["known_variant"]), + "entries": entries, + "missing_pairs": missing_pairs, + } + + +def _int_seed(value: Any, *, field: str) -> int: + if not isinstance(value, int) or isinstance(value, bool): + raise ValueError(f"{field} must be an integer sampler seed") + return value + + +def _probe_list(raw: Any, *, field: str) -> list[dict[str, Any]]: + if not isinstance(raw, list) or not raw: + raise ValueError(f"{field} must be a non-empty list") + probes: list[dict[str, Any]] = [] + for index, item in enumerate(raw): + if not isinstance(item, dict): + raise ValueError(f"{field}[{index}] must be an object") + probes.append(item) + return probes + + +def _image_path(value: Any, *, field: str) -> str: + path_text = _text(value) + if not path_text: + raise ValueError(f"{field} is required") + path = Path(path_text) + if not path.is_absolute(): + raise ValueError(f"{field} must be absolute") + if path.suffix.lower() != ".png": + raise ValueError(f"{field} must reference a PNG artifact") + return path_text + + +def _entry_for_variant(manifest: dict[str, Any], variant_key: str) -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + for entry in entries: + if isinstance(entry, dict) and entry.get("variant_key") == variant_key: + return entry + raise ValueError(f"manifest does not contain variant_key {variant_key!r}") + + +def _append_cues(base_text: str, cues: list[str]) -> str: + text = _text(base_text) + if not text: + raise ValueError("source prompt text is required") + _validate_no_negative_channel(text, field="source prompt text") + for cue in cues: + if text[-1] not in ".!?": + text += "." + text += f" {cue}" + return re.sub(r"\s+", " ", text).strip() + + +def _probe_id(entry_id: Any, variant_id: str) -> str: + base_id = _text(entry_id) + if not base_id: + raise ValueError("source entry id is required") + return f"{base_id}__{variant_id}" + + +def _variant_id_from_probe_id(probe_id: str, source_entry_id: str) -> str: + prefix = f"{source_entry_id}__" + if source_entry_id and probe_id.startswith(prefix): + return probe_id[len(prefix):] + if "__" in probe_id: + return probe_id.rsplit("__", 1)[-1] + return probe_id + + +def _variant_prompt_text(base_prompt: str, variant: dict[str, Any], *, field: str) -> str: + text = _text(variant.get("text")) + if text: + _validate_no_negative_channel(text, field=f"{field}.text") + return text + append_cues = _string_list(variant.get("append_cues"), field=f"{field}.append_cues") + return _append_cues(base_prompt, append_cues) + + +def build_prompt_batch( + manifest: dict[str, Any], + variant_key: str, + *, + sampler_seed: int | None = None, + include_baseline: bool = True, +) -> dict[str, Any]: + entry = _entry_for_variant(manifest, variant_key) + seed_metadata = _merge_known_values(_seed_metadata(), entry.get("seed_metadata")) + seed = _int_seed(sampler_seed if sampler_seed is not None else seed_metadata.get("sampler_seed"), field="sampler_seed") + seed_metadata["sampler_seed"] = seed + prompt_text = _text(entry.get("prompt_text")) + _validate_no_negative_channel(prompt_text, field="prompt_text") + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + cue_axes = _merge_known_values(_cue_axes(), entry.get("cue_axes")) + + probes: list[dict[str, Any]] = [] + if include_baseline: + probes.append( + { + "id": _probe_id(entry_id, "baseline"), + "prompt_order": "subject_first", + "text": prompt_text, + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "cue_axes": cue_axes, + "seed_metadata": seed_metadata, + "prompt_source": { + "kind": "baseline", + "tested_text_sha256": _sha256_text(prompt_text), + }, + "notes": "baseline", + } + ) + + for variant in entry.get("prompt_variants") or []: + if not isinstance(variant, dict): + raise ValueError("entry prompt_variants must contain objects") + variant_id = _text(variant.get("id")) + if not variant_id: + raise ValueError("entry prompt variant id is required") + prompt_order = _text(variant.get("prompt_order") or "subject_first") + if prompt_order not in PROMPT_ORDERS: + raise ValueError(f"entry prompt variant prompt_order must be one of {sorted(PROMPT_ORDERS)}") + exact_text = _text(variant.get("text")) + append_cues = _string_list(variant.get("append_cues"), field=f"entry prompt variant {variant_id}.append_cues") + if bool(exact_text) == bool(append_cues): + raise ValueError(f"entry prompt variant {variant_id} must provide exactly one of text or append_cues") + text = _variant_prompt_text(prompt_text, variant, field=f"entry prompt variant {variant_id}") + _validate_no_negative_channel(text, field=f"entry prompt variant {variant_id}.text") + prompt_source = _prompt_source_for_variant( + variant, + variant_id=variant_id, + text=text, + append_cues=append_cues, + ) + variant_seed_metadata = _merge_non_null_known_values(seed_metadata, variant.get("seed_metadata")) + variant_seed_metadata["sampler_seed"] = seed + probe = { + "id": _probe_id(entry_id, variant_id), + "prompt_order": prompt_order, + "text": text, + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "cue_axes": _merge_non_null_known_values(cue_axes, variant.get("cue_axes")), + "seed_metadata": variant_seed_metadata, + "evidence": _prompt_variant_evidence(variant.get("evidence"), field=f"entry prompt variant {variant_id}.evidence"), + "prompt_source": prompt_source, + "notes": _text(variant.get("notes")), + } + reference_images = _reference_images(variant.get("reference_images"), field=f"entry prompt variant {variant_id}.reference_images") + if reference_images: + probe["reference_images"] = reference_images + matrix_evidence = _stable_matrix_evidence_for_variant(variant, field=f"entry prompt variant {variant_id}") + if matrix_evidence: + probe["matrix_evidence"] = matrix_evidence + probes.append(probe) + + if not probes: + raise ValueError("prompt batch would contain no probes") + return { + "schema": BATCH_SCHEMA, + "seed": seed, + "channel_out": DEFAULT_OUT_CHANNEL, + "channel_in": DEFAULT_IN_CHANNEL, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "source_prompt_sha256": _text(entry.get("prompt_sha256")), + "probes": probes, + } + + +def select_seeded_prompt_variant( + manifest: dict[str, Any], + variant_key: str, + *, + selection_seed: int, + seed_slot: str = "atlas_cue_seed", +) -> dict[str, Any]: + seed = _int_seed(selection_seed, field="selection_seed") + if seed_slot not in SEED_SELECTION_SLOT_KEYS: + raise ValueError(f"seed_slot must be one of {list(SEED_SELECTION_SLOT_KEYS)} and must not be sampler_seed") + entry = _entry_for_variant(manifest, variant_key) + prompt_text = _text(entry.get("prompt_text")) + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + eligible: list[dict[str, Any]] = [] + ineligible: list[dict[str, Any]] = [] + + for variant in entry.get("prompt_variants") or []: + if not isinstance(variant, dict): + continue + variant_id = _text(variant.get("id")) + if not variant_id: + continue + evidence = _prompt_variant_evidence(variant.get("evidence"), field=f"prompt variant {variant_id}.evidence") + score = _merge_known_values(_score_template(), evidence.get("score")) + decision, blockers = _promotion_blockers(score) + if decision != "seedable_candidate": + reason = "missing_seedable_evidence" if blockers else "not_seedable" + if blockers: + reason += f": {', '.join(blockers)}" + ineligible.append( + { + "prompt_variant_id": variant_id, + "reason": reason, + "cue_axes": _merge_known_values(_cue_axes(), variant.get("cue_axes")), + "evidence": evidence, + } + ) + continue + matrix_evidence = _stable_matrix_evidence_for_variant(variant, field=f"prompt variant {variant_id}") + if "matrix_evidence" in variant and not matrix_evidence: + ineligible_item = { + "prompt_variant_id": variant_id, + "reason": "unstable_matrix_evidence", + "cue_axes": _merge_known_values(_cue_axes(), variant.get("cue_axes")), + "evidence": evidence, + } + if isinstance(variant.get("matrix_evidence"), dict): + ineligible_item["matrix_evidence"] = dict(variant["matrix_evidence"]) + ineligible.append(ineligible_item) + continue + append_cues = _string_list(variant.get("append_cues"), field=f"prompt variant {variant_id}.append_cues") + text = _variant_prompt_text(prompt_text, variant, field=f"prompt variant {variant_id}") + prompt_source = _prompt_source_for_variant( + variant, + variant_id=variant_id, + text=text, + append_cues=append_cues, + ) + candidate = { + "prompt_variant_id": variant_id, + "prompt_order": _text(variant.get("prompt_order") or "subject_first"), + "text": text, + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "cue_axes": _merge_known_values(_cue_axes(), variant.get("cue_axes")), + "seed_metadata": _merge_known_values(_seed_metadata(), variant.get("seed_metadata")), + "evidence": evidence, + "prompt_source": prompt_source, + "notes": _text(variant.get("notes")), + } + reference_images = _reference_images(variant.get("reference_images"), field=f"prompt variant {variant_id}.reference_images") + if reference_images: + candidate["reference_images"] = reference_images + if matrix_evidence: + candidate["matrix_evidence"] = matrix_evidence + eligible.append(candidate) + + eligible.sort(key=lambda candidate: _text(candidate.get("prompt_variant_id"))) + ineligible.sort(key=lambda candidate: _text(candidate.get("prompt_variant_id"))) + selected: dict[str, Any] = {} + selected_index = None + if eligible: + selected_index = seed % len(eligible) + selected = eligible[selected_index] + + return { + "schema": SEED_SELECTION_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "selection_seed": seed, + "seed_slot": seed_slot, + "eligible_candidate_count": len(eligible), + "ineligible_candidate_count": len(ineligible), + "selected_index": selected_index, + "selected": selected, + "eligible": eligible, + "ineligible": ineligible, + } + + +def build_seed_selected_prompt_batch( + manifest: dict[str, Any], + variant_key: str, + *, + selection_seed: int, + sampler_seed: int, + seed_slot: str = "atlas_cue_seed", + include_baseline: bool = True, +) -> dict[str, Any]: + seed = _int_seed(sampler_seed, field="sampler_seed") + selection = select_seeded_prompt_variant( + manifest, + variant_key, + selection_seed=selection_seed, + seed_slot=seed_slot, + ) + selected = selection.get("selected") + if not isinstance(selected, dict) or not selected: + raise ValueError(f"no seedable prompt variant is available for {variant_key!r}") + entry = _entry_for_variant(manifest, variant_key) + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + prompt_text = _text(entry.get("prompt_text")) + _validate_no_negative_channel(prompt_text, field="prompt_text") + entry_seed_metadata = _merge_known_values(_seed_metadata(), entry.get("seed_metadata")) + entry_seed_metadata["sampler_seed"] = seed + selected_seed_metadata = _merge_known_values(entry_seed_metadata, selected.get("seed_metadata")) + selected_seed_metadata["sampler_seed"] = seed + selected_seed_metadata[seed_slot] = selection["selection_seed"] + + probes: list[dict[str, Any]] = [] + if include_baseline: + probes.append( + { + "id": _probe_id(entry_id, "baseline"), + "prompt_order": "subject_first", + "text": prompt_text, + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "cue_axes": _merge_known_values(_cue_axes(), entry.get("cue_axes")), + "seed_metadata": entry_seed_metadata, + "prompt_source": { + "kind": "baseline", + "tested_text_sha256": _sha256_text(prompt_text), + }, + "notes": "baseline", + } + ) + selected_id = _text(selected.get("prompt_variant_id")) + selected_text = _text(selected.get("text")) + if not selected_id or not selected_text: + raise ValueError("selected prompt variant id and text are required") + _validate_no_negative_channel(selected_text, field="selected prompt text") + selected_probe = { + "id": _probe_id(entry_id, selected_id), + "prompt_order": _text(selected.get("prompt_order") or "subject_first"), + "text": selected_text, + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "cue_axes": _merge_known_values(_cue_axes(), selected.get("cue_axes")), + "seed_metadata": selected_seed_metadata, + "evidence": _prompt_variant_evidence(selected.get("evidence"), field=f"selected prompt variant {selected_id}.evidence"), + "prompt_source": _prompt_source(selected.get("prompt_source"), field=f"selected prompt variant {selected_id}.prompt_source"), + "selection": { + "selection_seed": selection["selection_seed"], + "seed_slot": selection["seed_slot"], + "selected_index": selection["selected_index"], + "prompt_variant_id": selected_id, + }, + "notes": _text(selected.get("notes")), + } + reference_images = _reference_images(selected.get("reference_images"), field=f"selected prompt variant {selected_id}.reference_images") + if reference_images: + selected_probe["reference_images"] = reference_images + matrix_evidence = _stable_matrix_evidence_for_variant(selected, field=f"selected prompt variant {selected_id}") + if matrix_evidence: + selected_probe["matrix_evidence"] = matrix_evidence + probes.append(selected_probe) + return { + "schema": BATCH_SCHEMA, + "seed": seed, + "channel_out": DEFAULT_OUT_CHANNEL, + "channel_in": DEFAULT_IN_CHANNEL, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "source_prompt_sha256": _text(entry.get("prompt_sha256")), + "selection": selection, + "probes": probes, + } + + +def build_seed_matrix( + manifest: dict[str, Any], + variant_key: str, + *, + selection_seeds: list[int], + sampler_seeds: list[int], + seed_slot: str = "atlas_cue_seed", +) -> dict[str, Any]: + if not selection_seeds: + raise ValueError("selection_seeds must contain at least one cue seed") + if not sampler_seeds: + raise ValueError("sampler_seeds must contain at least one sampler seed") + if len(set(selection_seeds)) != len(selection_seeds): + raise ValueError("selection_seeds must not contain duplicate cue seeds") + if len(set(sampler_seeds)) != len(sampler_seeds): + raise ValueError("sampler_seeds must not contain duplicate sampler seeds") + jobs: list[dict[str, Any]] = [] + for sampler_index, sampler_seed in enumerate(sampler_seeds): + sampler_seed_value = _int_seed(sampler_seed, field=f"sampler_seeds[{sampler_index}]") + for selection_index, selection_seed in enumerate(selection_seeds): + selection_seed_value = _int_seed(selection_seed, field=f"selection_seeds[{selection_index}]") + batch = build_seed_selected_prompt_batch( + manifest, + variant_key, + selection_seed=selection_seed_value, + sampler_seed=sampler_seed_value, + seed_slot=seed_slot, + ) + probes = [probe for probe in batch.get("probes") or [] if isinstance(probe, dict)] + candidate_probe = probes[-1] if probes else {} + selection = dict(batch.get("selection")) if isinstance(batch.get("selection"), dict) else {} + selected = dict(selection.get("selected")) if isinstance(selection.get("selected"), dict) else {} + jobs.append( + { + "id": f"{variant_key}__sampler_{sampler_seed_value}__{seed_slot}_{selection_seed_value}", + "variant_key": variant_key, + "sampler_seed": sampler_seed_value, + "selection_seed": selection_seed_value, + "seed_slot": seed_slot, + "selected": selected, + "candidate_probe": candidate_probe, + "batch": batch, + } + ) + + return { + "schema": SEED_MATRIX_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": variant_key, + "seed_slot": seed_slot, + "sampler_seeds": list(sampler_seeds), + "selection_seeds": list(selection_seeds), + "sampler_seed_count": len(sampler_seeds), + "selection_seed_count": len(selection_seeds), + "job_count": len(jobs), + "jobs": jobs, + } + + +def _score_value(score: dict[str, Any], key: str) -> str: + return _text(score.get(key)).lower() + + +def _promotion_blockers(score: dict[str, Any]) -> tuple[str, list[str]]: + missing: list[str] = [] + failed: list[str] = [] + for key in PROMOTION_REQUIRED_PASS_KEYS: + value = _score_value(score, key) + if not value: + missing.append(key) + elif value not in PROMOTION_PASS_VALUES: + failed.append(f"{key}={value}") + for key in PROMOTION_REQUIRED_PROGRESS_KEYS: + value = _score_value(score, key) + if not value: + missing.append(key) + elif value not in PROMOTION_PROGRESS_VALUES: + failed.append(f"{key}={value}") + if missing: + return "needs_visual_score", missing + if failed: + return "rejected", failed + return "seedable_candidate", [] + + +def build_promotion_report(result_sheet: dict[str, Any]) -> dict[str, Any]: + probes = _probe_list(result_sheet.get("probes"), field="result sheet probes") + seed = _int_seed(result_sheet.get("seed"), field="result sheet seed") + baseline_probe_id = _text(result_sheet.get("baseline_probe_id") or probes[0].get("id")) + source_entry_id = _text(result_sheet.get("source_entry_id")) + source_stem = _text(result_sheet.get("source_stem") or source_entry_id) + candidates: list[dict[str, Any]] = [] + + for probe in probes: + probe_id = _text(probe.get("id")) + if not probe_id: + raise ValueError("result sheet probe id is required") + if probe_id == baseline_probe_id: + continue + text = _text(probe.get("text")) + if not text: + raise ValueError(f"result sheet probe {probe_id}.text is required") + _validate_no_negative_channel(text, field=f"result sheet probe {probe_id}.text") + probe_source_entry_id = _text(probe.get("source_entry_id") or source_entry_id) + prompt_variant_id = _variant_id_from_probe_id(probe_id, probe_source_entry_id) + prompt_noise_issues = _prompt_noise_issues( + text, + context="result_sheet_probe", + prompt_variant_id=prompt_variant_id, + ) + score = _merge_known_values(_score_template(), probe.get("score")) + decision, blockers = _promotion_blockers(score) + matrix_evidence = _stable_matrix_evidence_for_variant(probe, field=f"result sheet probe {probe_id}") + if decision == "seedable_candidate" and prompt_noise_issues: + decision = "rejected" + blockers = ["prompt_noise_issue"] + if decision == "seedable_candidate" and "matrix_evidence" in probe and not matrix_evidence: + decision = "rejected" + blockers = ["unstable_matrix_evidence"] + probe_source_stem = _text(probe.get("source_stem") or source_stem or probe_source_entry_id) + candidate = { + "id": probe_id, + "prompt_variant_id": prompt_variant_id, + "decision": decision, + "blockers": blockers, + "variant_key": _text(probe.get("variant_key") or result_sheet.get("variant_key")), + "source_entry_id": probe_source_entry_id, + "source_stem": probe_source_stem, + "seed": seed, + "prompt_order": _text(probe.get("prompt_order") or "subject_first"), + "text": text, + "turn": probe.get("turn"), + "image_path": _image_path(probe.get("image_path"), field=f"result sheet probe {probe_id}.image_path"), + "cue_axes": _merge_known_values(_cue_axes(), probe.get("cue_axes")), + "seed_metadata": _merge_known_values(_seed_metadata(), probe.get("seed_metadata")), + "score": score, + "prompt_source": _prompt_source(probe.get("prompt_source"), field=f"result sheet probe {probe_id}.prompt_source"), + "analysis_notes": _text(probe.get("analysis_notes")), + } + reference_images = _reference_images(probe.get("reference_images"), field=f"result sheet probe {probe_id}.reference_images") + if reference_images: + candidate["reference_images"] = reference_images + if prompt_noise_issues: + candidate["prompt_noise_issues"] = prompt_noise_issues + candidate["prompt_noise_code_counts"] = _prompt_noise_code_counts(prompt_noise_issues) + if matrix_evidence: + candidate["matrix_evidence"] = matrix_evidence + candidates.append(candidate) + + return { + "schema": PROMOTION_REPORT_SCHEMA, + "seed": seed, + "subject_id": _text(result_sheet.get("subject_id")), + "variant_key": _text(result_sheet.get("variant_key")), + "source_entry_id": source_entry_id, + "source_stem": source_stem, + "baseline_probe_id": baseline_probe_id, + "candidate_count": len(candidates), + "promotion_ready_count": sum(1 for candidate in candidates if candidate["decision"] == "seedable_candidate"), + "blocked_count": sum(1 for candidate in candidates if candidate["decision"] != "seedable_candidate"), + "required_pass_keys": list(PROMOTION_REQUIRED_PASS_KEYS), + "required_progress_keys": list(PROMOTION_REQUIRED_PROGRESS_KEYS), + "candidates": candidates, + } + + +def build_sidecar_update_draft(promotion_report: dict[str, Any]) -> dict[str, Any]: + candidates = _probe_list(promotion_report.get("candidates"), field="promotion report candidates") + seed = _int_seed(promotion_report.get("seed"), field="promotion report seed") + ready_candidates = [candidate for candidate in candidates if candidate.get("decision") == "seedable_candidate"] + updates_by_stem: dict[str, dict[str, Any]] = {} + + for candidate in ready_candidates: + candidate_id = _text(candidate.get("id")) + prompt_variant_id = _text(candidate.get("prompt_variant_id")) + if not candidate_id or not prompt_variant_id: + raise ValueError("seedable candidate id and prompt_variant_id are required") + text = _text(candidate.get("text")) + if not text: + raise ValueError(f"seedable candidate {candidate_id}.text is required") + _validate_no_negative_channel(text, field=f"seedable candidate {candidate_id}.text") + source_entry_id = _text(candidate.get("source_entry_id") or promotion_report.get("source_entry_id")) + source_stem = _text(candidate.get("source_stem") or promotion_report.get("source_stem") or source_entry_id) + if not source_stem: + raise ValueError(f"seedable candidate {candidate_id}.source_stem is required") + update = updates_by_stem.setdefault( + source_stem, + { + "source_entry_id": source_entry_id, + "source_stem": source_stem, + "sidecar_filename": f"{source_stem}{SIDECAR_SUFFIX}", + "variant_key": _text(candidate.get("variant_key") or promotion_report.get("variant_key")), + "prompt_variants": [], + }, + ) + prompt_variant = { + "id": prompt_variant_id, + "prompt_order": _text(candidate.get("prompt_order") or "subject_first"), + "text": text, + "cue_axes": _merge_known_values(_cue_axes(), candidate.get("cue_axes")), + "seed_metadata": _merge_known_values(_seed_metadata(), candidate.get("seed_metadata")), + "notes": _text(candidate.get("analysis_notes")), + "prompt_source": _prompt_source(candidate.get("prompt_source"), field=f"seedable candidate {candidate_id}.prompt_source"), + "evidence": { + "seed": seed, + "turn": candidate.get("turn"), + "image_path": _image_path(candidate.get("image_path"), field=f"seedable candidate {candidate_id}.image_path"), + "score": _merge_known_values(_score_template(), candidate.get("score")), + }, + } + reference_images = _reference_images(candidate.get("reference_images"), field=f"seedable candidate {candidate_id}.reference_images") + if reference_images: + prompt_variant["reference_images"] = reference_images + prompt_variant["evidence"]["reference_images"] = reference_images + matrix_evidence = _stable_matrix_evidence_for_variant(candidate, field=f"seedable candidate {candidate_id}") + if matrix_evidence: + prompt_variant["matrix_evidence"] = matrix_evidence + update["prompt_variants"].append(prompt_variant) + + updates = [updates_by_stem[key] for key in sorted(updates_by_stem)] + return { + "schema": SIDECAR_UPDATE_DRAFT_SCHEMA, + "seed": seed, + "subject_id": _text(promotion_report.get("subject_id")), + "variant_key": _text(promotion_report.get("variant_key")), + "ready_candidate_count": len(ready_candidates), + "skipped_candidate_count": len(candidates) - len(ready_candidates), + "update_count": len(updates), + "updates": updates, + } + + +def build_matrix_sidecar_update_draft(matrix_promotion_report: dict[str, Any]) -> dict[str, Any]: + schema = _text(matrix_promotion_report.get("schema")) + if schema and schema != SEED_MATRIX_PROMOTION_REPORT_SCHEMA: + raise ValueError(f"seed matrix promotion report schema must be {SEED_MATRIX_PROMOTION_REPORT_SCHEMA}") + jobs = [job for job in matrix_promotion_report.get("jobs") or [] if isinstance(job, dict)] + jobs_by_id = {_text(job.get("id")): job for job in jobs if _text(job.get("id"))} + updates_by_stem: dict[str, dict[str, Any]] = {} + skipped: list[dict[str, Any]] = [] + ready_group_count = 0 + + for group in matrix_promotion_report.get("groups") or []: + if not isinstance(group, dict): + continue + prompt_variant_id = _text(group.get("prompt_variant_id")) + selection_seed = group.get("selection_seed") + blockers = [_text(blocker) for blocker in group.get("blockers") or [] if _text(blocker)] + group_context = { + "variant_key": _text(group.get("variant_key") or matrix_promotion_report.get("variant_key")), + "source_entry_id": _text(group.get("source_entry_id")), + "source_stem": _text(group.get("source_stem") or group.get("source_entry_id")), + "prompt_variant_id": prompt_variant_id, + "prompt_text_sha256": _text(group.get("prompt_text_sha256")), + "selection_seed": selection_seed, + "seed_slot": _text(group.get("seed_slot") or matrix_promotion_report.get("seed_slot")), + "sampler_seeds": list(group.get("sampler_seeds") or []), + "blockers": blockers, + } + if group.get("stable") is not True: + skipped.append({**group_context, "reason": "unstable_matrix_group"}) + continue + group_job_ids = [_text(job_id) for job_id in group.get("job_ids") or [] if _text(job_id)] + duplicate_job_ids = sorted({job_id for job_id in group_job_ids if group_job_ids.count(job_id) > 1}) + if duplicate_job_ids: + raise ValueError( + f"stable matrix group {prompt_variant_id!r} job_ids contain duplicated ids: {', '.join(duplicate_job_ids)}" + ) + missing_job_ids = [job_id for job_id in group_job_ids if job_id not in jobs_by_id] + if missing_job_ids: + raise ValueError( + f"stable matrix group {prompt_variant_id!r} job_ids reference missing jobs: {', '.join(missing_job_ids)}" + ) + group_jobs = [jobs_by_id[job_id] for job_id in group_job_ids if job_id in jobs_by_id] + expected_selection_seed = _int_seed(selection_seed, field=f"stable matrix group {prompt_variant_id}.selection_seed") + expected_prompt_text_sha256 = group_context["prompt_text_sha256"] + if not expected_prompt_text_sha256 and group_jobs: + first_candidate = group_jobs[0].get("candidate") if isinstance(group_jobs[0].get("candidate"), dict) else {} + first_text = _text(first_candidate.get("text")) if isinstance(first_candidate, dict) else "" + expected_prompt_text_sha256 = _sha256_text(first_text) if first_text else "" + for job in group_jobs: + job_id = _text(job.get("id")) + job_candidate = job.get("candidate") if isinstance(job.get("candidate"), dict) else {} + job_text = _text(job_candidate.get("text")) if isinstance(job_candidate, dict) else "" + job_prompt_text_sha256 = _sha256_text(job_text) if job_text else _text(job.get("prompt_text_sha256")) + declared_job_text_sha256 = _text(job.get("prompt_text_sha256")) + if declared_job_text_sha256 and job_prompt_text_sha256 and declared_job_text_sha256 != job_prompt_text_sha256: + raise ValueError( + f"stable matrix group {prompt_variant_id!r} job {job_id!r} candidate prompt text " + f"{job_prompt_text_sha256!r} does not match job prompt_text_sha256; " + f"expected {declared_job_text_sha256!r}" + ) + identity_checks = ( + ("prompt_variant_id", prompt_variant_id, _text(job.get("prompt_variant_id"))), + ("prompt text", expected_prompt_text_sha256, job_prompt_text_sha256), + ("selection_seed", expected_selection_seed, _int_seed(job.get("selection_seed"), field=f"matrix job {job_id}.selection_seed")), + ("seed_slot", group_context["seed_slot"], _text(job.get("seed_slot"))), + ("variant_key", group_context["variant_key"], _text(job.get("variant_key"))), + ("source_entry_id", group_context["source_entry_id"], _text(job.get("source_entry_id"))), + ("source_stem", group_context["source_stem"], _text(job.get("source_stem") or job.get("source_entry_id"))), + ) + for field, expected_value, actual_value in identity_checks: + if expected_value and actual_value and actual_value != expected_value: + raise ValueError( + f"stable matrix group {prompt_variant_id!r} job_ids include job {job_id!r} " + f"with {field} {actual_value!r}, expected {expected_value!r}" + ) + declared_sampler_seeds = sorted( + {_int_seed(seed, field=f"stable matrix group {prompt_variant_id}.sampler_seeds") for seed in group_context["sampler_seeds"]} + ) + job_sampler_seeds = sorted( + {_int_seed(job.get("sampler_seed"), field=f"stable matrix group {prompt_variant_id}.job_ids sampler_seed") for job in group_jobs} + ) + if declared_sampler_seeds != job_sampler_seeds: + raise ValueError( + f"stable matrix group {prompt_variant_id!r} sampler_seeds {declared_sampler_seeds} " + f"do not match job_ids sampler coverage {job_sampler_seeds}" + ) + if len(job_sampler_seeds) < MIN_STABLE_MATRIX_SAMPLER_SEEDS: + raise ValueError( + f"stable matrix group {prompt_variant_id!r} sampler_seeds must include at least " + f"{MIN_STABLE_MATRIX_SAMPLER_SEEDS} unique sampler seeds" + ) + actual_job_count = len(group_jobs) + actual_promotion_ready_count = sum(1 for job in group_jobs if job.get("decision") == "seedable_candidate") + actual_blocked_count = actual_job_count - actual_promotion_ready_count + count_mismatches: list[str] = [] + for field, actual_value in ( + ("job_count", actual_job_count), + ("promotion_ready_count", actual_promotion_ready_count), + ("blocked_count", actual_blocked_count), + ): + if field in group and group.get(field) is not None: + try: + declared_value = int(group.get(field)) + except (TypeError, ValueError) as exc: + raise ValueError(f"stable matrix group {prompt_variant_id!r} {field} must be an integer") from exc + if declared_value != actual_value: + count_mismatches.append(f"{field} {declared_value} != job_ids count {actual_value}") + if count_mismatches: + raise ValueError(f"stable matrix group {prompt_variant_id!r} count mismatch: {'; '.join(count_mismatches)}") + ready_jobs = [job for job in group_jobs if job.get("decision") == "seedable_candidate"] + if not ready_jobs: + skipped.append({**group_context, "reason": "no_seedable_jobs"}) + continue + representative_job = ready_jobs[0] + candidate = representative_job.get("candidate") + if not isinstance(candidate, dict): + skipped.append({**group_context, "reason": "missing_representative_candidate"}) + continue + source_entry_id = _text(candidate.get("source_entry_id")) + source_stem = _text(candidate.get("source_stem") or source_entry_id) + if not source_stem: + skipped.append({**group_context, "reason": "missing_source_stem"}) + continue + text = _text(candidate.get("text")) + if not text: + skipped.append({**group_context, "reason": "missing_candidate_text"}) + continue + _validate_no_negative_channel(text, field=f"matrix candidate {prompt_variant_id}.text") + matrix_jobs: list[dict[str, Any]] = [] + for job in ready_jobs: + job_candidate = job.get("candidate") if isinstance(job.get("candidate"), dict) else {} + matrix_jobs.append( + { + "id": _text(job.get("id")), + "sampler_seed": _int_seed(job.get("sampler_seed"), field=f"matrix job {job.get('id')}.sampler_seed"), + "selection_seed": _int_seed(job.get("selection_seed"), field=f"matrix job {job.get('id')}.selection_seed"), + "decision": _text(job.get("decision")), + "turn": job_candidate.get("turn"), + "image_path": _image_path(job_candidate.get("image_path"), field=f"matrix job {job.get('id')}.image_path"), + "score": _merge_known_values(_score_template(), job_candidate.get("score")), + } + ) + matrix_evidence = { + "stable": True, + "selection_seed": expected_selection_seed, + "seed_slot": group_context["seed_slot"], + "sampler_seeds": declared_sampler_seeds, + "job_count": actual_job_count, + "promotion_ready_count": actual_promotion_ready_count, + "blocked_count": actual_blocked_count, + "jobs": matrix_jobs, + } + update = updates_by_stem.setdefault( + source_stem, + { + "source_entry_id": source_entry_id, + "source_stem": source_stem, + "sidecar_filename": f"{source_stem}{SIDECAR_SUFFIX}", + "variant_key": group_context["variant_key"], + "prompt_variants": [], + }, + ) + update["prompt_variants"].append( + { + "id": prompt_variant_id, + "prompt_order": _text(candidate.get("prompt_order") or "subject_first"), + "text": text, + "cue_axes": _merge_known_values(_cue_axes(), candidate.get("cue_axes")), + "seed_metadata": _merge_known_values(_seed_metadata(), candidate.get("seed_metadata")), + "notes": f"stable matrix evidence for {group_context['seed_slot']}={matrix_evidence['selection_seed']}", + "prompt_source": _prompt_source(candidate.get("prompt_source"), field=f"matrix candidate {prompt_variant_id}.prompt_source"), + "evidence": { + "seed": _int_seed(representative_job.get("sampler_seed"), field="representative matrix sampler_seed"), + "turn": candidate.get("turn"), + "image_path": _image_path(candidate.get("image_path"), field=f"matrix candidate {prompt_variant_id}.image_path"), + "score": _merge_known_values(_score_template(), candidate.get("score")), + }, + "matrix_evidence": matrix_evidence, + } + ) + reference_images = _reference_images(candidate.get("reference_images"), field=f"matrix candidate {prompt_variant_id}.reference_images") + if reference_images: + update["prompt_variants"][-1]["reference_images"] = reference_images + update["prompt_variants"][-1]["evidence"]["reference_images"] = reference_images + ready_group_count += 1 + + updates = [updates_by_stem[key] for key in sorted(updates_by_stem)] + return { + "schema": MATRIX_SIDECAR_UPDATE_DRAFT_SCHEMA, + "subject_id": _text(matrix_promotion_report.get("subject_id")), + "variant_key": _text(matrix_promotion_report.get("variant_key")), + "ready_group_count": ready_group_count, + "skipped_group_count": len(skipped), + "update_count": len(updates), + "updates": updates, + "skipped": skipped, + } + + +def build_catalog_cue_draft(manifest: dict[str, Any], *, variant_key: str = "") -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + requested_variant_key = _text(variant_key) + candidates: list[dict[str, Any]] = [] + skipped: list[dict[str, Any]] = [] + + for entry in entries: + entry_variant_key = _text(entry.get("variant_key")) + if requested_variant_key and entry_variant_key != requested_variant_key: + continue + prompt_text = _text(entry.get("prompt_text")) + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + for variant in entry.get("prompt_variants") or []: + if not isinstance(variant, dict): + continue + variant_id = _text(variant.get("id")) + if not variant_id: + continue + append_cues = _string_list(variant.get("append_cues"), field=f"catalog cue variant {variant_id}.append_cues") + tested_text = _variant_prompt_text(prompt_text, variant, field=f"catalog cue variant {variant_id}") + prompt_source = _prompt_source_for_variant( + variant, + variant_id=variant_id, + text=tested_text, + append_cues=append_cues, + ) + evidence = _prompt_variant_evidence(variant.get("evidence"), field=f"catalog cue variant {variant_id}.evidence") + score = _merge_known_values(_score_template(), evidence.get("score")) + decision, blockers = _promotion_blockers(score) + if decision != "seedable_candidate": + skipped.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "prompt_variant_id": variant_id, + "reason": "missing_seedable_evidence" if blockers else "not_seedable", + "blockers": blockers, + } + ) + continue + matrix_evidence = _stable_matrix_evidence_for_variant(variant, field=f"catalog cue variant {variant_id}") + if "matrix_evidence" in variant and not matrix_evidence: + skipped_item = { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "prompt_variant_id": variant_id, + "reason": "unstable_matrix_evidence", + "blockers": ["unstable_matrix_evidence"], + } + if isinstance(variant.get("matrix_evidence"), dict): + skipped_item["matrix_evidence"] = dict(variant["matrix_evidence"]) + skipped.append(skipped_item) + continue + if prompt_source.get("kind") != "append_cues" or not prompt_source.get("append_cues"): + skipped.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "prompt_variant_id": variant_id, + "reason": "not_append_cues", + } + ) + continue + candidate = { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "sidecar_filename": f"{source_stem}{SIDECAR_SUFFIX}", + "prompt_variant_id": variant_id, + "prompt_variant_cues": list(prompt_source.get("append_cues") or []), + "tested_text": tested_text, + "tested_text_sha256": _sha256_text(tested_text), + "cue_axes": _merge_known_values(_cue_axes(), variant.get("cue_axes")), + "seed_metadata": _merge_known_values(_seed_metadata(), variant.get("seed_metadata")), + "evidence": evidence, + "notes": _text(variant.get("notes")), + } + reference_images = _reference_images(variant.get("reference_images"), field=f"catalog cue variant {variant_id}.reference_images") + if reference_images: + candidate["reference_images"] = reference_images + if matrix_evidence: + candidate["matrix_evidence"] = matrix_evidence + candidates.append(candidate) + + return { + "schema": CATALOG_CUE_DRAFT_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": requested_variant_key, + "ready_cue_count": len(candidates), + "skipped_count": len(skipped), + "candidates": candidates, + "skipped": skipped, + } + + +def _coverage_state( + *, + known_variant: bool, + prompt_noise_issue_count: int, + prompt_variant_count: int, + seedable_count: int, + catalog_cue_count: int, + unscored_count: int, + rejected_count: int, +) -> tuple[str, str]: + if not known_variant: + return "unknown_variant", "map the prompt/image stem to a catalog variant before seed testing" + if prompt_noise_issue_count: + return "needs_prompt_cleanup", "clean option/meta/negative prompt wording before visual scoring or seed promotion" + if prompt_variant_count == 0: + return "baseline_only", "add reviewed sidecar prompt_variants from MCP atlas probes" + if catalog_cue_count: + return "ready_for_catalog_review", "review catalog cue draft before editing prompt_variant_cues" + if seedable_count: + return "ready_for_seed_selection", "use atlas_cue_seed selection or create catalog cue draft if append_cues are available" + if unscored_count: + return "needs_visual_score", "score returned images against atlas preservation gates" + if rejected_count: + return "rejected_only", "try new prompt variants; current variants failed preservation gates" + return "needs_prompt_variants", "add explicit prompt variants before seed selection" + + +def _score_state(score: dict[str, Any]) -> str: + decision, _blockers = _promotion_blockers(score) + if decision == "seedable_candidate": + return "scored_pass" + if decision == "needs_visual_score": + if any(_text(score.get(key)) for key in SCORE_KEYS): + return "partially_scored" + return "needs_visual_score" + return "scored_rejected" + + +def build_baseline_score_sheet(manifest: dict[str, Any], *, variant_key: str = "") -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + requested_variant_key = _text(variant_key) + sheet_entries: list[dict[str, Any]] = [] + state_counts = { + "scored_pass_count": 0, + "needs_visual_score_count": 0, + "partially_scored_count": 0, + "scored_rejected_count": 0, + } + + for entry in entries: + if not isinstance(entry, dict): + continue + entry_variant_key = _text(entry.get("variant_key")) + if requested_variant_key and entry_variant_key != requested_variant_key: + continue + score = _merge_known_values(_score_template(), entry.get("score")) + score_state = _score_state(score) + if score_state == "scored_pass": + state_counts["scored_pass_count"] += 1 + elif score_state == "needs_visual_score": + state_counts["needs_visual_score_count"] += 1 + elif score_state == "partially_scored": + state_counts["partially_scored_count"] += 1 + else: + state_counts["scored_rejected_count"] += 1 + entry_id = _text(entry.get("id")) + sheet_entries.append( + { + "id": entry_id, + "source_stem": _text(entry.get("source_stem") or entry_id), + "variant_key": entry_variant_key, + "known_variant": bool(entry.get("known_variant")), + "prompt_path": _text(entry.get("prompt_path")), + "image_path": _text(entry.get("image_path")), + "prompt_text": _text(entry.get("prompt_text")), + "prompt_sha256": _text(entry.get("prompt_sha256")), + "seed_metadata": _merge_known_values(_seed_metadata(), entry.get("seed_metadata")), + "cue_axes": _merge_known_values(_cue_axes(), entry.get("cue_axes")), + "score": score, + "score_state": score_state, + "analysis_notes": "", + } + ) + + return { + "schema": BASELINE_SCORE_SHEET_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": requested_variant_key, + "entry_count": len(sheet_entries), + "score_keys": list(SCORE_KEYS), + "unscored_count": state_counts["needs_visual_score_count"], + **state_counts, + "entries": sheet_entries, + } + + +def _prompt_noise_excerpt(text: str, start: int, end: int, *, radius: int = 56) -> str: + prefix_start = max(0, start - radius) + suffix_end = min(len(text), end + radius) + excerpt = text[prefix_start:suffix_end].strip() + if prefix_start: + excerpt = f"...{excerpt}" + if suffix_end < len(text): + excerpt = f"{excerpt}..." + return re.sub(r"\s+", " ", excerpt) + + +def _normalized_prompt_phrase(text: str) -> str: + phrase = re.sub(r"[.!?;]+$", "", _text(text).lower()).strip() + return re.sub(r"\s+", " ", phrase) + + +def _prompt_noise_issues( + text: str, + *, + context: str, + prompt_variant_id: str = "", + cue_index: int | None = None, +) -> list[dict[str, Any]]: + prompt_text = _text(text) + if not prompt_text: + return [] + issues: list[dict[str, Any]] = [] + for match in PROMPT_OPTION_WORD_RE.finditer(prompt_text): + issues.append( + { + "context": context, + "prompt_variant_id": prompt_variant_id, + "cue_index": cue_index, + "code": "option_word", + "match": match.group(0), + "message": "option-list wording makes atlas geometry ambiguous for Krea2", + "excerpt": _prompt_noise_excerpt(prompt_text, match.start(), match.end()), + } + ) + for match in PROMPT_NEGATIVE_CONDITIONING_RE.finditer(prompt_text): + issues.append( + { + "context": context, + "prompt_variant_id": prompt_variant_id, + "cue_index": cue_index, + "code": "negative_conditioning", + "match": match.group(0), + "message": "negative or policy wording should not be placed in positive atlas conditioning", + "excerpt": _prompt_noise_excerpt(prompt_text, match.start(), match.end()), + } + ) + lower_text = prompt_text.lower() + for phrase in PROMPT_META_PHRASES: + start = lower_text.find(phrase) + while start != -1: + end = start + len(phrase) + issues.append( + { + "context": context, + "prompt_variant_id": prompt_variant_id, + "cue_index": cue_index, + "code": "meta_instruction", + "match": prompt_text[start:end], + "message": "meta or policy wording should be rewritten as direct visible image description", + "excerpt": _prompt_noise_excerpt(prompt_text, start, end), + } + ) + start = lower_text.find(phrase, end) + seen_phrases: dict[str, tuple[int, int, str]] = {} + for match in PROMPT_DUPLICATE_PHRASE_RE.finditer(prompt_text): + phrase_text = match.group(0).strip() + normalized = _normalized_prompt_phrase(phrase_text) + if not normalized: + continue + word_count = len(re.findall(r"[a-z0-9']+", normalized)) + if word_count < PROMPT_DUPLICATE_MIN_WORDS: + continue + if normalized not in seen_phrases: + seen_phrases[normalized] = (match.start(), match.end(), phrase_text) + continue + issues.append( + { + "context": context, + "prompt_variant_id": prompt_variant_id, + "cue_index": cue_index, + "code": "duplicate_phrase", + "match": phrase_text, + "message": "repeated prompt phrase makes atlas geometry noisy for Krea2", + "excerpt": _prompt_noise_excerpt(prompt_text, match.start(), match.end()), + } + ) + return issues + + +def _prompt_noise_issues_for_entry(entry: dict[str, Any]) -> list[dict[str, Any]]: + entry_issues: list[dict[str, Any]] = [] + entry_issues.extend( + _prompt_noise_issues( + _text(entry.get("prompt_text")), + context="baseline_prompt", + ) + ) + for variant in entry.get("prompt_variants") or []: + if not isinstance(variant, dict): + continue + prompt_variant_id = _text(variant.get("id")) + exact_text = _text(variant.get("text")) + if exact_text: + entry_issues.extend( + _prompt_noise_issues( + exact_text, + context="prompt_variant_text", + prompt_variant_id=prompt_variant_id, + ) + ) + for cue_index, cue in enumerate(_string_list(variant.get("append_cues"), field=f"prompt noise variant {prompt_variant_id}.append_cues")): + entry_issues.extend( + _prompt_noise_issues( + cue, + context="prompt_variant_append_cue", + prompt_variant_id=prompt_variant_id, + cue_index=cue_index, + ) + ) + return entry_issues + + +def _prompt_noise_code_counts(issues: list[dict[str, Any]]) -> dict[str, int]: + counts = {code: 0 for code in PROMPT_NOISE_CODES} + for issue in issues: + code = _text(issue.get("code")) + if code in counts: + counts[code] += 1 + return counts + + +def build_prompt_noise_report(manifest: dict[str, Any], *, variant_key: str = "") -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + requested_variant_key = _text(variant_key) + report_entries: list[dict[str, Any]] = [] + issue_code_counts = {code: 0 for code in PROMPT_NOISE_CODES} + scanned_entry_count = 0 + + for entry in entries: + if not isinstance(entry, dict): + continue + entry_variant_key = _text(entry.get("variant_key")) + if requested_variant_key and entry_variant_key != requested_variant_key: + continue + scanned_entry_count += 1 + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + entry_issues = _prompt_noise_issues_for_entry(entry) + + if not entry_issues: + continue + for code, count in _prompt_noise_code_counts(entry_issues).items(): + issue_code_counts[code] += count + report_entries.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "known_variant": bool(entry.get("known_variant")), + "issue_count": len(entry_issues), + "issues": entry_issues, + } + ) + + issue_count = sum(entry.get("issue_count", 0) for entry in report_entries) + return { + "schema": PROMPT_NOISE_REPORT_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": requested_variant_key, + "entry_count": scanned_entry_count, + "clean_entry_count": scanned_entry_count - len(report_entries), + "issue_entry_count": len(report_entries), + "issue_count": issue_count, + "issue_code_counts": issue_code_counts, + "entries": report_entries, + } + + +def _sidecar_path_text(manifest: dict[str, Any], source_stem: str) -> str: + root_text = _text(manifest.get("root")) + if not root_text or not source_stem: + return "" + return str((Path(root_text).resolve() / f"{source_stem}{SIDECAR_SUFFIX}")) + + +def _cleanup_source_type(context: str) -> str: + if context == "baseline_prompt": + return "prompt_file" + if context == "prompt_variant_text": + return "sidecar_prompt_variant_text" + if context == "prompt_variant_append_cue": + return "sidecar_prompt_variant_append_cue" + return "unknown" + + +def _cleanup_item_for_context( + *, + manifest: dict[str, Any], + entry: dict[str, Any], + context: str, + prompt_variant_id: str = "", + cue_index: int | None = None, +) -> dict[str, Any]: + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + sidecar_filename = f"{source_stem}{SIDECAR_SUFFIX}" if source_stem else "" + source_type = _cleanup_source_type(context) + current_text = "" + source_path = "" + if context == "baseline_prompt": + current_text = _text(entry.get("prompt_text")) + source_path = _text(entry.get("prompt_path")) + sidecar_filename = "" + else: + source_path = _sidecar_path_text(manifest, source_stem) + for variant in entry.get("prompt_variants") or []: + if not isinstance(variant, dict): + continue + if _text(variant.get("id")) != prompt_variant_id: + continue + if context == "prompt_variant_text": + current_text = _text(variant.get("text")) + elif context == "prompt_variant_append_cue": + cues = _string_list(variant.get("append_cues"), field=f"cleanup prompt variant {prompt_variant_id}.append_cues") + if cue_index is not None and 0 <= cue_index < len(cues): + current_text = cues[cue_index] + break + + return { + "variant_key": _text(entry.get("variant_key")), + "source_entry_id": entry_id, + "source_stem": source_stem, + "source_prompt_sha256": _text(entry.get("prompt_sha256")), + "context": context, + "source_type": source_type, + "source_path": source_path, + "sidecar_filename": sidecar_filename, + "prompt_variant_id": prompt_variant_id, + "cue_index": cue_index, + "current_text": current_text, + "current_text_sha256": _sha256_text(current_text), + "replacement_text": "", + "cleanup_notes": "", + "manual_review_required": True, + "issues": [], + } + + +def build_prompt_cleanup_sheet(manifest: dict[str, Any], *, variant_key: str = "") -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + requested_variant_key = _text(variant_key) + cleanup_items: list[dict[str, Any]] = [] + issue_code_counts = {code: 0 for code in PROMPT_NOISE_CODES} + + for entry in entries: + if not isinstance(entry, dict): + continue + entry_variant_key = _text(entry.get("variant_key")) + if requested_variant_key and entry_variant_key != requested_variant_key: + continue + issues = _prompt_noise_issues_for_entry(entry) + if not issues: + continue + for code, count in _prompt_noise_code_counts(issues).items(): + issue_code_counts[code] += count + item_map: dict[tuple[str, str, int | None], dict[str, Any]] = {} + for issue in issues: + context = _text(issue.get("context")) + prompt_variant_id = _text(issue.get("prompt_variant_id")) + raw_cue_index = issue.get("cue_index") + cue_index = raw_cue_index if isinstance(raw_cue_index, int) and not isinstance(raw_cue_index, bool) else None + key = (context, prompt_variant_id, cue_index) + if key not in item_map: + item_map[key] = _cleanup_item_for_context( + manifest=manifest, + entry=entry, + context=context, + prompt_variant_id=prompt_variant_id, + cue_index=cue_index, + ) + item_map[key]["issues"].append(issue) + for key in sorted(item_map): + item = item_map[key] + item["issue_count"] = len(item.get("issues") or []) + cleanup_items.append(item) + + return { + "schema": PROMPT_CLEANUP_SHEET_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": requested_variant_key, + "cleanup_item_count": len(cleanup_items), + "issue_count": sum(item.get("issue_count", 0) for item in cleanup_items), + "issue_code_counts": issue_code_counts, + "instructions": "Fill replacement_text manually with direct positive visual wording; do not use this sheet to auto-invent cues.", + "cleanup_items": cleanup_items, + } + + +def validate_prompt_cleanup_sheet(sheet: dict[str, Any]) -> dict[str, Any]: + errors: list[str] = [] + warnings: list[str] = [] + schema = _text(sheet.get("schema")) + if schema and schema != PROMPT_CLEANUP_SHEET_SCHEMA: + errors.append(f"schema must be {PROMPT_CLEANUP_SHEET_SCHEMA}") + cleanup_items_raw = sheet.get("cleanup_items") + if not isinstance(cleanup_items_raw, list): + errors.append("cleanup_items must be a list") + cleanup_items_raw = [] + + validated_item_count = 0 + for item_index, item in enumerate(cleanup_items_raw): + prefix = f"cleanup_items[{item_index}]" + if not isinstance(item, dict): + errors.append(f"{prefix} must be an object") + continue + validated_item_count += 1 + context = _text(item.get("context")) + source_type = _text(item.get("source_type")) + expected_source_type = _cleanup_source_type(context) + if expected_source_type == "unknown": + errors.append(f"{prefix}.context is unsupported") + elif source_type != expected_source_type: + errors.append(f"{prefix}.source_type must be {expected_source_type}") + if not _text(item.get("variant_key")): + errors.append(f"{prefix}.variant_key is required") + if not _text(item.get("source_stem")): + errors.append(f"{prefix}.source_stem is required") + source_prompt_hash = _text(item.get("source_prompt_sha256")) + if not source_prompt_hash: + errors.append(f"{prefix}.source_prompt_sha256 is required") + current_text = _text(item.get("current_text")) + if not current_text: + errors.append(f"{prefix}.current_text is required") + current_text_hash = _text(item.get("current_text_sha256")) + if not current_text_hash: + errors.append(f"{prefix}.current_text_sha256 is required") + elif current_text and current_text_hash != _sha256_text(current_text): + errors.append(f"{prefix}.current_text_sha256 must match current_text") + if context == "baseline_prompt" and source_prompt_hash and current_text_hash and source_prompt_hash != current_text_hash: + errors.append(f"{prefix}.source_prompt_sha256 must match current_text_sha256 for baseline prompt cleanup") + replacement_text = _text(item.get("replacement_text")) + if not replacement_text: + errors.append(f"{prefix}.replacement_text is required") + elif replacement_text == current_text: + errors.append(f"{prefix}.replacement_text must change current_text") + else: + replacement_issues = _prompt_noise_issues( + replacement_text, + context=context or "cleanup_replacement", + prompt_variant_id=_text(item.get("prompt_variant_id")), + cue_index=item.get("cue_index") if isinstance(item.get("cue_index"), int) and not isinstance(item.get("cue_index"), bool) else None, + ) + if replacement_issues: + errors.append(f"{prefix}.replacement_text still has prompt-noise issues") + if context == "baseline_prompt": + source_path = _text(item.get("source_path")) + if not source_path: + errors.append(f"{prefix}.source_path is required for baseline prompt cleanup") + elif Path(source_path).suffix.lower() not in PROMPT_SUFFIXES: + errors.append(f"{prefix}.source_path must reference a prompt file") + elif context == "prompt_variant_text": + if not _text(item.get("prompt_variant_id")): + errors.append(f"{prefix}.prompt_variant_id is required for sidecar text cleanup") + if not _text(item.get("sidecar_filename")): + errors.append(f"{prefix}.sidecar_filename is required for sidecar text cleanup") + elif context == "prompt_variant_append_cue": + if not _text(item.get("prompt_variant_id")): + errors.append(f"{prefix}.prompt_variant_id is required for sidecar append-cue cleanup") + cue_index = item.get("cue_index") + if not isinstance(cue_index, int) or isinstance(cue_index, bool) or cue_index < 0: + errors.append(f"{prefix}.cue_index must be a non-negative integer") + if not _text(item.get("sidecar_filename")): + errors.append(f"{prefix}.sidecar_filename is required for sidecar append-cue cleanup") + if not item.get("manual_review_required"): + warnings.append(f"{prefix}.manual_review_required is not true") + + return { + "schema": PROMPT_CLEANUP_VALIDATION_SCHEMA, + "valid": not errors, + "error_count": len(errors), + "warning_count": len(warnings), + "cleanup_item_count": len(cleanup_items_raw), + "validated_item_count": validated_item_count, + "errors": errors, + "warnings": warnings, + } + + +def _path_is_under_root(path: Path, root: Path) -> bool: + try: + path.resolve().relative_to(root.resolve()) + except ValueError: + return False + return True + + +def _cleanup_target_path(item: dict[str, Any], root: Path) -> Path: + context = _text(item.get("context")) + if context == "baseline_prompt": + path = Path(_text(item.get("source_path"))).resolve() + else: + sidecar_filename = _text(item.get("sidecar_filename")) + if not sidecar_filename or Path(sidecar_filename).name != sidecar_filename: + raise ValueError(f"sidecar filename must be a plain filename: {sidecar_filename!r}") + path = (root / sidecar_filename).resolve() + if not _path_is_under_root(path, root): + raise ValueError(f"cleanup target must be inside {root}: {path}") + return path + + +def _replace_sidecar_prompt_variant_text(sidecar: dict[str, Any], item: dict[str, Any]) -> tuple[dict[str, Any], str]: + variants = sidecar.get("prompt_variants") + if not isinstance(variants, list): + raise ValueError("sidecar prompt_variants must be a list") + prompt_variant_id = _text(item.get("prompt_variant_id")) + current_text = _text(item.get("current_text")) + replacement_text = _text(item.get("replacement_text")) + context = _text(item.get("context")) + for variant in variants: + if not isinstance(variant, dict) or _text(variant.get("id")) != prompt_variant_id: + continue + if context == "prompt_variant_text": + actual_text = _text(variant.get("text")) + if actual_text not in {current_text, replacement_text}: + raise ValueError(f"sidecar variant {prompt_variant_id}.text has drifted") + variant["text"] = replacement_text + return sidecar, "sidecar_prompt_variant_text" + if context == "prompt_variant_append_cue": + cues = _string_list(variant.get("append_cues"), field=f"cleanup sidecar variant {prompt_variant_id}.append_cues") + cue_index = item.get("cue_index") + if not isinstance(cue_index, int) or isinstance(cue_index, bool) or cue_index < 0 or cue_index >= len(cues): + raise ValueError(f"sidecar variant {prompt_variant_id}.append_cues index is out of range") + if cues[cue_index] not in {current_text, replacement_text}: + raise ValueError(f"sidecar variant {prompt_variant_id}.append_cues[{cue_index}] has drifted") + cues[cue_index] = replacement_text + variant["append_cues"] = cues + return sidecar, "sidecar_prompt_variant_append_cue" + raise ValueError(f"sidecar prompt variant {prompt_variant_id!r} was not found") + + +def apply_prompt_cleanup_sheet(sheet: dict[str, Any], folder: str | Path) -> dict[str, Any]: + validation = validate_prompt_cleanup_sheet(sheet) + if not validation["valid"]: + return { + "schema": PROMPT_CLEANUP_APPLY_REPORT_SCHEMA, + "applied": False, + "root": str(Path(folder).resolve()), + "updated_file_count": 0, + "updated_files": [], + "validation": validation, + } + + root = Path(folder).resolve() + if not root.is_dir(): + raise FileNotFoundError(f"cleanup folder does not exist: {root}") + + updated_by_path: dict[str, dict[str, Any]] = {} + for item in sheet.get("cleanup_items", []): + if not isinstance(item, dict): + continue + target_path = _cleanup_target_path(item, root) + context = _text(item.get("context")) + current_text = _text(item.get("current_text")) + replacement_text = _text(item.get("replacement_text")) + if context == "baseline_prompt": + actual_text = target_path.read_text(encoding="utf-8").strip() + if actual_text not in {current_text, replacement_text}: + raise ValueError(f"prompt file has drifted: {target_path}") + target_path.write_text(replacement_text, encoding="utf-8") + source_type = "prompt_file" + else: + sidecar = _read_json_object_if_present(target_path) + sidecar, source_type = _replace_sidecar_prompt_variant_text(sidecar, item) + target_path.write_text(json.dumps(sidecar, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8") + + path_key = str(target_path) + if path_key not in updated_by_path: + updated_by_path[path_key] = { + "path": path_key, + "source_type": source_type, + "cleanup_item_count": 0, + } + updated_by_path[path_key]["cleanup_item_count"] += 1 + + updated_files = list(updated_by_path.values()) + return { + "schema": PROMPT_CLEANUP_APPLY_REPORT_SCHEMA, + "applied": True, + "root": str(root), + "updated_file_count": len(updated_files), + "updated_files": updated_files, + "validation": validation, + } + + +def build_coverage_report(manifest: dict[str, Any]) -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + + report_entries: list[dict[str, Any]] = [] + totals = { + "baseline_only_count": 0, + "needs_prompt_cleanup_count": 0, + "needs_visual_score_count": 0, + "ready_for_seed_selection_count": 0, + "ready_for_catalog_review_count": 0, + "unknown_variant_count": 0, + "rejected_only_count": 0, + "prompt_variant_count": 0, + "seedable_variant_count": 0, + "catalog_cue_candidate_count": 0, + "unscored_variant_count": 0, + "rejected_variant_count": 0, + "prompt_noise_issue_count": 0, + "prompt_noise_entry_count": 0, + } + + for entry in entries: + if not isinstance(entry, dict): + continue + variant_key = _text(entry.get("variant_key")) + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + known_variant = bool(entry.get("known_variant")) + prompt_text = _text(entry.get("prompt_text")) + prompt_variants = [variant for variant in entry.get("prompt_variants") or [] if isinstance(variant, dict)] + prompt_noise_issues = _prompt_noise_issues_for_entry(entry) + prompt_noise_issue_count = len(prompt_noise_issues) + prompt_noise_code_counts = _prompt_noise_code_counts(prompt_noise_issues) + + seedable_count = 0 + catalog_cue_count = 0 + unscored_count = 0 + rejected_count = 0 + prompt_variant_summaries: list[dict[str, Any]] = [] + + for variant in prompt_variants: + variant_id = _text(variant.get("id")) + if not variant_id: + continue + append_cues = _string_list(variant.get("append_cues"), field=f"coverage prompt variant {variant_id}.append_cues") + tested_text = _variant_prompt_text(prompt_text, variant, field=f"coverage prompt variant {variant_id}") + prompt_source = _prompt_source_for_variant( + variant, + variant_id=variant_id, + text=tested_text, + append_cues=append_cues, + ) + evidence = _prompt_variant_evidence(variant.get("evidence"), field=f"coverage prompt variant {variant_id}.evidence") + score = _merge_known_values(_score_template(), evidence.get("score")) + decision, blockers = _promotion_blockers(score) + matrix_evidence = _stable_matrix_evidence_for_variant(variant, field=f"coverage prompt variant {variant_id}") + if decision == "seedable_candidate" and "matrix_evidence" in variant and not matrix_evidence: + decision = "rejected" + blockers = ["unstable_matrix_evidence"] + if decision == "seedable_candidate": + seedable_count += 1 + if prompt_source.get("kind") == "append_cues" and prompt_source.get("append_cues"): + catalog_cue_count += 1 + elif decision == "needs_visual_score": + unscored_count += 1 + elif decision == "rejected": + rejected_count += 1 + prompt_variant_summaries.append( + { + "prompt_variant_id": variant_id, + "decision": decision, + "blockers": blockers, + "prompt_source_kind": prompt_source.get("kind") or "", + "has_append_cues": bool(prompt_source.get("append_cues")), + "has_evidence": bool(evidence), + "has_matrix_evidence": "matrix_evidence" in variant, + "matrix_evidence_stable": bool(matrix_evidence), + } + ) + + state, next_action = _coverage_state( + known_variant=known_variant, + prompt_noise_issue_count=prompt_noise_issue_count, + prompt_variant_count=len(prompt_variants), + seedable_count=seedable_count, + catalog_cue_count=catalog_cue_count, + unscored_count=unscored_count, + rejected_count=rejected_count, + ) + totals["prompt_variant_count"] += len(prompt_variants) + totals["seedable_variant_count"] += seedable_count + totals["catalog_cue_candidate_count"] += catalog_cue_count + totals["unscored_variant_count"] += unscored_count + totals["rejected_variant_count"] += rejected_count + totals["prompt_noise_issue_count"] += prompt_noise_issue_count + if prompt_noise_issue_count: + totals["prompt_noise_entry_count"] += 1 + if state == "baseline_only": + totals["baseline_only_count"] += 1 + elif state == "needs_prompt_cleanup": + totals["needs_prompt_cleanup_count"] += 1 + elif state == "needs_visual_score": + totals["needs_visual_score_count"] += 1 + elif state == "ready_for_seed_selection": + totals["ready_for_seed_selection_count"] += 1 + elif state == "ready_for_catalog_review": + totals["ready_for_catalog_review_count"] += 1 + elif state == "unknown_variant": + totals["unknown_variant_count"] += 1 + elif state == "rejected_only": + totals["rejected_only_count"] += 1 + + report_entries.append( + { + "id": entry_id, + "source_stem": source_stem, + "variant_key": variant_key, + "known_variant": known_variant, + "state": state, + "next_action": next_action, + "prompt_variant_count": len(prompt_variants), + "seedable_variant_count": seedable_count, + "catalog_cue_candidate_count": catalog_cue_count, + "unscored_variant_count": unscored_count, + "rejected_variant_count": rejected_count, + "prompt_noise_issue_count": prompt_noise_issue_count, + "prompt_noise_code_counts": prompt_noise_code_counts, + "prompt_variants": prompt_variant_summaries, + } + ) + + return { + "schema": COVERAGE_REPORT_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "entry_count": len(report_entries), + "missing_pair_count": int(manifest.get("missing_pair_count") or 0), + "manifest_unknown_variant_count": int(manifest.get("unknown_variant_count") or 0), + **totals, + "entries": report_entries, + } + + +def build_sidecar_scaffold(manifest: dict[str, Any], *, variant_key: str = "") -> dict[str, Any]: + entries = manifest.get("entries") + if not isinstance(entries, list): + raise ValueError("manifest entries must be a list") + requested_variant_key = _text(variant_key) + scaffolds: list[dict[str, Any]] = [] + skipped: list[dict[str, Any]] = [] + + for entry in entries: + if not isinstance(entry, dict): + continue + entry_variant_key = _text(entry.get("variant_key")) + if requested_variant_key and entry_variant_key != requested_variant_key: + continue + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + prompt_variant_count = len([variant for variant in entry.get("prompt_variants") or [] if isinstance(variant, dict)]) + if not bool(entry.get("known_variant")): + skipped.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "reason": "unknown_variant", + } + ) + continue + if prompt_variant_count: + skipped.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "reason": "has_prompt_variants", + "prompt_variant_count": prompt_variant_count, + } + ) + continue + + seed_metadata = _merge_known_values(_seed_metadata(), entry.get("seed_metadata")) + cue_axes = _merge_known_values(_cue_axes(), entry.get("cue_axes")) + score = _merge_known_values(_score_template(), entry.get("score")) + scaffolds.append( + { + "variant_key": entry_variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "sidecar_filename": f"{source_stem}{SIDECAR_SUFFIX}", + "source_prompt_sha256": _text(entry.get("prompt_sha256")), + "prompt_path": _text(entry.get("prompt_path")), + "image_path": _text(entry.get("image_path")), + "sidecar_json": { + "seed_metadata": seed_metadata, + "cue_axes": cue_axes, + "score": score, + "prompt_variants": [], + "notes": "Add user-authored prompt_variants here; do not add negative-conditioning fields.", + }, + "prompt_variant_template": { + "id": "", + "prompt_order": "subject_first", + "append_cues": [], + "reference_images": [], + "cue_axes": _cue_axes(), + "seed_metadata": _seed_metadata(), + "notes": "", + }, + } + ) + + return { + "schema": SIDECAR_SCAFFOLD_SCHEMA, + "subject_id": _text(manifest.get("subject_id")), + "variant_key": requested_variant_key, + "scaffold_count": len(scaffolds), + "skipped_count": len(skipped), + "scaffolds": scaffolds, + "skipped": skipped, + } + + +def _has_filled_axis(values: dict[str, Any], keys: tuple[str, ...]) -> bool: + return any(values.get(key) not in (None, "", [], {}) for key in keys) + + +def build_baseline_score_update_draft(baseline_score_sheet: dict[str, Any]) -> dict[str, Any]: + schema = _text(baseline_score_sheet.get("schema")) + if schema and schema != BASELINE_SCORE_SHEET_SCHEMA: + raise ValueError(f"baseline score sheet schema must be {BASELINE_SCORE_SHEET_SCHEMA}") + entries = baseline_score_sheet.get("entries") + if not isinstance(entries, list): + raise ValueError("baseline score sheet entries must be a list") + + updates: list[dict[str, Any]] = [] + skipped: list[dict[str, Any]] = [] + requested_variant_key = _text(baseline_score_sheet.get("variant_key")) + + for index, entry in enumerate(entries): + if not isinstance(entry, dict): + skipped.append({"entry_index": index, "reason": "not_object"}) + continue + entry_id = _text(entry.get("id")) + source_stem = _text(entry.get("source_stem") or entry_id) + variant_key = _text(entry.get("variant_key")) + skip_context = { + "entry_index": index, + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + } + if not source_stem: + skipped.append({**skip_context, "reason": "missing_source_stem"}) + continue + if not bool(entry.get("known_variant")): + skipped.append({**skip_context, "reason": "unknown_variant"}) + continue + score = _merge_known_values(_score_template(), entry.get("score")) + if not _has_filled_axis(score, SCORE_KEYS): + skipped.append({**skip_context, "reason": "no_score"}) + continue + analysis_notes = _text(entry.get("analysis_notes")) + _validate_no_negative_channel(analysis_notes, field=f"baseline score entry {source_stem}.analysis_notes") + score_state = _score_state(score) + updates.append( + { + "variant_key": variant_key, + "source_entry_id": entry_id, + "source_stem": source_stem, + "sidecar_filename": f"{source_stem}{SIDECAR_SUFFIX}", + "source_prompt_sha256": _text(entry.get("prompt_sha256") or entry.get("source_prompt_sha256")), + "prompt_path": _text(entry.get("prompt_path")), + "image_path": _text(entry.get("image_path")), + "seed_metadata": _merge_known_values(_seed_metadata(), entry.get("seed_metadata")), + "cue_axes": _merge_known_values(_cue_axes(), entry.get("cue_axes")), + "score": score, + "score_state": score_state, + "analysis_notes": analysis_notes, + } + ) + + return { + "schema": BASELINE_SCORE_UPDATE_DRAFT_SCHEMA, + "subject_id": _text(baseline_score_sheet.get("subject_id")), + "variant_key": requested_variant_key, + "update_count": len(updates), + "skipped_count": len(skipped), + "updates": updates, + "skipped": skipped, + } + + +def validate_baseline_score_update_draft(draft: dict[str, Any]) -> dict[str, Any]: + errors: list[str] = [] + warnings: list[str] = [] + schema = _text(draft.get("schema")) + if schema and schema != BASELINE_SCORE_UPDATE_DRAFT_SCHEMA: + errors.append(f"schema must be {BASELINE_SCORE_UPDATE_DRAFT_SCHEMA}") + updates_raw = draft.get("updates") + if not isinstance(updates_raw, list): + errors.append("updates must be a list") + updates_raw = [] + + validated_update_count = 0 + for update_index, update in enumerate(updates_raw): + if not isinstance(update, dict): + errors.append(f"updates[{update_index}] must be an object") + continue + validated_update_count += 1 + prefix = f"updates[{update_index}]" + for forbidden in (*FORBIDDEN_PROMPT_FIELDS, "prompt_variants"): + if forbidden in update: + errors.append(f"{prefix} must not contain {forbidden}") + variant_key = _text(update.get("variant_key")) + if not variant_key: + errors.append(f"{prefix}.variant_key is required") + source_stem = _text(update.get("source_stem")) + if not source_stem: + errors.append(f"{prefix}.source_stem is required") + expected_sidecar = f"{source_stem}{SIDECAR_SUFFIX}" if source_stem else "" + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename: + errors.append(f"{prefix}.sidecar_filename is required") + elif Path(sidecar_filename).name != sidecar_filename: + errors.append(f"{prefix}.sidecar_filename must be a plain filename") + elif expected_sidecar and sidecar_filename != expected_sidecar: + errors.append(f"{prefix}.sidecar_filename must be {expected_sidecar}") + if not _text(update.get("source_prompt_sha256")): + errors.append(f"{prefix}.source_prompt_sha256 is required") + image_path = _text(update.get("image_path")) + if image_path: + try: + _image_path(image_path, field=f"{prefix}.image_path") + except ValueError as exc: + errors.append(str(exc)) + score = _merge_known_values(_score_template(), update.get("score")) + if not _has_filled_axis(score, SCORE_KEYS): + errors.append(f"{prefix}.score must include at least one filled score") + continue + score_state = _score_state(score) + declared_score_state = _text(update.get("score_state")) + if declared_score_state and declared_score_state != score_state: + errors.append(f"{prefix}.score_state must be {score_state}") + if score_state == "partially_scored": + warnings.append(f"{prefix}.score is partially scored") + elif score_state == "scored_rejected": + warnings.append(f"{prefix}.score is rejected baseline evidence") + analysis_notes = _text(update.get("analysis_notes")) + try: + _validate_no_negative_channel(analysis_notes, field=f"{prefix}.analysis_notes") + except ValueError as exc: + errors.append(str(exc)) + + return { + "schema": BASELINE_SCORE_UPDATE_VALIDATION_SCHEMA, + "valid": not errors, + "error_count": len(errors), + "warning_count": len(warnings), + "update_count": len(updates_raw), + "validated_update_count": validated_update_count, + "errors": errors, + "warnings": warnings, + } + + +def validate_reference_cue_sidecar_author_draft(draft: dict[str, Any]) -> dict[str, Any]: + errors: list[str] = [] + warnings: list[str] = [] + schema = _text(draft.get("schema")) + if schema and schema != REFERENCE_CUE_SIDECAR_AUTHOR_DRAFT_SCHEMA: + errors.append(f"schema must be {REFERENCE_CUE_SIDECAR_AUTHOR_DRAFT_SCHEMA}") + updates_raw = draft.get("updates") + if not isinstance(updates_raw, list): + errors.append("updates must be a list") + updates_raw = [] + + validated_variant_count = 0 + for update_index, update in enumerate(updates_raw): + if not isinstance(update, dict): + errors.append(f"updates[{update_index}] must be an object") + continue + prefix = f"updates[{update_index}]" + variant_key = _text(update.get("variant_key")) + if not variant_key: + errors.append(f"{prefix}.variant_key is required") + source_stem = _text(update.get("source_stem")) + if not source_stem: + errors.append(f"{prefix}.source_stem is required") + expected_sidecar = f"{source_stem}{SIDECAR_SUFFIX}" if source_stem else "" + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename: + errors.append(f"{prefix}.sidecar_filename is required") + elif Path(sidecar_filename).name != sidecar_filename: + errors.append(f"{prefix}.sidecar_filename must be a plain filename") + elif expected_sidecar and sidecar_filename != expected_sidecar: + errors.append(f"{prefix}.sidecar_filename must be {expected_sidecar}") + if not _text(update.get("source_prompt_sha256")): + errors.append(f"{prefix}.source_prompt_sha256 is required") + image_path = _text(update.get("image_path")) + if image_path: + try: + _image_path(image_path, field=f"{prefix}.image_path") + except ValueError as exc: + errors.append(str(exc)) + + variants_raw = update.get("prompt_variants") + if not isinstance(variants_raw, list) or not variants_raw: + errors.append(f"{prefix}.prompt_variants must be a non-empty list") + continue + seen_variant_ids: set[str] = set() + for variant_index, variant in enumerate(variants_raw): + variant_prefix = f"{prefix}.prompt_variants[{variant_index}]" + if not isinstance(variant, dict): + errors.append(f"{variant_prefix} must be an object") + continue + validated_variant_count += 1 + for forbidden in FORBIDDEN_PROMPT_FIELDS: + if forbidden in variant: + errors.append(f"{variant_prefix} must not contain {forbidden}") + variant_id = _text(variant.get("id")) + if not variant_id: + errors.append(f"{variant_prefix}.id is required") + elif variant_id in seen_variant_ids: + errors.append(f"{variant_prefix}.id {variant_id!r} is duplicated in this sidecar author draft") + seen_variant_ids.add(variant_id) + if variant_id: + _validate_prompt_source_identity(variant, variant_id=variant_id, prefix=variant_prefix, errors=errors) + prompt_order = _text(variant.get("prompt_order") or "subject_first") + if prompt_order not in PROMPT_ORDERS: + errors.append(f"{variant_prefix}.prompt_order must be one of {sorted(PROMPT_ORDERS)}") + text = _text(variant.get("text")) + append_cues: list[str] = [] + try: + append_cues = _string_list(variant.get("append_cues"), field=f"{variant_prefix}.append_cues") + except ValueError as exc: + errors.append(str(exc)) + if bool(text) == bool(append_cues): + errors.append(f"{variant_prefix} must provide exactly one of text or append_cues") + if text: + try: + _validate_no_negative_channel(text, field=f"{variant_prefix}.text") + except ValueError as exc: + errors.append(str(exc)) + for cue_index, cue in enumerate(append_cues): + prompt_noise_issues = _prompt_noise_issues( + cue, + context="reference_cue_sidecar_author_append_cue", + prompt_variant_id=variant_id, + cue_index=cue_index, + ) + for issue in prompt_noise_issues: + errors.append( + f"{variant_prefix}.append_cues[{cue_index}] prompt_noise {issue.get('code')}: {issue.get('match')}" + ) + reference_images = _reference_images(variant.get("reference_images"), field=f"{variant_prefix}.reference_images") + if not reference_images: + errors.append(f"{variant_prefix}.reference_images must include at least one canonical atlas reference") + cue_axes = _merge_known_values(_cue_axes(), variant.get("cue_axes")) + if not _has_filled_axis(cue_axes, CUE_AXIS_KEYS): + errors.append(f"{variant_prefix}.cue_axes must include at least one filled cue axis") + if not _text(variant.get("notes")): + warnings.append(f"{variant_prefix}.notes is empty") + + return { + "schema": REFERENCE_CUE_SIDECAR_AUTHOR_VALIDATION_SCHEMA, + "valid": not errors, + "error_count": len(errors), + "warning_count": len(warnings), + "update_count": len(updates_raw), + "validated_variant_count": validated_variant_count, + "errors": errors, + "warnings": warnings, + } + + +def validate_sidecar_update_draft(draft: dict[str, Any]) -> dict[str, Any]: + errors: list[str] = [] + warnings: list[str] = [] + schema = _text(draft.get("schema")) + if schema and schema != SIDECAR_UPDATE_DRAFT_SCHEMA: + errors.append(f"schema must be {SIDECAR_UPDATE_DRAFT_SCHEMA}") + seed = draft.get("seed") + if not isinstance(seed, int) or isinstance(seed, bool): + errors.append("seed must be an integer sampler seed") + updates_raw = draft.get("updates") + if not isinstance(updates_raw, list): + errors.append("updates must be a list") + updates_raw = [] + + validated_variant_count = 0 + for update_index, update in enumerate(updates_raw): + if not isinstance(update, dict): + errors.append(f"updates[{update_index}] must be an object") + continue + source_stem = _text(update.get("source_stem")) + if not source_stem: + errors.append(f"updates[{update_index}].source_stem is required") + expected_sidecar = f"{source_stem}{SIDECAR_SUFFIX}" if source_stem else "" + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename: + errors.append(f"updates[{update_index}].sidecar_filename is required") + elif expected_sidecar and sidecar_filename != expected_sidecar: + errors.append(f"updates[{update_index}].sidecar_filename must be {expected_sidecar}") + variants_raw = update.get("prompt_variants") + if not isinstance(variants_raw, list) or not variants_raw: + errors.append(f"updates[{update_index}].prompt_variants must be a non-empty list") + continue + seen_variant_ids: set[str] = set() + for variant_index, variant in enumerate(variants_raw): + prefix = f"updates[{update_index}].prompt_variants[{variant_index}]" + if not isinstance(variant, dict): + errors.append(f"{prefix} must be an object") + continue + validated_variant_count += 1 + for forbidden in FORBIDDEN_PROMPT_FIELDS: + if forbidden in variant: + errors.append(f"{prefix} must not contain {forbidden}") + variant_id = _text(variant.get("id")) + if not variant_id: + errors.append(f"{prefix}.id is required") + elif variant_id in seen_variant_ids: + errors.append(f"{prefix}.id {variant_id!r} is duplicated in this sidecar update") + seen_variant_ids.add(variant_id) + if variant_id: + _validate_prompt_source_identity(variant, variant_id=variant_id, prefix=prefix, errors=errors) + prompt_order = _text(variant.get("prompt_order") or "subject_first") + if prompt_order not in PROMPT_ORDERS: + errors.append(f"{prefix}.prompt_order must be one of {sorted(PROMPT_ORDERS)}") + text = _text(variant.get("text")) + if not text: + errors.append(f"{prefix}.text is required") + elif NEGATIVE_OUT_CHANNEL in text: + errors.append(f"{prefix}.text must not mention {NEGATIVE_OUT_CHANNEL}") + cue_axes = _merge_known_values(_cue_axes(), variant.get("cue_axes")) + if not _has_filled_axis(cue_axes, CUE_AXIS_KEYS): + errors.append(f"{prefix}.cue_axes must include at least one filled cue axis") + evidence = variant.get("evidence") + if not isinstance(evidence, dict): + errors.append(f"{prefix}.evidence is required") + continue + evidence_seed = evidence.get("seed") + if not isinstance(evidence_seed, int) or isinstance(evidence_seed, bool): + errors.append(f"{prefix}.evidence.seed must be an integer sampler seed") + elif isinstance(seed, int) and not isinstance(seed, bool) and evidence_seed != seed: + errors.append(f"{prefix}.evidence.seed {evidence_seed} does not match draft seed {seed}") + try: + _image_path(evidence.get("image_path"), field=f"{prefix}.evidence.image_path") + except ValueError as exc: + errors.append(str(exc)) + score = _merge_known_values(_score_template(), evidence.get("score")) + decision, blockers = _promotion_blockers(score) + if decision != "seedable_candidate": + for blocker in blockers: + errors.append(f"{prefix}.evidence.score failed promotion gate: {blocker}") + if not _text(variant.get("notes")): + warnings.append(f"{prefix}.notes is empty") + + return { + "schema": SIDECAR_UPDATE_VALIDATION_SCHEMA, + "valid": not errors, + "error_count": len(errors), + "warning_count": len(warnings), + "update_count": len(updates_raw), + "validated_variant_count": validated_variant_count, + "errors": errors, + "warnings": warnings, + } + + +def validate_matrix_sidecar_update_draft(draft: dict[str, Any]) -> dict[str, Any]: + errors: list[str] = [] + warnings: list[str] = [] + schema = _text(draft.get("schema")) + if schema and schema != MATRIX_SIDECAR_UPDATE_DRAFT_SCHEMA: + errors.append(f"schema must be {MATRIX_SIDECAR_UPDATE_DRAFT_SCHEMA}") + updates_raw = draft.get("updates") + if not isinstance(updates_raw, list): + errors.append("updates must be a list") + updates_raw = [] + + validated_variant_count = 0 + for update_index, update in enumerate(updates_raw): + if not isinstance(update, dict): + errors.append(f"updates[{update_index}] must be an object") + continue + source_stem = _text(update.get("source_stem")) + if not source_stem: + errors.append(f"updates[{update_index}].source_stem is required") + expected_sidecar = f"{source_stem}{SIDECAR_SUFFIX}" if source_stem else "" + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename: + errors.append(f"updates[{update_index}].sidecar_filename is required") + elif Path(sidecar_filename).name != sidecar_filename: + errors.append(f"updates[{update_index}].sidecar_filename must be a plain filename") + elif expected_sidecar and sidecar_filename != expected_sidecar: + errors.append(f"updates[{update_index}].sidecar_filename must be {expected_sidecar}") + variants_raw = update.get("prompt_variants") + if not isinstance(variants_raw, list) or not variants_raw: + errors.append(f"updates[{update_index}].prompt_variants must be a non-empty list") + continue + seen_variant_ids: set[str] = set() + for variant_index, variant in enumerate(variants_raw): + prefix = f"updates[{update_index}].prompt_variants[{variant_index}]" + if not isinstance(variant, dict): + errors.append(f"{prefix} must be an object") + continue + validated_variant_count += 1 + for forbidden in FORBIDDEN_PROMPT_FIELDS: + if forbidden in variant: + errors.append(f"{prefix} must not contain {forbidden}") + variant_id = _text(variant.get("id")) + if not variant_id: + errors.append(f"{prefix}.id is required") + elif variant_id in seen_variant_ids: + errors.append(f"{prefix}.id {variant_id!r} is duplicated in this sidecar update") + seen_variant_ids.add(variant_id) + if variant_id: + _validate_prompt_source_identity(variant, variant_id=variant_id, prefix=prefix, errors=errors) + prompt_order = _text(variant.get("prompt_order") or "subject_first") + if prompt_order not in PROMPT_ORDERS: + errors.append(f"{prefix}.prompt_order must be one of {sorted(PROMPT_ORDERS)}") + text = _text(variant.get("text")) + if not text: + errors.append(f"{prefix}.text is required") + else: + try: + _validate_no_negative_channel(text, field=f"{prefix}.text") + except ValueError as exc: + errors.append(str(exc)) + cue_axes = _merge_known_values(_cue_axes(), variant.get("cue_axes")) + if not _has_filled_axis(cue_axes, CUE_AXIS_KEYS): + errors.append(f"{prefix}.cue_axes must include at least one filled cue axis") + + evidence = variant.get("evidence") + evidence_seed: int | None = None + evidence_image_path = "" + evidence_turn: Any = None + evidence_score: dict[str, Any] | None = None + if not isinstance(evidence, dict): + errors.append(f"{prefix}.evidence is required") + else: + try: + evidence_seed = _int_seed(evidence.get("seed"), field=f"{prefix}.evidence.seed") + except ValueError as exc: + errors.append(str(exc)) + evidence_turn = evidence.get("turn") + if not isinstance(evidence_turn, int) or isinstance(evidence_turn, bool): + errors.append(f"{prefix}.evidence.turn must be an integer") + try: + evidence_image_path = _image_path(evidence.get("image_path"), field=f"{prefix}.evidence.image_path") + except ValueError as exc: + errors.append(str(exc)) + evidence_score = _merge_known_values(_score_template(), evidence.get("score")) + decision, blockers = _promotion_blockers(evidence_score) + if decision != "seedable_candidate": + for blocker in blockers: + errors.append(f"{prefix}.evidence.score failed promotion gate: {blocker}") + + matrix_evidence = variant.get("matrix_evidence") + if not isinstance(matrix_evidence, dict): + errors.append(f"{prefix}.matrix_evidence is required") + continue + if matrix_evidence.get("stable") is not True: + errors.append(f"{prefix}.matrix_evidence.stable must be true") + try: + selection_seed = _int_seed(matrix_evidence.get("selection_seed"), field=f"{prefix}.matrix_evidence.selection_seed") + except ValueError as exc: + errors.append(str(exc)) + selection_seed = None + seed_slot = _text(matrix_evidence.get("seed_slot")) + if seed_slot not in SEED_SELECTION_SLOT_KEYS: + errors.append( + f"{prefix}.matrix_evidence.seed_slot must be one of {list(SEED_SELECTION_SLOT_KEYS)} and must not be sampler_seed" + ) + elif selection_seed is not None: + seed_metadata = _merge_known_values(_seed_metadata(), variant.get("seed_metadata")) + try: + seed_metadata_value = _int_seed( + seed_metadata.get(seed_slot), + field=f"{prefix}.seed_metadata.{seed_slot}", + ) + except ValueError as exc: + errors.append(str(exc)) + else: + if seed_metadata_value != selection_seed: + errors.append( + f"{prefix}.seed_metadata.{seed_slot} {seed_metadata_value} " + f"must match matrix_evidence.selection_seed {selection_seed}" + ) + sampler_seeds_raw = matrix_evidence.get("sampler_seeds") + sampler_seeds: list[int] = [] + if not isinstance(sampler_seeds_raw, list) or not sampler_seeds_raw: + errors.append(f"{prefix}.matrix_evidence.sampler_seeds must be a non-empty list") + else: + seen_declared_sampler_seeds: set[int] = set() + for seed_index, sampler_seed in enumerate(sampler_seeds_raw): + try: + declared_sampler_seed = _int_seed( + sampler_seed, + field=f"{prefix}.matrix_evidence.sampler_seeds[{seed_index}]", + ) + sampler_seeds.append(declared_sampler_seed) + if declared_sampler_seed in seen_declared_sampler_seeds: + errors.append( + f"{prefix}.matrix_evidence.sampler_seeds value {declared_sampler_seed} is duplicated" + ) + seen_declared_sampler_seeds.add(declared_sampler_seed) + except ValueError as exc: + errors.append(str(exc)) + if len(seen_declared_sampler_seeds) < MIN_STABLE_MATRIX_SAMPLER_SEEDS: + errors.append( + f"{prefix}.matrix_evidence.sampler_seeds must include at least " + f"{MIN_STABLE_MATRIX_SAMPLER_SEEDS} unique sampler seeds" + ) + jobs_raw = matrix_evidence.get("jobs") + if not isinstance(jobs_raw, list) or not jobs_raw: + errors.append(f"{prefix}.matrix_evidence.jobs must be a non-empty list") + jobs_raw = [] + + for count_field, expected_count in ( + ("job_count", len(jobs_raw)), + ("promotion_ready_count", len(jobs_raw)), + ): + count_value = matrix_evidence.get(count_field) + if not isinstance(count_value, int) or isinstance(count_value, bool): + errors.append(f"{prefix}.matrix_evidence.{count_field} must be an integer") + elif count_value != expected_count: + errors.append(f"{prefix}.matrix_evidence.{count_field} must equal matrix_evidence.jobs count") + blocked_count = matrix_evidence.get("blocked_count") + if blocked_count != 0: + errors.append(f"{prefix}.matrix_evidence.blocked_count must be 0") + job_sampler_seeds: list[int] = [] + seen_job_ids: set[str] = set() + seen_job_sampler_seeds: set[int] = set() + jobs_by_sampler_seed: dict[int, dict[str, Any]] = {} + for job_index, job in enumerate(jobs_raw): + job_prefix = f"{prefix}.matrix_evidence.jobs[{job_index}]" + if not isinstance(job, dict): + errors.append(f"{job_prefix} must be an object") + continue + job_id = _text(job.get("id")) + if not job_id: + errors.append(f"{job_prefix}.id is required") + elif job_id in seen_job_ids: + errors.append(f"{prefix}.matrix_evidence.jobs id {job_id!r} is duplicated") + seen_job_ids.add(job_id) + if _text(job.get("decision")) != "seedable_candidate": + errors.append(f"{job_prefix}.decision must be seedable_candidate") + try: + job_sampler_seed = _int_seed(job.get("sampler_seed"), field=f"{job_prefix}.sampler_seed") + job_sampler_seeds.append(job_sampler_seed) + if job_sampler_seed in seen_job_sampler_seeds: + errors.append(f"{prefix}.matrix_evidence.jobs sampler_seed {job_sampler_seed} is duplicated") + else: + jobs_by_sampler_seed[job_sampler_seed] = job + seen_job_sampler_seeds.add(job_sampler_seed) + if sampler_seeds and job_sampler_seed not in sampler_seeds: + errors.append(f"{job_prefix}.sampler_seed must be listed in matrix_evidence.sampler_seeds") + except ValueError as exc: + errors.append(str(exc)) + try: + job_selection_seed = _int_seed(job.get("selection_seed"), field=f"{job_prefix}.selection_seed") + if selection_seed is not None and job_selection_seed != selection_seed: + errors.append(f"{job_prefix}.selection_seed must match matrix_evidence.selection_seed") + except ValueError as exc: + errors.append(str(exc)) + try: + _image_path(job.get("image_path"), field=f"{job_prefix}.image_path") + except ValueError as exc: + errors.append(str(exc)) + turn = job.get("turn") + if not isinstance(turn, int) or isinstance(turn, bool): + errors.append(f"{job_prefix}.turn must be an integer") + job_score = _merge_known_values(_score_template(), job.get("score")) + decision, blockers = _promotion_blockers(job_score) + if decision != "seedable_candidate": + for blocker in blockers: + errors.append(f"{job_prefix}.score failed promotion gate: {blocker}") + if sampler_seeds and sorted(set(job_sampler_seeds)) != sorted(set(sampler_seeds)): + errors.append(f"{prefix}.matrix_evidence.jobs must cover every sampler seed") + if evidence_seed is not None and sampler_seeds and evidence_seed not in sampler_seeds: + errors.append(f"{prefix}.evidence.seed must be one of matrix_evidence.sampler_seeds") + if evidence_seed is not None: + representative_job = jobs_by_sampler_seed.get(evidence_seed) + if representative_job is None: + errors.append(f"{prefix}.evidence.seed must match a matrix_evidence.jobs sampler_seed") + else: + representative_prefix = f"{prefix}.matrix_evidence.jobs entry for evidence.seed {evidence_seed}" + try: + representative_image_path = _image_path( + representative_job.get("image_path"), + field=f"{representative_prefix}.image_path", + ) + except ValueError: + representative_image_path = "" + if evidence_image_path and representative_image_path and evidence_image_path != representative_image_path: + errors.append(f"{prefix}.evidence.image_path must match {representative_prefix}.image_path") + if evidence_turn != representative_job.get("turn"): + errors.append(f"{prefix}.evidence.turn must match {representative_prefix}.turn") + representative_score = _merge_known_values(_score_template(), representative_job.get("score")) + if evidence_score is not None and evidence_score != representative_score: + errors.append(f"{prefix}.evidence.score must match {representative_prefix}.score") + if not _text(variant.get("notes")): + warnings.append(f"{prefix}.notes is empty") + + return { + "schema": MATRIX_SIDECAR_UPDATE_VALIDATION_SCHEMA, + "valid": not errors, + "error_count": len(errors), + "warning_count": len(warnings), + "update_count": len(updates_raw), + "validated_variant_count": validated_variant_count, + "errors": errors, + "warnings": warnings, + } + + +def _read_json_object_if_present(path: Path) -> dict[str, Any]: + if not path.is_file(): + return {} + with path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + if not isinstance(data, dict): + raise ValueError(f"{path} must contain one JSON object") + return data + + +def _validate_prompt_source_identity(variant: dict[str, Any], *, variant_id: str, prefix: str, errors: list[str]) -> None: + prompt_source = variant.get("prompt_source") + if prompt_source is None: + return + if not isinstance(prompt_source, dict): + errors.append(f"{prefix}.prompt_source must be an object") + return + source_variant_id = _text(prompt_source.get("prompt_variant_id")) + if source_variant_id and source_variant_id != variant_id: + errors.append(f"{prefix}.prompt_source.prompt_variant_id {source_variant_id!r} must match id {variant_id!r}") + + +def apply_baseline_score_update_draft(draft: dict[str, Any], folder: str | Path) -> dict[str, Any]: + validation = validate_baseline_score_update_draft(draft) + if not validation["valid"]: + return { + "schema": BASELINE_SCORE_APPLY_REPORT_SCHEMA, + "applied": False, + "root": str(Path(folder).resolve()), + "updated_file_count": 0, + "updated_files": [], + "validation": validation, + } + + root = Path(folder).resolve() + if not root.is_dir(): + raise FileNotFoundError(f"sidecar folder does not exist: {root}") + updated_files: list[dict[str, Any]] = [] + for update in draft.get("updates", []): + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename or Path(sidecar_filename).name != sidecar_filename: + raise ValueError(f"sidecar filename must be a plain filename: {sidecar_filename!r}") + sidecar_path = root / sidecar_filename + sidecar = _read_json_object_if_present(sidecar_path) + score = _merge_known_values(_score_template(), update.get("score")) + sidecar["seed_metadata"] = _merge_known_values(_seed_metadata(), update.get("seed_metadata")) + sidecar["cue_axes"] = _merge_known_values(_cue_axes(), update.get("cue_axes")) + sidecar["score"] = score + sidecar["baseline_score_state"] = _score_state(score) + sidecar["baseline_source_prompt_sha256"] = _text(update.get("source_prompt_sha256")) + sidecar["baseline_analysis_notes"] = _text(update.get("analysis_notes")) + sidecar_path.write_text(json.dumps(sidecar, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8") + updated_files.append( + { + "sidecar_filename": sidecar_filename, + "sidecar_path": str(sidecar_path), + "score_state": sidecar["baseline_score_state"], + } + ) + + return { + "schema": BASELINE_SCORE_APPLY_REPORT_SCHEMA, + "applied": True, + "root": str(root), + "updated_file_count": len(updated_files), + "updated_files": updated_files, + "validation": validation, + } + + +def _upsert_prompt_variants(existing: Any, incoming: list[dict[str, Any]]) -> list[dict[str, Any]]: + if existing is None: + variants: list[dict[str, Any]] = [] + elif not isinstance(existing, list): + raise ValueError("existing sidecar prompt_variants must be a list") + else: + variants = [] + seen_existing_ids: set[str] = set() + for index, item in enumerate(existing): + if not isinstance(item, dict): + raise ValueError(f"existing sidecar prompt_variants[{index}] must be an object") + variant_id = _text(item.get("id")) + if not variant_id: + raise ValueError(f"existing sidecar prompt_variants[{index}].id is required") + if variant_id in seen_existing_ids: + raise ValueError(f"existing sidecar prompt_variants[{index}].id {variant_id!r} is duplicated") + seen_existing_ids.add(variant_id) + variants.append(dict(item)) + index_by_id = {_text(variant.get("id")): index for index, variant in enumerate(variants)} + for variant in incoming: + variant_copy = dict(variant) + variant_id = _text(variant_copy.get("id")) + if variant_id in index_by_id: + variants[index_by_id[variant_id]] = variant_copy + else: + index_by_id[variant_id] = len(variants) + variants.append(variant_copy) + return variants + + +def _prompt_path_for_source_stem(root: Path, source_stem: str) -> Path: + for suffix in (".txt", ".prompt"): + path = root / f"{source_stem}{suffix}" + if path.is_file(): + return path + raise FileNotFoundError(f"prompt file for source stem {source_stem!r} does not exist in {root}") + + +def apply_reference_cue_sidecar_author_draft(draft: dict[str, Any], folder: str | Path) -> dict[str, Any]: + validation = validate_reference_cue_sidecar_author_draft(draft) + if not validation["valid"]: + return { + "schema": REFERENCE_CUE_SIDECAR_AUTHOR_APPLY_REPORT_SCHEMA, + "applied": False, + "root": str(Path(folder).resolve()), + "updated_file_count": 0, + "updated_files": [], + "validation": validation, + } + + root = Path(folder).resolve() + if not root.is_dir(): + raise FileNotFoundError(f"sidecar folder does not exist: {root}") + updated_files: list[dict[str, Any]] = [] + for update in draft.get("updates", []): + source_stem = _text(update.get("source_stem")) + source_prompt_sha256 = _text(update.get("source_prompt_sha256")) + prompt_path = _prompt_path_for_source_stem(root, source_stem) + actual_prompt_sha256 = _sha256_text(prompt_path.read_text(encoding="utf-8").strip()) + if source_prompt_sha256 and actual_prompt_sha256 != source_prompt_sha256: + raise ValueError(f"prompt file has drifted for {source_stem}: {prompt_path}") + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename or Path(sidecar_filename).name != sidecar_filename: + raise ValueError(f"sidecar filename must be a plain filename: {sidecar_filename!r}") + sidecar_path = root / sidecar_filename + sidecar = _read_json_object_if_present(sidecar_path) + incoming_variants = [dict(variant) for variant in update.get("prompt_variants", []) if isinstance(variant, dict)] + sidecar["prompt_variants"] = _upsert_prompt_variants(sidecar.get("prompt_variants"), incoming_variants) + sidecar["reference_cue_author_source_prompt_sha256"] = source_prompt_sha256 + sidecar["reference_cue_author_notes"] = _text(update.get("notes")) + sidecar_path.write_text(json.dumps(sidecar, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8") + updated_files.append( + { + "sidecar_filename": sidecar_filename, + "sidecar_path": str(sidecar_path), + "prompt_variant_count": len(incoming_variants), + } + ) + + return { + "schema": REFERENCE_CUE_SIDECAR_AUTHOR_APPLY_REPORT_SCHEMA, + "applied": True, + "root": str(root), + "updated_file_count": len(updated_files), + "updated_files": updated_files, + "validation": validation, + } + + +def apply_sidecar_update_draft(draft: dict[str, Any], folder: str | Path) -> dict[str, Any]: + validation = validate_sidecar_update_draft(draft) + if not validation["valid"]: + return { + "schema": SIDECAR_APPLY_REPORT_SCHEMA, + "applied": False, + "root": str(Path(folder).resolve()), + "updated_file_count": 0, + "updated_files": [], + "validation": validation, + } + + root = Path(folder).resolve() + if not root.is_dir(): + raise FileNotFoundError(f"sidecar folder does not exist: {root}") + updated_files: list[dict[str, Any]] = [] + for update in draft.get("updates", []): + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename or Path(sidecar_filename).name != sidecar_filename: + raise ValueError(f"sidecar filename must be a plain filename: {sidecar_filename!r}") + sidecar_path = root / sidecar_filename + sidecar = _read_json_object_if_present(sidecar_path) + incoming_variants = [dict(variant) for variant in update.get("prompt_variants", []) if isinstance(variant, dict)] + sidecar["prompt_variants"] = _upsert_prompt_variants(sidecar.get("prompt_variants"), incoming_variants) + sidecar_path.write_text(json.dumps(sidecar, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8") + updated_files.append( + { + "sidecar_filename": sidecar_filename, + "sidecar_path": str(sidecar_path), + "prompt_variant_count": len(incoming_variants), + } + ) + + return { + "schema": SIDECAR_APPLY_REPORT_SCHEMA, + "applied": True, + "root": str(root), + "updated_file_count": len(updated_files), + "updated_files": updated_files, + "validation": validation, + } + + +def apply_matrix_sidecar_update_draft(draft: dict[str, Any], folder: str | Path) -> dict[str, Any]: + validation = validate_matrix_sidecar_update_draft(draft) + if not validation["valid"]: + return { + "schema": MATRIX_SIDECAR_APPLY_REPORT_SCHEMA, + "applied": False, + "root": str(Path(folder).resolve()), + "updated_file_count": 0, + "updated_files": [], + "validation": validation, + } + + root = Path(folder).resolve() + if not root.is_dir(): + raise FileNotFoundError(f"sidecar folder does not exist: {root}") + updated_files: list[dict[str, Any]] = [] + for update in draft.get("updates", []): + sidecar_filename = _text(update.get("sidecar_filename")) + if not sidecar_filename or Path(sidecar_filename).name != sidecar_filename: + raise ValueError(f"sidecar filename must be a plain filename: {sidecar_filename!r}") + sidecar_path = root / sidecar_filename + sidecar = _read_json_object_if_present(sidecar_path) + incoming_variants = [dict(variant) for variant in update.get("prompt_variants", []) if isinstance(variant, dict)] + sidecar["prompt_variants"] = _upsert_prompt_variants(sidecar.get("prompt_variants"), incoming_variants) + sidecar_path.write_text(json.dumps(sidecar, ensure_ascii=True, indent=2, sort_keys=True) + "\n", encoding="utf-8") + updated_files.append( + { + "sidecar_filename": sidecar_filename, + "sidecar_path": str(sidecar_path), + "prompt_variant_count": len(incoming_variants), + } + ) + + return { + "schema": MATRIX_SIDECAR_APPLY_REPORT_SCHEMA, + "applied": True, + "root": str(root), + "updated_file_count": len(updated_files), + "updated_files": updated_files, + "validation": validation, + } + + +def build_result_sheet(batch: dict[str, Any], results: dict[str, Any], *, notes: str = "") -> dict[str, Any]: + seed = _int_seed(batch.get("seed"), field="batch seed") + result_seed = _int_seed(results.get("seed"), field="result seed") + if result_seed != seed: + raise ValueError(f"result seed {result_seed} does not match batch seed {seed}") + + channel_in = _text(batch.get("channel_in") or DEFAULT_IN_CHANNEL) + result_channel_in = _text(results.get("channel_in") or DEFAULT_IN_CHANNEL) + _validate_no_negative_channel(channel_in, field="batch channel_in") + _validate_no_negative_channel(result_channel_in, field="result channel_in") + if result_channel_in != channel_in: + raise ValueError(f"result channel_in {result_channel_in!r} does not match batch channel_in {channel_in!r}") + + batch_probes = _probe_list(batch.get("probes"), field="batch probes") + result_probes = _probe_list(results.get("probes"), field="result probes") + if len(result_probes) != len(batch_probes): + raise ValueError("result probe count must match batch probe count") + + sheet_probes: list[dict[str, Any]] = [] + for index, (batch_probe, result_probe) in enumerate(zip(batch_probes, result_probes)): + probe_id = _text(batch_probe.get("id")) + if not probe_id: + raise ValueError(f"batch probes[{index}].id is required") + result_probe_id = _text(result_probe.get("id")) + if result_probe_id != probe_id: + raise ValueError(f"result probes[{index}].id {result_probe_id!r} does not match batch probe id {probe_id!r}") + prompt_order = _text(batch_probe.get("prompt_order") or "subject_first") + result_prompt_order = _text(result_probe.get("prompt_order") or "subject_first") + if prompt_order not in PROMPT_ORDERS: + raise ValueError(f"batch probes[{index}].prompt_order must be one of {sorted(PROMPT_ORDERS)}") + if result_prompt_order != prompt_order: + raise ValueError(f"result probes[{index}].prompt_order does not match batch prompt_order {prompt_order!r}") + text = _text(batch_probe.get("text")) + if not text: + raise ValueError(f"batch probes[{index}].text is required") + _validate_no_negative_channel(text, field=f"batch probes[{index}].text") + turn = result_probe.get("turn") + if not isinstance(turn, int) or isinstance(turn, bool): + raise ValueError(f"result probes[{index}].turn must be an integer") + returned_seed = _int_seed(result_probe.get("returned_seed"), field=f"result probes[{index}].returned_seed") + if returned_seed != seed: + raise ValueError(f"result probes[{index}].returned_seed {returned_seed} does not match batch seed {seed}") + sheet_probe = { + "id": probe_id, + "variant_key": _text(batch_probe.get("variant_key") or batch.get("variant_key")), + "source_entry_id": _text(batch_probe.get("source_entry_id") or batch.get("source_entry_id")), + "source_stem": _text(batch_probe.get("source_stem") or batch.get("source_stem") or batch_probe.get("source_entry_id")), + "prompt_order": prompt_order, + "text": text, + "turn": turn, + "image_path": _image_path(result_probe.get("image_path"), field=f"result probes[{index}].image_path"), + "returned_seed": returned_seed, + "cue_axes": _merge_known_values(_cue_axes(), batch_probe.get("cue_axes")), + "seed_metadata": _merge_known_values(_seed_metadata(), batch_probe.get("seed_metadata")), + "prompt_source": _prompt_source(batch_probe.get("prompt_source"), field=f"batch probes[{index}].prompt_source"), + "selection": dict(batch_probe.get("selection")) if isinstance(batch_probe.get("selection"), dict) else {}, + "score": _score_template(), + "analysis_notes": "", + } + reference_images = _reference_images(batch_probe.get("reference_images"), field=f"batch probes[{index}].reference_images") + if reference_images: + sheet_probe["reference_images"] = reference_images + matrix_evidence = _stable_matrix_evidence_for_variant(batch_probe, field=f"batch probes[{index}]") + if matrix_evidence: + sheet_probe["matrix_evidence"] = matrix_evidence + sheet_probes.append(sheet_probe) + + return { + "schema": RESULT_SHEET_SCHEMA, + "seed": seed, + "channel_in": channel_in, + "subject_id": _text(batch.get("subject_id")), + "variant_key": _text(batch.get("variant_key")), + "source_entry_id": _text(batch.get("source_entry_id")), + "source_stem": _text(batch.get("source_stem") or batch.get("source_entry_id")), + "source_prompt_sha256": _text(batch.get("source_prompt_sha256")), + "selection": dict(batch.get("selection")) if isinstance(batch.get("selection"), dict) else {}, + "baseline_probe_id": sheet_probes[0]["id"], + "probe_count": len(sheet_probes), + "score_keys": list(SCORE_KEYS), + "notes": _text(notes), + "probes": sheet_probes, + } + + +def _matrix_result_jobs(results: dict[str, Any]) -> dict[str, dict[str, Any]]: + jobs_raw = results.get("jobs") + if not isinstance(jobs_raw, list): + raise ValueError("seed matrix results jobs must be a list") + jobs: dict[str, dict[str, Any]] = {} + for index, job in enumerate(jobs_raw): + if not isinstance(job, dict): + raise ValueError(f"seed matrix results jobs[{index}] must be an object") + job_id = _text(job.get("id")) + if not job_id: + raise ValueError(f"seed matrix results jobs[{index}].id is required") + if job_id in jobs: + raise ValueError(f"seed matrix results job id {job_id!r} is duplicated") + job_results = job.get("results") + if not isinstance(job_results, dict): + raise ValueError(f"seed matrix results jobs[{index}].results must be an object") + jobs[job_id] = job_results + return jobs + + +def build_seed_matrix_result_sheet(seed_matrix: dict[str, Any], results: dict[str, Any], *, notes: str = "") -> dict[str, Any]: + schema = _text(seed_matrix.get("schema")) + if schema and schema != SEED_MATRIX_SCHEMA: + raise ValueError(f"seed matrix schema must be {SEED_MATRIX_SCHEMA}") + matrix_jobs_raw = seed_matrix.get("jobs") + if not isinstance(matrix_jobs_raw, list) or not matrix_jobs_raw: + raise ValueError("seed matrix jobs must be a non-empty list") + result_jobs_by_id = _matrix_result_jobs(results) + sheet_jobs: list[dict[str, Any]] = [] + seen_matrix_ids: set[str] = set() + for index, job in enumerate(matrix_jobs_raw): + if not isinstance(job, dict): + raise ValueError(f"seed matrix jobs[{index}] must be an object") + job_id = _text(job.get("id")) + if not job_id: + raise ValueError(f"seed matrix jobs[{index}].id is required") + if job_id in seen_matrix_ids: + raise ValueError(f"seed matrix jobs[{index}].id {job_id!r} is duplicated") + seen_matrix_ids.add(job_id) + batch = job.get("batch") + if not isinstance(batch, dict): + raise ValueError(f"seed matrix jobs[{index}].batch must be an object") + job_results = result_jobs_by_id.get(job_id) + if not isinstance(job_results, dict): + raise ValueError(f"seed matrix results missing job {job_id!r}") + result_sheet = build_result_sheet(batch, job_results, notes=notes) + sheet_jobs.append( + { + "id": job_id, + "variant_key": _text(job.get("variant_key") or seed_matrix.get("variant_key")), + "sampler_seed": _int_seed(job.get("sampler_seed"), field=f"seed matrix jobs[{index}].sampler_seed"), + "selection_seed": _int_seed(job.get("selection_seed"), field=f"seed matrix jobs[{index}].selection_seed"), + "seed_slot": _text(job.get("seed_slot") or seed_matrix.get("seed_slot")), + "selected": dict(job.get("selected")) if isinstance(job.get("selected"), dict) else {}, + "candidate_probe": dict(job.get("candidate_probe")) if isinstance(job.get("candidate_probe"), dict) else {}, + "result_sheet": result_sheet, + } + ) + extra_ids = sorted(set(result_jobs_by_id) - seen_matrix_ids) + if extra_ids: + raise ValueError(f"seed matrix results contain unknown job ids: {', '.join(extra_ids)}") + return { + "schema": SEED_MATRIX_RESULT_SHEET_SCHEMA, + "subject_id": _text(seed_matrix.get("subject_id")), + "variant_key": _text(seed_matrix.get("variant_key")), + "seed_slot": _text(seed_matrix.get("seed_slot")), + "sampler_seeds": list(seed_matrix.get("sampler_seeds") or []), + "selection_seeds": list(seed_matrix.get("selection_seeds") or []), + "job_count": len(sheet_jobs), + "score_keys": list(SCORE_KEYS), + "notes": _text(notes), + "jobs": sheet_jobs, + } + + +def build_seed_matrix_promotion_report(matrix_result_sheet: dict[str, Any]) -> dict[str, Any]: + schema = _text(matrix_result_sheet.get("schema")) + if schema and schema != SEED_MATRIX_RESULT_SHEET_SCHEMA: + raise ValueError(f"seed matrix result sheet schema must be {SEED_MATRIX_RESULT_SHEET_SCHEMA}") + jobs_raw = matrix_result_sheet.get("jobs") + if not isinstance(jobs_raw, list) or not jobs_raw: + raise ValueError("seed matrix result sheet jobs must be a non-empty list") + expected_seed_slot = _text(matrix_result_sheet.get("seed_slot")) + if expected_seed_slot and expected_seed_slot not in SEED_SELECTION_SLOT_KEYS: + raise ValueError(f"seed matrix result sheet seed_slot must be one of {list(SEED_SELECTION_SLOT_KEYS)}") + expected_sampler_seeds_raw = matrix_result_sheet.get("sampler_seeds") + expected_sampler_seeds: list[int] = [] + if isinstance(expected_sampler_seeds_raw, list): + expected_sampler_seeds = [ + _int_seed(seed, field=f"seed matrix result sheet sampler_seeds[{index}]") + for index, seed in enumerate(expected_sampler_seeds_raw) + ] + if len(set(expected_sampler_seeds)) != len(expected_sampler_seeds): + raise ValueError("seed matrix result sheet sampler_seeds must not contain duplicate sampler seeds") + expected_selection_seeds_raw = matrix_result_sheet.get("selection_seeds") + expected_selection_seeds: list[int] = [] + if isinstance(expected_selection_seeds_raw, list): + expected_selection_seeds = [ + _int_seed(seed, field=f"seed matrix result sheet selection_seeds[{index}]") + for index, seed in enumerate(expected_selection_seeds_raw) + ] + if len(set(expected_selection_seeds)) != len(expected_selection_seeds): + raise ValueError("seed matrix result sheet selection_seeds must not contain duplicate cue seeds") + + report_jobs: list[dict[str, Any]] = [] + groups_by_key: dict[tuple[str, int], dict[str, Any]] = {} + seen_job_ids: set[str] = set() + for index, job in enumerate(jobs_raw): + if not isinstance(job, dict): + raise ValueError(f"seed matrix result sheet jobs[{index}] must be an object") + job_id = _text(job.get("id")) + if not job_id: + raise ValueError(f"seed matrix result sheet jobs[{index}].id is required") + if job_id in seen_job_ids: + raise ValueError(f"seed matrix result sheet jobs[{index}].id {job_id!r} is duplicated") + seen_job_ids.add(job_id) + result_sheet = job.get("result_sheet") + if not isinstance(result_sheet, dict): + raise ValueError(f"seed matrix result sheet jobs[{index}].result_sheet must be an object") + promotion_report = build_promotion_report(result_sheet) + candidates = promotion_report.get("candidates") or [] + if len(candidates) != 1 or not isinstance(candidates[0], dict): + raise ValueError(f"seed matrix result sheet jobs[{index}] must contain exactly one candidate") + candidate = candidates[0] + sampler_seed = _int_seed(job.get("sampler_seed"), field=f"seed matrix result sheet jobs[{index}].sampler_seed") + if expected_sampler_seeds and sampler_seed not in expected_sampler_seeds: + raise ValueError( + f"seed matrix result sheet jobs[{index}].sampler_seed {sampler_seed} must be listed in sampler_seeds" + ) + selection_seed = _int_seed(job.get("selection_seed"), field=f"seed matrix result sheet jobs[{index}].selection_seed") + if expected_selection_seeds and selection_seed not in expected_selection_seeds: + raise ValueError( + f"seed matrix result sheet jobs[{index}].selection_seed {selection_seed} must be listed in selection_seeds" + ) + seed_slot = _text(job.get("seed_slot") or expected_seed_slot) + if seed_slot not in SEED_SELECTION_SLOT_KEYS: + raise ValueError(f"seed matrix result sheet jobs[{index}].seed_slot must be one of {list(SEED_SELECTION_SLOT_KEYS)}") + if expected_seed_slot and seed_slot != expected_seed_slot: + raise ValueError( + f"seed matrix result sheet jobs[{index}].seed_slot {seed_slot!r} does not match matrix seed_slot {expected_seed_slot!r}" + ) + selected = job.get("selected") if isinstance(job.get("selected"), dict) else {} + selected_prompt_variant_id = _text(selected.get("prompt_variant_id")) + candidate_prompt_variant_id = _text(candidate.get("prompt_variant_id")) + if selected_prompt_variant_id and candidate_prompt_variant_id and selected_prompt_variant_id != candidate_prompt_variant_id: + raise ValueError( + f"seed matrix result sheet jobs[{index}].selected.prompt_variant_id {selected_prompt_variant_id!r} " + f"does not match candidate prompt_variant_id {candidate_prompt_variant_id!r}" + ) + prompt_variant_id = _text( + candidate_prompt_variant_id + or selected_prompt_variant_id + ) + if not prompt_variant_id: + raise ValueError(f"seed matrix result sheet jobs[{index}] selected prompt_variant_id is required") + source_entry_id = _text(candidate.get("source_entry_id")) + source_stem = _text(candidate.get("source_stem") or source_entry_id) + job_variant_key = _text(job.get("variant_key") or matrix_result_sheet.get("variant_key")) + candidate_variant_key = _text(candidate.get("variant_key")) + if job_variant_key and candidate_variant_key and candidate_variant_key != job_variant_key: + raise ValueError( + f"seed matrix result sheet jobs[{index}].candidate.variant_key {candidate_variant_key!r} " + f"does not match job variant_key {job_variant_key!r}" + ) + candidate_text = _text(candidate.get("text")) + candidate_text_sha256 = _sha256_text(candidate_text) if candidate_text else "" + decision = _text(candidate.get("decision")) + blockers = [_text(blocker) for blocker in candidate.get("blockers") or [] if _text(blocker)] + report_job = { + "id": job_id, + "variant_key": job_variant_key or candidate_variant_key, + "source_entry_id": source_entry_id, + "source_stem": source_stem, + "sampler_seed": sampler_seed, + "selection_seed": selection_seed, + "seed_slot": seed_slot, + "prompt_variant_id": prompt_variant_id, + "prompt_text_sha256": candidate_text_sha256, + "decision": decision, + "blockers": blockers, + "candidate": candidate, + } + report_jobs.append(report_job) + + group_key = (prompt_variant_id, selection_seed) + group = groups_by_key.get(group_key) + if group is None: + group = { + "variant_key": report_job["variant_key"], + "source_entry_id": source_entry_id, + "source_stem": source_stem, + "prompt_variant_id": prompt_variant_id, + "prompt_text_sha256": candidate_text_sha256, + "selection_seed": selection_seed, + "seed_slot": report_job["seed_slot"], + "sampler_seeds": [], + "job_ids": [], + "job_count": 0, + "promotion_ready_count": 0, + "blocked_count": 0, + "blockers": [], + } + groups_by_key[group_key] = group + else: + for field, value in ( + ("variant_key", report_job["variant_key"]), + ("source_stem", source_stem), + ("source_entry_id", source_entry_id), + ("prompt_text_sha256", candidate_text_sha256), + ): + expected_value = _text(group.get(field)) + if expected_value and value and value != expected_value: + label = "prompt text" if field == "prompt_text_sha256" else field + raise ValueError( + f"seed matrix result sheet jobs[{index}].candidate.{label} {value!r} " + f"does not match group {label} {expected_value!r}" + ) + if sampler_seed in group["sampler_seeds"]: + raise ValueError( + f"seed matrix result sheet jobs[{index}].sampler_seed {sampler_seed} is duplicated in this cue group" + ) + group["sampler_seeds"].append(sampler_seed) + group["job_ids"].append(report_job["id"]) + group["job_count"] += 1 + if decision == "seedable_candidate": + group["promotion_ready_count"] += 1 + else: + group["blocked_count"] += 1 + for blocker in blockers: + if blocker not in group["blockers"]: + group["blockers"].append(blocker) + + groups = [] + for key in sorted(groups_by_key, key=lambda item: (item[1], item[0])): + group = groups_by_key[key] + group["sampler_seeds"] = sorted(group["sampler_seeds"]) + group["sampler_seed_count"] = len(set(group["sampler_seeds"])) + missing_sampler_seeds = sorted(set(expected_sampler_seeds) - set(group["sampler_seeds"])) + if missing_sampler_seeds: + group["missing_sampler_seeds"] = missing_sampler_seeds + if "missing_sampler_coverage" not in group["blockers"]: + group["blockers"].append("missing_sampler_coverage") + insufficient_sampler_coverage = group["sampler_seed_count"] < MIN_STABLE_MATRIX_SAMPLER_SEEDS + if insufficient_sampler_coverage and "insufficient_sampler_coverage" not in group["blockers"]: + group["blockers"].append("insufficient_sampler_coverage") + group["stable"] = ( + group["job_count"] > 0 + and group["blocked_count"] == 0 + and not missing_sampler_seeds + and not insufficient_sampler_coverage + ) + groups.append(group) + + return { + "schema": SEED_MATRIX_PROMOTION_REPORT_SCHEMA, + "subject_id": _text(matrix_result_sheet.get("subject_id")), + "variant_key": _text(matrix_result_sheet.get("variant_key")), + "seed_slot": _text(matrix_result_sheet.get("seed_slot")), + "job_count": len(report_jobs), + "promotion_ready_job_count": sum(1 for job in report_jobs if job["decision"] == "seedable_candidate"), + "blocked_job_count": sum(1 for job in report_jobs if job["decision"] != "seedable_candidate"), + "stable_group_count": sum(1 for group in groups if group.get("stable") is True), + "unstable_group_count": sum(1 for group in groups if group.get("stable") is False), + "required_pass_keys": list(PROMOTION_REQUIRED_PASS_KEYS), + "required_progress_keys": list(PROMOTION_REQUIRED_PROGRESS_KEYS), + "minimum_stable_sampler_seed_count": MIN_STABLE_MATRIX_SAMPLER_SEEDS, + "jobs": report_jobs, + "groups": groups, + } + + +def _load_json_object(path: str | Path, *, field: str) -> dict[str, Any]: + json_path = Path(path) + with json_path.open("r", encoding="utf-8") as handle: + data = json.load(handle) + if not isinstance(data, dict): + raise ValueError(f"{field} must contain one JSON object") + return data + + +def _parse_int_csv(value: str, *, field: str) -> list[int]: + text = _text(value) + if not text: + raise ValueError(f"{field} must contain at least one integer") + items: list[int] = [] + for index, part in enumerate(text.split(",")): + item = part.strip() + if not item: + raise ValueError(f"{field}[{index}] is empty") + try: + parsed = int(item) + except ValueError as exc: + raise ValueError(f"{field}[{index}] must be an integer") from exc + items.append(_int_seed(parsed, field=f"{field}[{index}]")) + return items + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description="Build a manifest for a same-subject Krea2 atlas-refine prompt/image deck.") + parser.add_argument("--folder", help="Folder containing paired .txt/.png atlas refine artifacts.") + parser.add_argument("--subject-id", default="", help="Stable subject id for this reference deck.") + parser.add_argument("--indent", type=int, default=2, help="JSON indentation level.") + parser.add_argument("--print-manifest", action="store_true", help="Print the atlas refine manifest explicitly.") + parser.add_argument("--print-batch", action="store_true", help="Print an sxcp_prompt_batch-compatible probe batch instead of the manifest.") + parser.add_argument("--print-seed-selection", action="store_true", help="Print a deterministic seed-selected prompt variant from a manifest.") + parser.add_argument("--print-seed-selected-batch", action="store_true", help="Print an sxcp prompt batch containing baseline and a deterministic seed-selected candidate.") + parser.add_argument("--print-seed-matrix", action="store_true", help="Print seed-selected batches for every sampler/cue seed pair.") + parser.add_argument("--print-seed-matrix-result-sheet", action="store_true", help="Print visual scoring sheets for completed seed-matrix jobs.") + parser.add_argument("--print-seed-matrix-promotion-report", action="store_true", help="Print stability/promotion gates from a scored seed-matrix result sheet.") + parser.add_argument("--print-matrix-sidecar-update-draft", action="store_true", help="Print sidecar prompt-variant updates from stable seed-matrix groups.") + parser.add_argument("--print-catalog-cue-draft", action="store_true", help="Print review-only catalog prompt_variant_cues candidates from seedable append-cue sidecars.") + parser.add_argument("--print-reference-pool-report", action="store_true", help="Print canonical/supplemental atlas reference-pool coverage for cue expansion.") + parser.add_argument("--print-reference-cue-review-sheet", action="store_true", help="Print blank atlas reference cue-labeling slots for prompt-variant review.") + parser.add_argument("--print-reference-cue-candidate-draft", action="store_true", help="Print sidecar-ready prompt-variant candidates from a filled reference cue-review sheet.") + parser.add_argument("--print-reference-cue-sidecar-author-draft", action="store_true", help="Print same-stem sidecar authoring updates from reviewed reference cue candidates.") + parser.add_argument("--validate-reference-cue-sidecar-author-draft", action="store_true", help="Validate pre-test reference cue sidecar authoring updates without writing sidecars.") + parser.add_argument("--apply-reference-cue-sidecar-author-draft", action="store_true", help="Apply pre-test reference cue sidecar authoring updates to a folder.") + parser.add_argument("--print-coverage-report", action="store_true", help="Print atlas refine readiness coverage by variant.") + parser.add_argument("--print-sidecar-scaffold", action="store_true", help="Print review-only same-stem sidecar JSON scaffolds for known baseline-only entries.") + parser.add_argument("--print-baseline-score-sheet", action="store_true", help="Print baseline image/prompt scoring slots for manifest entries.") + parser.add_argument("--print-prompt-noise-report", action="store_true", help="Print read-only option/meta/negative prompt-noise findings for atlas prompts.") + parser.add_argument("--print-prompt-cleanup-sheet", action="store_true", help="Print manual cleanup slots for prompt-noise findings.") + parser.add_argument("--validate-prompt-cleanup-sheet", action="store_true", help="Validate manually filled prompt cleanup replacements without writing files.") + parser.add_argument("--apply-prompt-cleanup-sheet", action="store_true", help="Apply validated prompt cleanup replacements to prompt files or sidecars.") + parser.add_argument("--print-baseline-score-update-draft", action="store_true", help="Print sidecar baseline score updates from a manually scored baseline sheet.") + parser.add_argument("--validate-baseline-score-update-draft", action="store_true", help="Validate baseline score sidecar updates without writing files.") + parser.add_argument("--apply-baseline-score-update-draft", action="store_true", help="Apply baseline score sidecar updates to a folder.") + parser.add_argument("--variant-key", default="", help="Variant key to export when --print-batch is set.") + parser.add_argument("--reference-pool-folder", action="append", default=[], help="Supplemental atlas-root-relative folder for --print-reference-pool-report. Can be repeated.") + parser.add_argument("--sampler-seed", type=int, default=None, help="Override sampler seed for --print-batch.") + parser.add_argument("--selection-seed", type=int, default=None, help="Cue seed for --print-seed-selection.") + parser.add_argument("--sampler-seeds", default="", help="Comma-separated sampler seeds for --print-seed-matrix.") + parser.add_argument("--selection-seeds", default="", help="Comma-separated cue seeds for --print-seed-matrix.") + parser.add_argument("--seed-slot", default="atlas_cue_seed", help="Seed slot label for --print-seed-selection.") + parser.add_argument("--print-result-sheet", action="store_true", help="Print a visual scoring sheet from a batch JSON and result JSON.") + parser.add_argument("--print-promotion-report", action="store_true", help="Print conservative seedable-candidate gates from a scored result sheet.") + parser.add_argument("--print-sidecar-update-draft", action="store_true", help="Print reviewable sidecar prompt_variants from a promotion report.") + parser.add_argument("--validate-sidecar-update-draft", action="store_true", help="Validate a sidecar update draft without writing sidecar files.") + parser.add_argument("--apply-sidecar-update-draft", action="store_true", help="Apply a validated sidecar update draft to a folder.") + parser.add_argument("--validate-matrix-sidecar-update-draft", action="store_true", help="Validate a matrix sidecar update draft without writing sidecar files.") + parser.add_argument("--apply-matrix-sidecar-update-draft", action="store_true", help="Apply a validated matrix sidecar update draft to a folder.") + parser.add_argument("--batch-json", default="", help="Prompt batch JSON path for --print-result-sheet.") + parser.add_argument("--result-json", default="", help="Result JSON path for --print-result-sheet.") + parser.add_argument("--seed-matrix-json", default="", help="Seed matrix JSON path for --print-seed-matrix-result-sheet.") + parser.add_argument("--seed-matrix-results-json", default="", help="Seed matrix results JSON path for --print-seed-matrix-result-sheet.") + parser.add_argument("--seed-matrix-result-sheet-json", default="", help="Scored seed matrix result sheet JSON path for --print-seed-matrix-promotion-report.") + parser.add_argument("--seed-matrix-promotion-report-json", default="", help="Seed matrix promotion report JSON path for --print-matrix-sidecar-update-draft.") + parser.add_argument("--result-sheet-json", default="", help="Scored result sheet JSON path for --print-promotion-report.") + parser.add_argument("--promotion-report-json", default="", help="Promotion report JSON path for --print-sidecar-update-draft.") + parser.add_argument("--sidecar-update-draft-json", default="", help="Sidecar update draft JSON path for --validate-sidecar-update-draft.") + parser.add_argument("--matrix-sidecar-update-draft-json", default="", help="Matrix sidecar update draft JSON path for validation or apply.") + parser.add_argument("--baseline-score-sheet-json", default="", help="Baseline score sheet JSON path for --print-baseline-score-update-draft.") + parser.add_argument("--baseline-score-update-draft-json", default="", help="Baseline score update draft JSON path for validation or apply.") + parser.add_argument("--prompt-cleanup-sheet-json", default="", help="Prompt cleanup sheet JSON path for validation or apply.") + parser.add_argument("--reference-cue-review-sheet-json", default="", help="Filled reference cue-review sheet JSON path for --print-reference-cue-candidate-draft.") + parser.add_argument("--reference-cue-candidate-draft-json", default="", help="Reference cue candidate draft JSON path for --print-reference-cue-sidecar-author-draft.") + parser.add_argument("--reference-cue-sidecar-author-draft-json", default="", help="Reference cue sidecar author draft JSON path for validation or apply.") + parser.add_argument("--notes", default="", help="Notes to include in --print-result-sheet output.") + args = parser.parse_args(argv) + + if args.apply_reference_cue_sidecar_author_draft: + if not args.reference_cue_sidecar_author_draft_json or not args.folder: + parser.error("--reference-cue-sidecar-author-draft-json and --folder are required with --apply-reference-cue-sidecar-author-draft") + reference_cue_sidecar_author_draft = _load_json_object( + args.reference_cue_sidecar_author_draft_json, + field="reference-cue-sidecar-author-draft-json", + ) + payload = apply_reference_cue_sidecar_author_draft(reference_cue_sidecar_author_draft, args.folder) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["applied"] else 1 + + if args.validate_reference_cue_sidecar_author_draft: + if not args.reference_cue_sidecar_author_draft_json: + parser.error("--reference-cue-sidecar-author-draft-json is required with --validate-reference-cue-sidecar-author-draft") + reference_cue_sidecar_author_draft = _load_json_object( + args.reference_cue_sidecar_author_draft_json, + field="reference-cue-sidecar-author-draft-json", + ) + payload = validate_reference_cue_sidecar_author_draft(reference_cue_sidecar_author_draft) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["valid"] else 1 + + if args.apply_prompt_cleanup_sheet: + if not args.prompt_cleanup_sheet_json or not args.folder: + parser.error("--prompt-cleanup-sheet-json and --folder are required with --apply-prompt-cleanup-sheet") + prompt_cleanup_sheet = _load_json_object(args.prompt_cleanup_sheet_json, field="prompt-cleanup-sheet-json") + payload = apply_prompt_cleanup_sheet(prompt_cleanup_sheet, args.folder) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["applied"] else 1 + + if args.validate_prompt_cleanup_sheet: + if not args.prompt_cleanup_sheet_json: + parser.error("--prompt-cleanup-sheet-json is required with --validate-prompt-cleanup-sheet") + prompt_cleanup_sheet = _load_json_object(args.prompt_cleanup_sheet_json, field="prompt-cleanup-sheet-json") + payload = validate_prompt_cleanup_sheet(prompt_cleanup_sheet) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["valid"] else 1 + + if args.apply_baseline_score_update_draft: + if not args.baseline_score_update_draft_json or not args.folder: + parser.error("--baseline-score-update-draft-json and --folder are required with --apply-baseline-score-update-draft") + baseline_score_update_draft = _load_json_object(args.baseline_score_update_draft_json, field="baseline-score-update-draft-json") + payload = apply_baseline_score_update_draft(baseline_score_update_draft, args.folder) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["applied"] else 1 + + if args.validate_baseline_score_update_draft: + if not args.baseline_score_update_draft_json: + parser.error("--baseline-score-update-draft-json is required with --validate-baseline-score-update-draft") + baseline_score_update_draft = _load_json_object(args.baseline_score_update_draft_json, field="baseline-score-update-draft-json") + payload = validate_baseline_score_update_draft(baseline_score_update_draft) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["valid"] else 1 + + if args.print_baseline_score_update_draft: + if not args.baseline_score_sheet_json: + parser.error("--baseline-score-sheet-json is required with --print-baseline-score-update-draft") + baseline_score_sheet = _load_json_object(args.baseline_score_sheet_json, field="baseline-score-sheet-json") + payload = build_baseline_score_update_draft(baseline_score_sheet) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.apply_matrix_sidecar_update_draft: + if not args.matrix_sidecar_update_draft_json or not args.folder: + parser.error("--matrix-sidecar-update-draft-json and --folder are required with --apply-matrix-sidecar-update-draft") + matrix_sidecar_update_draft = _load_json_object( + args.matrix_sidecar_update_draft_json, + field="matrix-sidecar-update-draft-json", + ) + payload = apply_matrix_sidecar_update_draft(matrix_sidecar_update_draft, args.folder) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["applied"] else 1 + + if args.validate_matrix_sidecar_update_draft: + if not args.matrix_sidecar_update_draft_json: + parser.error("--matrix-sidecar-update-draft-json is required with --validate-matrix-sidecar-update-draft") + matrix_sidecar_update_draft = _load_json_object( + args.matrix_sidecar_update_draft_json, + field="matrix-sidecar-update-draft-json", + ) + payload = validate_matrix_sidecar_update_draft(matrix_sidecar_update_draft) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["valid"] else 1 + + if args.apply_sidecar_update_draft: + if not args.sidecar_update_draft_json or not args.folder: + parser.error("--sidecar-update-draft-json and --folder are required with --apply-sidecar-update-draft") + sidecar_update_draft = _load_json_object(args.sidecar_update_draft_json, field="sidecar-update-draft-json") + payload = apply_sidecar_update_draft(sidecar_update_draft, args.folder) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["applied"] else 1 + + if args.validate_sidecar_update_draft: + if not args.sidecar_update_draft_json: + parser.error("--sidecar-update-draft-json is required with --validate-sidecar-update-draft") + sidecar_update_draft = _load_json_object(args.sidecar_update_draft_json, field="sidecar-update-draft-json") + payload = validate_sidecar_update_draft(sidecar_update_draft) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 if payload["valid"] else 1 + + if args.print_sidecar_update_draft: + if not args.promotion_report_json: + parser.error("--promotion-report-json is required with --print-sidecar-update-draft") + promotion_report = _load_json_object(args.promotion_report_json, field="promotion-report-json") + payload = build_sidecar_update_draft(promotion_report) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_promotion_report: + if not args.result_sheet_json: + parser.error("--result-sheet-json is required with --print-promotion-report") + result_sheet = _load_json_object(args.result_sheet_json, field="result-sheet-json") + payload = build_promotion_report(result_sheet) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_result_sheet: + if not args.batch_json or not args.result_json: + parser.error("--batch-json and --result-json are required with --print-result-sheet") + batch = _load_json_object(args.batch_json, field="batch-json") + results = _load_json_object(args.result_json, field="result-json") + payload = build_result_sheet(batch, results, notes=args.notes) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_seed_matrix_result_sheet: + if not args.seed_matrix_json or not args.seed_matrix_results_json: + parser.error("--seed-matrix-json and --seed-matrix-results-json are required with --print-seed-matrix-result-sheet") + seed_matrix = _load_json_object(args.seed_matrix_json, field="seed-matrix-json") + seed_matrix_results = _load_json_object(args.seed_matrix_results_json, field="seed-matrix-results-json") + payload = build_seed_matrix_result_sheet(seed_matrix, seed_matrix_results, notes=args.notes) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_seed_matrix_promotion_report: + if not args.seed_matrix_result_sheet_json: + parser.error("--seed-matrix-result-sheet-json is required with --print-seed-matrix-promotion-report") + seed_matrix_result_sheet = _load_json_object(args.seed_matrix_result_sheet_json, field="seed-matrix-result-sheet-json") + payload = build_seed_matrix_promotion_report(seed_matrix_result_sheet) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_matrix_sidecar_update_draft: + if not args.seed_matrix_promotion_report_json: + parser.error("--seed-matrix-promotion-report-json is required with --print-matrix-sidecar-update-draft") + seed_matrix_promotion_report = _load_json_object(args.seed_matrix_promotion_report_json, field="seed-matrix-promotion-report-json") + payload = build_matrix_sidecar_update_draft(seed_matrix_promotion_report) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_reference_pool_report: + if not args.variant_key: + parser.error("--variant-key is required with --print-reference-pool-report") + payload = build_reference_pool_report( + args.variant_key, + supplemental_folders=list(args.reference_pool_folder or []), + ) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_reference_cue_review_sheet: + if not args.variant_key: + parser.error("--variant-key is required with --print-reference-cue-review-sheet") + payload = build_reference_cue_review_sheet( + args.variant_key, + supplemental_folders=list(args.reference_pool_folder or []), + ) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if args.print_reference_cue_candidate_draft: + if not args.reference_cue_review_sheet_json: + parser.error("--reference-cue-review-sheet-json is required with --print-reference-cue-candidate-draft") + reference_cue_review_sheet = _load_json_object( + args.reference_cue_review_sheet_json, + field="reference-cue-review-sheet-json", + ) + payload = build_reference_cue_candidate_draft(reference_cue_review_sheet) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + if not args.folder: + parser.error("--folder is required unless a JSON-only output mode is set") + manifest = build_manifest(args.folder, subject_id=args.subject_id) + payload = manifest + if args.print_seed_selection: + if not args.variant_key: + parser.error("--variant-key is required with --print-seed-selection") + if args.selection_seed is None: + parser.error("--selection-seed is required with --print-seed-selection") + payload = select_seeded_prompt_variant( + manifest, + args.variant_key, + selection_seed=args.selection_seed, + seed_slot=args.seed_slot, + ) + elif args.print_seed_selected_batch: + if not args.variant_key: + parser.error("--variant-key is required with --print-seed-selected-batch") + if args.selection_seed is None or args.sampler_seed is None: + parser.error("--selection-seed and --sampler-seed are required with --print-seed-selected-batch") + payload = build_seed_selected_prompt_batch( + manifest, + args.variant_key, + selection_seed=args.selection_seed, + sampler_seed=args.sampler_seed, + seed_slot=args.seed_slot, + ) + elif args.print_seed_matrix: + if not args.variant_key: + parser.error("--variant-key is required with --print-seed-matrix") + if not args.selection_seeds or not args.sampler_seeds: + parser.error("--selection-seeds and --sampler-seeds are required with --print-seed-matrix") + payload = build_seed_matrix( + manifest, + args.variant_key, + selection_seeds=_parse_int_csv(args.selection_seeds, field="selection-seeds"), + sampler_seeds=_parse_int_csv(args.sampler_seeds, field="sampler-seeds"), + seed_slot=args.seed_slot, + ) + elif args.print_reference_cue_sidecar_author_draft: + if not args.reference_cue_candidate_draft_json: + parser.error("--reference-cue-candidate-draft-json is required with --print-reference-cue-sidecar-author-draft") + reference_cue_candidate_draft = _load_json_object( + args.reference_cue_candidate_draft_json, + field="reference-cue-candidate-draft-json", + ) + payload = build_reference_cue_sidecar_author_draft( + manifest, + reference_cue_candidate_draft, + variant_key=args.variant_key, + ) + elif args.print_catalog_cue_draft: + payload = build_catalog_cue_draft(manifest, variant_key=args.variant_key) + elif args.print_coverage_report: + payload = build_coverage_report(manifest) + elif args.print_sidecar_scaffold: + payload = build_sidecar_scaffold(manifest, variant_key=args.variant_key) + elif args.print_baseline_score_sheet: + payload = build_baseline_score_sheet(manifest, variant_key=args.variant_key) + elif args.print_prompt_noise_report: + payload = build_prompt_noise_report(manifest, variant_key=args.variant_key) + elif args.print_prompt_cleanup_sheet: + payload = build_prompt_cleanup_sheet(manifest, variant_key=args.variant_key) + elif args.print_batch: + if not args.variant_key: + parser.error("--variant-key is required with --print-batch") + payload = build_prompt_batch(manifest, args.variant_key, sampler_seed=args.sampler_seed) + print(json.dumps(payload, ensure_ascii=True, indent=args.indent, sort_keys=True)) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/node_hardcore_position.py b/node_hardcore_position.py index e627210..07c8a5d 100644 --- a/node_hardcore_position.py +++ b/node_hardcore_position.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import random try: from . import krea2_eval_log @@ -97,6 +98,57 @@ def _selected_variant_keys(variants): return [str(variant.get("key")) for variant in variants if variant.get("key")] +def _int_seed(value, default=-1): + try: + seed = int(value) + except (TypeError, ValueError): + return default + return seed if seed >= 0 else default + + +def _seeded_prompt_variant_indices(variants, atlas_cue_seed=-1): + seed = _int_seed(atlas_cue_seed) + if seed < 0: + return {}, seed + indices = {} + for variant in variants: + key = str(variant.get("key") or "").strip() + if not key: + continue + cue_sets = krea2_pose_variant_catalog.prompt_cue_sets(variant) + if len(cue_sets) <= 1: + continue + rng = random.Random(f"sxcp_krea2_atlas_cue:{seed}:{key}") + indices[key] = rng.randrange(len(cue_sets)) + return indices, seed + + +def _normalized_prompt_variant_indices(value): + if not isinstance(value, dict): + return {} + indices = {} + for key, index in value.items(): + key_text = str(key or "").strip() + if not key_text: + continue + try: + indices[key_text] = int(index) + except (TypeError, ValueError): + continue + return indices + + +def _summary_without_variant_metadata(summary): + return "; ".join( + part + for part in (str(summary or "").split(";")) + if part.strip() + and not part.strip().startswith("variants=") + and not part.strip().startswith("cue_seed=") + and not part.strip().startswith("cue_indices=") + ).strip() + + def _merged_family_for_variant_filter(incoming_config, combine_mode, family): family = _variant_family(family) if combine_mode != "add": @@ -120,7 +172,7 @@ def _empty_or_incoming_config(incoming_config, combine_mode): return json.dumps(config, ensure_ascii=True, sort_keys=True) -def _merge_variant_metadata(config_json, variants): +def _merge_variant_metadata(config_json, variants, atlas_cue_seed=-1): config = json.loads(config_json) selected_keys = _selected_variant_keys(variants) existing_keys = config.get("krea2_variant_keys") or [] @@ -133,10 +185,32 @@ def _merge_variant_metadata(config_json, variants): existing_statuses = config.get("krea2_variant_statuses") if isinstance(config.get("krea2_variant_statuses"), dict) else {} config["krea2_variant_statuses"] = {**existing_statuses, **selected_statuses} - 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 + existing_indices = _normalized_prompt_variant_indices(config.get("krea2_prompt_variant_indices")) + seeded_indices, seed = _seeded_prompt_variant_indices(variants, atlas_cue_seed) + prompt_variant_indices = {**existing_indices, **seeded_indices} + if prompt_variant_indices: + config["krea2_prompt_variant_indices"] = prompt_variant_indices + if seeded_indices: + config["krea2_prompt_variant_seed"] = seed + config["krea2_prompt_variant_seed_axis"] = "atlas_cue_seed" + + base_summary = _summary_without_variant_metadata(config.get("summary") or hardcore_position_summary(config)) + summary_parts = [base_summary] if base_summary else [] + if variant_keys: + summary_parts.append("variants=" + ",".join(variant_keys)) + if seeded_indices: + summary_parts.append(f"cue_seed={seed}") + selected_indices = { + key: prompt_variant_indices[key] + for key in variant_keys + if key in prompt_variant_indices + } + if selected_indices: + summary_parts.append( + "cue_indices=" + + ",".join(f"{key}:{selected_indices[key]}" for key in variant_keys if key in selected_indices) + ) + config["summary"] = "; ".join(part for part in summary_parts if part) return json.dumps(config, ensure_ascii=True, sort_keys=True) @@ -195,6 +269,7 @@ class SxCPKrea2PoseVariant: }, "optional": { "hardcore_position_config": (SXCP_HARDCORE_POSITION_CONFIG,), + "atlas_cue_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}), }, } @@ -210,7 +285,7 @@ class SxCPKrea2PoseVariant: FUNCTION = "build" CATEGORY = "prompt_builder" - def build(self, variant_key, combine_mode="replace", hardcore_position_config=""): + def build(self, variant_key, combine_mode="replace", hardcore_position_config="", atlas_cue_seed=-1): variant = krea2_pose_variant_catalog.get_variant(variant_key) if not variant: empty = { @@ -228,12 +303,23 @@ class SxCPKrea2PoseVariant: family=family, selected_positions=positions, ) + config = _merge_variant_metadata(config, [variant], atlas_cue_seed=atlas_cue_seed) + parsed_config = json.loads(config) 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'}" - ) + summary_parts = [ + f"variant={variant.get('key')}", + f"status={variant.get('status')}", + f"family={family}", + f"positions={','.join(positions) or 'none'}", + ] + if parsed_config.get("krea2_prompt_variant_seed") is not None: + summary_parts.append(f"cue_seed={parsed_config.get('krea2_prompt_variant_seed')}") + prompt_variant_indices = _normalized_prompt_variant_indices(parsed_config.get("krea2_prompt_variant_indices")) + selected_index = prompt_variant_indices.get(str(variant.get("key") or "")) + if selected_index is not None: + summary_parts.append(f"cue_indices={variant.get('key')}:{selected_index}") + summary = "; ".join(summary_parts) return ( config, str(variant.get("key") or variant_key), @@ -252,6 +338,7 @@ class _SxCPKrea2POVVariantFilter: def INPUT_TYPES(cls): required = { "combine_mode": (["replace", "add"], {"default": "replace"}), + "atlas_cue_seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "step": 1}), } for variant in _variants_for_action_family(cls.ACTION_FAMILY): required[_variant_input_key(variant.get("key"))] = ("BOOLEAN", {"default": False}) @@ -274,7 +361,7 @@ class _SxCPKrea2POVVariantFilter: FUNCTION = "build" CATEGORY = "prompt_builder" - def build(self, combine_mode="replace", hardcore_position_config="", **kwargs): + def build(self, combine_mode="replace", hardcore_position_config="", atlas_cue_seed=-1, **kwargs): variants = _selected_variant_rows(self.ACTION_FAMILY, kwargs) if not variants: config = _empty_or_incoming_config(hardcore_position_config or "", combine_mode) @@ -292,7 +379,7 @@ class _SxCPKrea2POVVariantFilter: family=family, selected_positions=positions, ) - config = _merge_variant_metadata(config, variants) + config = _merge_variant_metadata(config, variants, atlas_cue_seed=atlas_cue_seed) parsed = json.loads(config) selected_keys = parsed.get("krea2_variant_keys") or [] selected_positions = parsed.get("positions") or [] diff --git a/node_tooltips.py b/node_tooltips.py index ec2a4ed..c3673c3 100644 --- a/node_tooltips.py +++ b/node_tooltips.py @@ -325,8 +325,30 @@ NODE_INPUT_TOOLTIPS = { "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.", + "atlas_cue_seed": "Optional cue seed for selecting an explicit catalog prompt_variant_cues set. Use -1 to let the generator pose seed choose.", "hardcore_position_config": "Optional incoming hardcore position config. Connect this when layering a variant on an existing pool.", }, + "SxCPKrea2POVPenetrationFilter": { + "atlas_cue_seed": "Optional cue seed for selecting explicit catalog atlas prompt variants. It is separate from the sampler seed; -1 follows the generator pose seed.", + }, + "SxCPKrea2POVOralFilter": { + "atlas_cue_seed": "Optional cue seed for selecting explicit catalog atlas prompt variants. It is separate from the sampler seed; -1 follows the generator pose seed.", + }, + "SxCPKrea2POVOutercourseFilter": { + "atlas_cue_seed": "Optional cue seed for selecting explicit catalog atlas prompt variants. It is separate from the sampler seed; -1 follows the generator pose seed.", + }, + "SxCPKrea2POVManualFilter": { + "atlas_cue_seed": "Optional cue seed for selecting explicit catalog atlas prompt variants. It is separate from the sampler seed; -1 follows the generator pose seed.", + }, + "SxCPKrea2POVToyFilter": { + "atlas_cue_seed": "Optional cue seed for selecting explicit catalog atlas prompt variants. It is separate from the sampler seed; -1 follows the generator pose seed.", + }, + "SxCPKrea2POVClimaxFilter": { + "atlas_cue_seed": "Optional cue seed for selecting explicit catalog atlas prompt variants. It is separate from the sampler seed; -1 follows the generator pose seed.", + }, + "SxCPKrea2POVInteractionFilter": { + "atlas_cue_seed": "Optional cue seed for selecting explicit catalog atlas prompt variants. It is separate from the sampler seed; -1 follows the generator pose seed.", + }, "SxCPKrea2POVPromptRestore": { "restore_clothing_detail": "Let compatible clothing/body-exposure detail survive a strict Krea2 POV atlas pose lock when the source category has that axis.", "restore_face_expression_detail": "Restore compatible face, expression, mouth, and reaction detail as visible prompt detail without changing the atlas pose.", diff --git a/prompt_builder.py b/prompt_builder.py index fc53382..cee1d2d 100644 --- a/prompt_builder.py +++ b/prompt_builder.py @@ -716,6 +716,19 @@ def _axis_values_with_krea2_variant_keys( return axis_values merged = dict(axis_values) merged["krea2_variant_keys"] = [str(key) for key in variant_keys if str(key).strip()] + prompt_variant_indices = hardcore_position_config.get("krea2_prompt_variant_indices") + if isinstance(prompt_variant_indices, dict): + normalized_indices: dict[str, int] = {} + for key, value in prompt_variant_indices.items(): + key_text = str(key or "").strip() + if not key_text: + continue + try: + normalized_indices[key_text] = int(value) + except (TypeError, ValueError): + continue + if normalized_indices: + merged["krea2_prompt_variant_indices"] = normalized_indices return merged @@ -730,12 +743,25 @@ def _axis_values_with_krea2_prompt_variant_indices( if not isinstance(variant_keys, list) or not variant_keys: return axis_values rng = seed_policy.axis_rng(seed_config, "pose", seed, row_number) + existing_indices = axis_values.get("krea2_prompt_variant_indices") if isinstance(axis_values, dict) else {} indices: dict[str, int] = {} + if isinstance(existing_indices, dict): + for key, value in existing_indices.items(): + key_text = str(key or "").strip() + if not key_text: + continue + try: + indices[key_text] = int(value) + except (TypeError, ValueError): + continue for key in variant_keys: + key_text = str(key) + if key_text in indices: + continue variant = krea2_pose_variant_catalog.get_variant(str(key)) cue_sets = krea2_pose_variant_catalog.prompt_cue_sets(variant) if len(cue_sets) > 1: - indices[str(key)] = rng.randrange(len(cue_sets)) + indices[key_text] = rng.randrange(len(cue_sets)) if not indices: return axis_values merged = dict(axis_values) diff --git a/tools/krea2_atlas_refine_manifest.py b/tools/krea2_atlas_refine_manifest.py new file mode 100644 index 0000000..9995ed7 --- /dev/null +++ b/tools/krea2_atlas_refine_manifest.py @@ -0,0 +1,17 @@ +#!/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)) + +from krea2_atlas_refine_manifest import main # noqa: E402 + + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 6722c95..e2b58cf 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -106,6 +106,7 @@ import seed_config # noqa: E402 import krea_pov # noqa: E402 import subject_context # noqa: E402 from tools import prompt_route_simulation # noqa: E402 +from tools import sxcp_prompt_batch # noqa: E402 Trigger = "sxcppnl7" @@ -6977,6 +6978,27 @@ def smoke_krea2_pose_variant_catalog_policy() -> None: len(selected_indices) >= 2, f"Krea2 prompt variant selector should vary across pose seeds, got {sorted(selected_indices)}", ) + preselected_axis_values = pb._axis_values_with_krea2_variant_keys( + {}, + { + "krea2_variant_keys": ["pov_synthetic_seeded_variant"], + "krea2_prompt_variant_indices": {"pov_synthetic_seeded_variant": 1}, + }, + ) + _expect( + preselected_axis_values.get("krea2_prompt_variant_indices") == {"pov_synthetic_seeded_variant": 1}, + "Krea2 variant-key merge should preserve node-selected prompt variant indices", + ) + generated_axis_values = pb._axis_values_with_krea2_prompt_variant_indices( + preselected_axis_values, + seed_config={}, + seed=4510, + row_number=1, + ) + _expect( + generated_axis_values.get("krea2_prompt_variant_indices") == {"pov_synthetic_seeded_variant": 1}, + "Pose-seed prompt variant selector should not overwrite node-selected atlas cue indices", + ) finally: krea2_pose_variant_catalog.get_variant = original_get_variant @@ -8162,6 +8184,3709 @@ def smoke_krea2_tuning_report_policy() -> None: _expect("Avoid cues" not in markdown, "Krea2 tuning report should omit next-test avoid section when no normal tests remain") +def smoke_krea2_atlas_refine_manifest_policy() -> None: + import importlib + + manifest_module = importlib.import_module("krea2_atlas_refine_manifest") + with tempfile.TemporaryDirectory() as tmpdir: + root = Path(tmpdir) + known_prompt = root / "pov_footjob_frontal_sole_stroke_00001_.txt" + known_image = root / "pov_footjob_frontal_sole_stroke_00001_.png" + known_sidecar = root / "pov_footjob_frontal_sole_stroke_00001_.json" + baseline_only_prompt = root / "pov_handjob_upright_centered_00001_.txt" + baseline_only_image = root / "pov_handjob_upright_centered_00001_.png" + unknown_prompt = root / "pov_unknown_pose_candidate_00001_.txt" + unknown_image = root / "pov_unknown_pose_candidate_00001_.png" + orphan_prompt = root / "pov_blowjob_side_profile_oral_00002_.txt" + known_prompt.write_text( + "A controlled same-subject footjob reference prompt with foreground soles.", + encoding="utf-8", + ) + known_image.write_bytes(b"fake-png") + known_sidecar.write_text( + json.dumps( + { + "seed_metadata": { + "sampler_seed": 101, + "atlas_cue_seed": 202, + "micro_position_seed": 303, + "workspace_seed": 404, + }, + "cue_axes": { + "foot_position": "soles_more_forward", + "workspace_surface": "floor_between_desks", + }, + "score": { + "atlas_pose_match": "partial", + "workspace_continuity": "pass", + "subject_identity": "pass", + }, + "prompt_variants": [ + { + "id": "soles_more_forward", + "append_cues": [ + "the woman's soles press farther forward along the same contact line" + ], + "reference_images": [ + "blowjob_top_view/22_blowjob_top_view.png" + ], + "cue_axes": { + "foot_position": "soles_more_forward", + "contact_depth": "contact_line_farther_forward", + }, + "seed_metadata": { + "atlas_cue_seed": 202, + "micro_position_seed": 303, + }, + "notes": "explicit sidecar cue, not invented by the batch builder", + } + ], + "notes": "same-subject seedable foot placement frame", + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + baseline_only_prompt.write_text( + "A controlled same-subject handjob reference prompt awaiting prompt variants.", + encoding="utf-8", + ) + baseline_only_image.write_bytes(b"fake-png") + unknown_prompt.write_text("A controlled same-subject unknown pose reference prompt.", encoding="utf-8") + unknown_image.write_bytes(b"fake-png") + orphan_prompt.write_text("A prompt without a matching image should be reported.", encoding="utf-8") + + manifest = manifest_module.build_manifest(root, subject_id="same_woman_001") + with tempfile.TemporaryDirectory() as duplicate_tmpdir: + duplicate_root = Path(duplicate_tmpdir) + duplicate_prompt = duplicate_root / "pov_footjob_frontal_sole_stroke_00001_.txt" + duplicate_image = duplicate_root / "pov_footjob_frontal_sole_stroke_00001_.png" + duplicate_sidecar = duplicate_root / "pov_footjob_frontal_sole_stroke_00001_.json" + duplicate_prompt.write_text( + "A controlled same-subject footjob reference prompt with foreground soles.", + encoding="utf-8", + ) + duplicate_image.write_bytes(b"fake-png") + duplicate_sidecar.write_text( + json.dumps( + { + "seed_metadata": {"sampler_seed": 101}, + "prompt_variants": [ + { + "id": "duplicate_axis", + "append_cues": ["first duplicate cue"], + }, + { + "id": "duplicate_axis", + "append_cues": ["second duplicate cue"], + }, + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + try: + manifest_module.build_manifest(duplicate_root, subject_id="same_woman_001") + except ValueError as exc: + _expect( + "duplicate_axis" in str(exc) and "duplicated" in str(exc), + f"Atlas refine manifest duplicate prompt-variant error should identify the duplicated id: {exc}", + ) + else: + raise AssertionError("Atlas refine manifest should reject duplicate sidecar prompt_variant ids") + with tempfile.TemporaryDirectory() as source_mismatch_tmpdir: + source_mismatch_root = Path(source_mismatch_tmpdir) + source_mismatch_prompt = source_mismatch_root / "pov_footjob_frontal_sole_stroke_00001_.txt" + source_mismatch_image = source_mismatch_root / "pov_footjob_frontal_sole_stroke_00001_.png" + source_mismatch_sidecar = source_mismatch_root / "pov_footjob_frontal_sole_stroke_00001_.json" + source_mismatch_prompt.write_text( + "A controlled same-subject footjob reference prompt with foreground soles.", + encoding="utf-8", + ) + source_mismatch_image.write_bytes(b"fake-png") + source_mismatch_sidecar.write_text( + json.dumps( + { + "seed_metadata": {"sampler_seed": 101}, + "prompt_variants": [ + { + "id": "soles_more_forward", + "append_cues": [ + "the woman's soles press farther forward along the same contact line" + ], + "prompt_source": { + "kind": "append_cues", + "prompt_variant_id": "other_axis", + "append_cues": [ + "the woman's soles press farther forward along the same contact line" + ], + }, + }, + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + try: + manifest_module.build_manifest(source_mismatch_root, subject_id="same_woman_001") + except ValueError as exc: + _expect( + "prompt_source.prompt_variant_id" in str(exc) + and "soles_more_forward" in str(exc) + and "other_axis" in str(exc), + f"Atlas refine manifest prompt-source mismatch error should name both ids: {exc}", + ) + else: + raise AssertionError("Atlas refine manifest should reject mismatched prompt_source prompt_variant_id") + with tempfile.TemporaryDirectory() as missing_reference_tmpdir: + missing_reference_root = Path(missing_reference_tmpdir) + missing_reference_prompt = missing_reference_root / "pov_footjob_frontal_sole_stroke_00001_.txt" + missing_reference_image = missing_reference_root / "pov_footjob_frontal_sole_stroke_00001_.png" + missing_reference_sidecar = missing_reference_root / "pov_footjob_frontal_sole_stroke_00001_.json" + missing_reference_prompt.write_text( + "A controlled same-subject footjob reference prompt with foreground soles.", + encoding="utf-8", + ) + missing_reference_image.write_bytes(b"fake-png") + missing_reference_sidecar.write_text( + json.dumps( + { + "seed_metadata": {"sampler_seed": 101}, + "prompt_variants": [ + { + "id": "missing_atlas_reference", + "append_cues": [ + "the woman's soles press farther forward along the same contact line" + ], + "reference_images": [ + "blowjob_top_view/does_not_exist.png" + ], + } + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + try: + manifest_module.build_manifest(missing_reference_root, subject_id="same_woman_001") + except ValueError as exc: + _expect( + "reference_images" in str(exc) and "does_not_exist.png" in str(exc), + f"Atlas refine manifest missing reference-image error should identify the stale atlas path: {exc}", + ) + else: + raise AssertionError("Atlas refine manifest should reject missing atlas reference_images when atlas root exists") + cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(root), + "--subject-id", + "same_woman_001", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + cli_stdout = cli_result.stdout + _expect(cli_result.returncode == 0, f"Atlas refine manifest CLI failed: {cli_result.stderr}") + cli_manifest = json.loads(cli_stdout) + explicit_manifest_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(root), + "--subject-id", + "same_woman_001", + "--print-manifest", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect( + explicit_manifest_cli_result.returncode == 0, + f"Atlas refine explicit manifest CLI failed: {explicit_manifest_cli_result.stderr}", + ) + explicit_cli_manifest = json.loads(explicit_manifest_cli_result.stdout) + _expect( + explicit_cli_manifest == cli_manifest, + "Atlas refine --print-manifest should match the default manifest CLI output", + ) + scaffold_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(root), + "--subject-id", + "same_woman_001", + "--print-sidecar-scaffold", + "--variant-key", + "pov_handjob_upright_centered", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + scaffold_cli_returncode = scaffold_cli_result.returncode + scaffold_cli_stdout = scaffold_cli_result.stdout + scaffold_cli_stderr = scaffold_cli_result.stderr + baseline_score_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(root), + "--subject-id", + "same_woman_001", + "--print-baseline-score-sheet", + "--variant-key", + "pov_handjob_upright_centered", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + baseline_score_cli_returncode = baseline_score_cli_result.returncode + baseline_score_cli_stdout = baseline_score_cli_result.stdout + baseline_score_cli_stderr = baseline_score_cli_result.stderr + prompt_noise_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(root), + "--subject-id", + "same_woman_001", + "--print-prompt-noise-report", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + prompt_noise_cli_returncode = prompt_noise_cli_result.returncode + prompt_noise_cli_stdout = prompt_noise_cli_result.stdout + prompt_noise_cli_stderr = prompt_noise_cli_result.stderr + prompt_cleanup_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(root), + "--subject-id", + "same_woman_001", + "--print-prompt-cleanup-sheet", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + prompt_cleanup_cli_returncode = prompt_cleanup_cli_result.returncode + prompt_cleanup_cli_stdout = prompt_cleanup_cli_result.stdout + prompt_cleanup_cli_stderr = prompt_cleanup_cli_result.stderr + + _expect(manifest.get("schema") == "sxcp_krea2_atlas_refine_manifest_v1", "Atlas refine manifest lost schema") + _expect(manifest.get("subject_id") == "same_woman_001", "Atlas refine manifest lost subject id") + _expect(manifest.get("entry_count") == 3, f"Atlas refine manifest should include paired prompt/image entries: {manifest}") + _expect(manifest.get("missing_pair_count") == 1, "Atlas refine manifest should report orphan prompt/image files") + _expect(manifest.get("unknown_variant_count") == 1, "Atlas refine manifest should count unknown variant keys") + entries = manifest.get("entries") or [] + by_variant = {entry.get("variant_key"): entry for entry in entries} + footjob = by_variant.get("pov_footjob_frontal_sole_stroke") or {} + _expect(footjob.get("known_variant") is True, "Atlas refine manifest should validate known variant keys") + _expect(footjob.get("prompt_text", "").startswith("A controlled same-subject footjob"), "Atlas refine manifest lost prompt text") + _expect(footjob.get("prompt_sha256"), "Atlas refine manifest should include a prompt hash for drift checks") + _expect(footjob.get("image_size_bytes") == len(b"fake-png"), "Atlas refine manifest should record image byte size") + seed_metadata = footjob.get("seed_metadata") or {} + _expect(seed_metadata.get("sampler_seed") == 101, "Atlas refine manifest should merge sampler seed metadata") + _expect(seed_metadata.get("atlas_cue_seed") == 202, "Atlas refine manifest should merge atlas cue seed metadata") + _expect(seed_metadata.get("micro_position_seed") == 303, "Atlas refine manifest should merge micro-position seed metadata") + _expect(seed_metadata.get("workspace_seed") == 404, "Atlas refine manifest should merge workspace seed metadata") + _expect(seed_metadata.get("generator_seed") is None, "Atlas refine manifest should keep missing generator seed explicit") + cue_axes = footjob.get("cue_axes") or {} + for key in ( + "contact_depth", + "hand_position", + "foot_position", + "body_angle", + "camera_height", + "workspace_surface", + "clothing_visibility", + "expression_eye_detail", + "anatomy_shape_detail", + ): + _expect(key in cue_axes, f"Atlas refine manifest missing cue-axis slot {key}") + _expect(cue_axes.get("foot_position") == "soles_more_forward", "Atlas refine manifest should merge cue-axis metadata") + _expect(cue_axes.get("workspace_surface") == "floor_between_desks", "Atlas refine manifest should merge workspace cue axis") + score = footjob.get("score") or {} + for key in ( + "atlas_pose_match", + "contact_match", + "pose_ownership", + "workspace_continuity", + "clothing_visibility", + "subject_identity", + "expression_eye_control", + "anatomy_proportion", + "prompt_noise", + ): + _expect(key in score, f"Atlas refine manifest missing score slot {key}") + _expect(score.get("atlas_pose_match") == "partial", "Atlas refine manifest should merge atlas pose score") + _expect(score.get("workspace_continuity") == "pass", "Atlas refine manifest should merge workspace continuity score") + _expect(score.get("subject_identity") == "pass", "Atlas refine manifest should merge subject identity score") + _expect(footjob.get("notes") == "same-subject seedable foot placement frame", "Atlas refine manifest should merge notes") + prompt_variants = footjob.get("prompt_variants") or [] + _expect(len(prompt_variants) == 1, f"Atlas refine manifest should keep sidecar prompt variants: {prompt_variants}") + _expect(prompt_variants[0].get("id") == "soles_more_forward", "Atlas refine manifest lost prompt variant id") + _expect( + prompt_variants[0].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine manifest should keep nearest atlas reference-image provenance on prompt variants", + ) + baseline_score_sheet = manifest_module.build_baseline_score_sheet(manifest) + _expect( + baseline_score_sheet.get("schema") == "sxcp_atlas_refine_baseline_score_sheet_v1", + "Atlas refine baseline score sheet lost schema", + ) + _expect( + baseline_score_sheet.get("entry_count") == 3, + "Atlas refine baseline score sheet should include every paired manifest entry", + ) + _expect( + baseline_score_sheet.get("unscored_count") == 2, + "Atlas refine baseline score sheet should count unscored baselines", + ) + _expect( + baseline_score_sheet.get("partially_scored_count") == 1, + "Atlas refine baseline score sheet should count partially scored baselines separately", + ) + baseline_score_entries = { + entry.get("variant_key"): entry for entry in baseline_score_sheet.get("entries") or [] + } + footjob_baseline_score = baseline_score_entries.get("pov_footjob_frontal_sole_stroke") or {} + _expect( + footjob_baseline_score.get("score", {}).get("atlas_pose_match") == "partial", + "Atlas refine baseline score sheet should preserve existing sidecar baseline scores", + ) + _expect( + footjob_baseline_score.get("score_state") == "partially_scored", + "Atlas refine baseline score sheet should flag incomplete baseline scores as partial", + ) + handjob_baseline_score = baseline_score_entries.get("pov_handjob_upright_centered") or {} + _expect( + handjob_baseline_score.get("score_state") == "needs_visual_score", + "Atlas refine baseline score sheet should flag unscored baseline-only known entries", + ) + _expect( + baseline_score_cli_returncode == 0, + f"Atlas refine baseline score sheet CLI failed: {baseline_score_cli_stderr}", + ) + cli_baseline_score = json.loads(baseline_score_cli_stdout) + _expect( + cli_baseline_score.get("entry_count") == 1, + "Atlas refine baseline score sheet CLI should honor variant-key filtering", + ) + scored_baseline_sheet = json.loads(json.dumps(baseline_score_sheet)) + scored_baseline_entries = { + entry.get("variant_key"): entry for entry in scored_baseline_sheet.get("entries") or [] + } + scored_footjob_baseline = scored_baseline_entries.get("pov_footjob_frontal_sole_stroke") or {} + scored_footjob_baseline["analysis_notes"] = "Partial baseline is still useful as scored evidence." + scored_handjob_baseline = scored_baseline_entries.get("pov_handjob_upright_centered") or {} + scored_handjob_baseline["cue_axes"]["hand_position"] = "centered_upright_base_grip" + scored_handjob_baseline["score"].update( + { + "atlas_pose_match": "pass", + "contact_match": "pass", + "pose_ownership": "pass", + "workspace_continuity": "pass", + "clothing_visibility": "pass", + "subject_identity": "pass", + "expression_eye_control": "pass", + "anatomy_proportion": "pass", + "prompt_noise": "pass", + } + ) + scored_handjob_baseline["analysis_notes"] = "Baseline handjob frame preserves subject, pose, and workspace." + baseline_score_update_draft = manifest_module.build_baseline_score_update_draft(scored_baseline_sheet) + _expect( + baseline_score_update_draft.get("schema") == "sxcp_atlas_refine_baseline_score_update_draft_v1", + "Atlas refine baseline score update draft lost schema", + ) + _expect( + baseline_score_update_draft.get("update_count") == 2, + f"Atlas refine baseline score update draft should include scored and partially scored baselines: {baseline_score_update_draft}", + ) + baseline_score_updates = { + update.get("variant_key"): update for update in baseline_score_update_draft.get("updates") or [] + } + footjob_baseline_update = baseline_score_updates.get("pov_footjob_frontal_sole_stroke") or {} + _expect( + footjob_baseline_update.get("score_state") == "partially_scored", + "Atlas refine baseline score update draft should preserve partial baseline scoring", + ) + _expect( + "prompt_variants" not in footjob_baseline_update, + "Atlas refine baseline score update draft must not carry prompt variants", + ) + handjob_baseline_update = baseline_score_updates.get("pov_handjob_upright_centered") or {} + _expect( + handjob_baseline_update.get("score_state") == "scored_pass", + "Atlas refine baseline score update draft should recalculate filled baseline scores", + ) + _expect( + handjob_baseline_update.get("score", {}).get("pose_ownership") == "pass", + "Atlas refine baseline score update draft should keep manual baseline score values", + ) + _expect( + handjob_baseline_update.get("cue_axes", {}).get("hand_position") == "centered_upright_base_grip", + "Atlas refine baseline score update draft should keep manually reviewed cue axes", + ) + baseline_score_update_validation = manifest_module.validate_baseline_score_update_draft(baseline_score_update_draft) + _expect( + baseline_score_update_validation.get("schema") == "sxcp_atlas_refine_baseline_score_update_validation_v1", + "Atlas refine baseline score update validation lost schema", + ) + _expect( + baseline_score_update_validation.get("valid") is True, + f"Atlas refine baseline score update draft should validate: {baseline_score_update_validation}", + ) + _expect( + baseline_score_update_validation.get("warning_count", 0) >= 1, + "Atlas refine baseline score update validation should warn about partial baseline scores", + ) + invalid_baseline_score_update = json.loads(json.dumps(baseline_score_update_draft)) + invalid_baseline_score_update["updates"][0]["prompt_variants"] = [] + invalid_baseline_score_update["updates"][1]["sidecar_filename"] = "wrong.json" + invalid_baseline_score_validation = manifest_module.validate_baseline_score_update_draft(invalid_baseline_score_update) + _expect( + invalid_baseline_score_validation.get("valid") is False, + "Atlas refine baseline score update validation should reject contaminated baseline updates", + ) + _expect( + any("prompt_variants" in error for error in invalid_baseline_score_validation.get("errors", [])), + f"Atlas refine baseline score update validation should reject prompt variant fields: {invalid_baseline_score_validation}", + ) + _expect( + any("sidecar_filename" in error for error in invalid_baseline_score_validation.get("errors", [])), + f"Atlas refine baseline score update validation should reject sidecar filename drift: {invalid_baseline_score_validation}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as baseline_sheet_handle: + json.dump(scored_baseline_sheet, baseline_sheet_handle) + cli_baseline_sheet_path = Path(baseline_sheet_handle.name) + try: + baseline_update_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-baseline-score-update-draft", + "--baseline-score-sheet-json", + str(cli_baseline_sheet_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_baseline_sheet_path.unlink(missing_ok=True) + _expect( + baseline_update_cli_result.returncode == 0, + f"Atlas refine baseline score update draft CLI failed: {baseline_update_cli_result.stderr}", + ) + cli_baseline_update_draft = json.loads(baseline_update_cli_result.stdout) + _expect( + cli_baseline_update_draft.get("update_count") == 2, + "Atlas refine baseline score update draft CLI should keep scored baseline updates", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as baseline_update_handle: + json.dump(baseline_score_update_draft, baseline_update_handle) + cli_baseline_update_path = Path(baseline_update_handle.name) + try: + baseline_update_validation_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--validate-baseline-score-update-draft", + "--baseline-score-update-draft-json", + str(cli_baseline_update_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_baseline_update_path.unlink(missing_ok=True) + _expect( + baseline_update_validation_cli_result.returncode == 0, + f"Atlas refine baseline score update validation CLI failed: {baseline_update_validation_cli_result.stderr}", + ) + cli_baseline_update_validation = json.loads(baseline_update_validation_cli_result.stdout) + _expect( + cli_baseline_update_validation.get("valid") is True, + "Atlas refine baseline score update validation CLI should validate the draft", + ) + with tempfile.TemporaryDirectory() as baseline_apply_tmpdir: + baseline_apply_root = Path(baseline_apply_tmpdir) + baseline_footjob_sidecar_path = baseline_apply_root / "pov_footjob_frontal_sole_stroke_00001_.json" + baseline_footjob_sidecar_path.write_text( + json.dumps( + { + "notes": "preserve prompt variant sidecar notes", + "prompt_variants": [ + { + "id": "existing_axis", + "text": "Existing tested prompt variant.", + "cue_axes": {"foot_position": "existing"}, + } + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + baseline_apply_report = manifest_module.apply_baseline_score_update_draft( + baseline_score_update_draft, + baseline_apply_root, + ) + _expect( + baseline_apply_report.get("schema") == "sxcp_atlas_refine_baseline_score_apply_report_v1", + "Atlas refine baseline score apply report lost schema", + ) + _expect(baseline_apply_report.get("applied") is True, "Atlas refine baseline score apply should mark applied") + _expect( + baseline_apply_report.get("updated_file_count") == 2, + "Atlas refine baseline score apply should update each scored baseline sidecar", + ) + applied_footjob_baseline_sidecar = json.loads(baseline_footjob_sidecar_path.read_text(encoding="utf-8")) + _expect( + [variant.get("id") for variant in applied_footjob_baseline_sidecar.get("prompt_variants", [])] + == ["existing_axis"], + "Atlas refine baseline score apply must preserve existing prompt variants without adding new ones", + ) + _expect( + applied_footjob_baseline_sidecar.get("score", {}).get("atlas_pose_match") == "partial", + "Atlas refine baseline score apply should write partial baseline score metadata", + ) + _expect( + applied_footjob_baseline_sidecar.get("baseline_analysis_notes") == "Partial baseline is still useful as scored evidence.", + "Atlas refine baseline score apply should keep manual baseline analysis notes", + ) + baseline_handjob_sidecar_path = baseline_apply_root / "pov_handjob_upright_centered_00001_.json" + applied_handjob_baseline_sidecar = json.loads(baseline_handjob_sidecar_path.read_text(encoding="utf-8")) + _expect( + applied_handjob_baseline_sidecar.get("score", {}).get("pose_ownership") == "pass", + "Atlas refine baseline score apply should write passing baseline score metadata", + ) + _expect( + applied_handjob_baseline_sidecar.get("cue_axes", {}).get("hand_position") == "centered_upright_base_grip", + "Atlas refine baseline score apply should write manually reviewed cue axes", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as baseline_apply_handle: + json.dump(baseline_score_update_draft, baseline_apply_handle) + cli_baseline_apply_path = Path(baseline_apply_handle.name) + try: + baseline_apply_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--apply-baseline-score-update-draft", + "--baseline-score-update-draft-json", + str(cli_baseline_apply_path), + "--folder", + str(baseline_apply_root), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_baseline_apply_path.unlink(missing_ok=True) + _expect( + baseline_apply_cli_result.returncode == 0, + f"Atlas refine baseline score apply CLI failed: {baseline_apply_cli_result.stderr}", + ) + cli_baseline_apply_report = json.loads(baseline_apply_cli_result.stdout) + _expect( + cli_baseline_apply_report.get("applied") is True, + "Atlas refine baseline score apply CLI should report applied", + ) + (baseline_apply_root / "pov_footjob_frontal_sole_stroke_00001_.txt").write_text( + footjob.get("prompt_text", ""), + encoding="utf-8", + ) + (baseline_apply_root / "pov_footjob_frontal_sole_stroke_00001_.png").write_bytes(b"fake-png") + (baseline_apply_root / "pov_handjob_upright_centered_00001_.txt").write_text( + handjob_baseline_score.get("prompt_text", ""), + encoding="utf-8", + ) + (baseline_apply_root / "pov_handjob_upright_centered_00001_.png").write_bytes(b"fake-png") + baseline_applied_manifest = manifest_module.build_manifest(baseline_apply_root, subject_id="same_woman_001") + baseline_applied_sheet = manifest_module.build_baseline_score_sheet(baseline_applied_manifest) + baseline_applied_entries = { + entry.get("variant_key"): entry for entry in baseline_applied_sheet.get("entries") or [] + } + _expect( + baseline_applied_entries.get("pov_footjob_frontal_sole_stroke", {}).get("score_state") + == "partially_scored", + "Atlas refine baseline score apply should rescan partial baseline score state", + ) + _expect( + baseline_applied_entries.get("pov_handjob_upright_centered", {}).get("score_state") == "scored_pass", + "Atlas refine baseline score apply should rescan passing baseline score state", + ) + clean_noise_report = manifest_module.build_prompt_noise_report(manifest) + _expect( + clean_noise_report.get("schema") == "sxcp_atlas_refine_prompt_noise_report_v1", + "Atlas refine prompt-noise report lost schema", + ) + _expect( + clean_noise_report.get("issue_count") == 0, + f"Atlas refine prompt-noise report should not flag clean direct atlas prompts: {clean_noise_report}", + ) + _expect( + prompt_noise_cli_returncode == 0, + f"Atlas refine prompt-noise report CLI failed: {prompt_noise_cli_stderr}", + ) + cli_prompt_noise_report = json.loads(prompt_noise_cli_stdout) + _expect( + cli_prompt_noise_report.get("issue_count") == 0, + "Atlas refine prompt-noise report CLI should preserve clean issue count", + ) + clean_cleanup_sheet = manifest_module.build_prompt_cleanup_sheet(manifest) + _expect( + clean_cleanup_sheet.get("schema") == "sxcp_atlas_refine_prompt_cleanup_sheet_v1", + "Atlas refine prompt-cleanup sheet lost schema", + ) + _expect( + clean_cleanup_sheet.get("cleanup_item_count") == 0, + f"Atlas refine prompt-cleanup sheet should not emit cleanup items for clean prompts: {clean_cleanup_sheet}", + ) + _expect( + prompt_cleanup_cli_returncode == 0, + f"Atlas refine prompt-cleanup sheet CLI failed: {prompt_cleanup_cli_stderr}", + ) + cli_prompt_cleanup_sheet = json.loads(prompt_cleanup_cli_stdout) + _expect( + cli_prompt_cleanup_sheet.get("cleanup_item_count") == 0, + "Atlas refine prompt-cleanup sheet CLI should preserve clean cleanup count", + ) + noisy_manifest = json.loads(json.dumps(manifest)) + noisy_entries = { + entry.get("variant_key"): entry for entry in noisy_manifest.get("entries") or [] + } + noisy_handjob = noisy_entries.get("pov_handjob_upright_centered") or {} + noisy_handjob["prompt_text"] += ( + " Keep the visible partner and the action primary; context stays beside or behind the bodies." + " The viewer looks straight down from his torso." + " The viewer looks straight down from his torso." + ) + noisy_handjob["prompt_sha256"] = manifest_module._sha256_text(noisy_handjob["prompt_text"]) + noisy_footjob = noisy_entries.get("pov_footjob_frontal_sole_stroke") or {} + noisy_footjob.setdefault("prompt_variants", []).append( + { + "id": "noisy_option_wording", + "append_cues": [ + "either hand or foot may move optionally while the POV foreground clothing cue stays visible" + ], + "cue_axes": {"foot_position": "ambiguous_option_axis"}, + } + ) + noisy_footjob.setdefault("prompt_variants", []).append( + { + "id": "noisy_negative_text", + "text": "No lower torso should appear; keep the visible partner primary.", + "cue_axes": {"body_angle": "negative_meta_text"}, + } + ) + noisy_noise_report = manifest_module.build_prompt_noise_report(noisy_manifest) + _expect( + noisy_noise_report.get("issue_count", 0) >= 6, + f"Atlas refine prompt-noise report should flag option/meta/negative prompt noise: {noisy_noise_report}", + ) + noisy_issue_codes = [ + issue.get("code") + for entry in noisy_noise_report.get("entries") or [] + for issue in entry.get("issues") or [] + ] + _expect( + "meta_instruction" in noisy_issue_codes, + f"Atlas refine prompt-noise report should flag meta instructions: {noisy_noise_report}", + ) + _expect( + "option_word" in noisy_issue_codes, + f"Atlas refine prompt-noise report should flag option-list wording: {noisy_noise_report}", + ) + _expect( + "negative_conditioning" in noisy_issue_codes, + f"Atlas refine prompt-noise report should flag negative positive-channel wording: {noisy_noise_report}", + ) + _expect( + "duplicate_phrase" in noisy_issue_codes, + f"Atlas refine prompt-noise report should flag repeated prompt phrases: {noisy_noise_report}", + ) + _expect( + any(issue.get("context") == "prompt_variant_append_cue" for entry in noisy_noise_report.get("entries") or [] for issue in entry.get("issues") or []), + f"Atlas refine prompt-noise report should localize append-cue noise: {noisy_noise_report}", + ) + noisy_coverage_report = manifest_module.build_coverage_report(noisy_manifest) + _expect( + noisy_coverage_report.get("needs_prompt_cleanup_count") == 2, + f"Atlas refine coverage should count noisy entries as cleanup-needed: {noisy_coverage_report}", + ) + _expect( + noisy_coverage_report.get("prompt_noise_issue_count", 0) >= 6, + f"Atlas refine coverage should carry prompt-noise issue totals: {noisy_coverage_report}", + ) + noisy_coverage_by_variant = { + entry.get("variant_key"): entry for entry in noisy_coverage_report.get("entries") or [] + } + noisy_handjob_coverage = noisy_coverage_by_variant.get("pov_handjob_upright_centered") or {} + _expect( + noisy_handjob_coverage.get("state") == "needs_prompt_cleanup", + f"Atlas refine coverage should block noisy baseline-only entries before scoring: {noisy_handjob_coverage}", + ) + noisy_footjob_coverage = noisy_coverage_by_variant.get("pov_footjob_frontal_sole_stroke") or {} + _expect( + noisy_footjob_coverage.get("state") == "needs_prompt_cleanup", + f"Atlas refine coverage should block noisy sidecar variants before seed selection: {noisy_footjob_coverage}", + ) + _expect( + noisy_footjob_coverage.get("prompt_noise_issue_count", 0) >= 4, + f"Atlas refine coverage should localize sidecar prompt-noise counts: {noisy_footjob_coverage}", + ) + noisy_cleanup_sheet = manifest_module.build_prompt_cleanup_sheet(noisy_manifest) + _expect( + noisy_cleanup_sheet.get("schema") == "sxcp_atlas_refine_prompt_cleanup_sheet_v1", + "Atlas refine prompt-cleanup sheet lost schema for noisy prompts", + ) + _expect( + noisy_cleanup_sheet.get("cleanup_item_count") == 3, + f"Atlas refine prompt-cleanup sheet should group issues by editable source text: {noisy_cleanup_sheet}", + ) + _expect( + noisy_cleanup_sheet.get("issue_count", 0) >= 6, + f"Atlas refine prompt-cleanup sheet should carry all issue counts: {noisy_cleanup_sheet}", + ) + cleanup_items = { + (item.get("variant_key"), item.get("context"), item.get("prompt_variant_id")): item + for item in noisy_cleanup_sheet.get("cleanup_items") or [] + } + handjob_cleanup = cleanup_items.get(("pov_handjob_upright_centered", "baseline_prompt", "")) or {} + _expect( + handjob_cleanup.get("source_type") == "prompt_file", + f"Atlas refine prompt-cleanup sheet should point baseline cleanup at prompt files: {handjob_cleanup}", + ) + _expect( + handjob_cleanup.get("source_path") == handjob_baseline_score.get("prompt_path"), + f"Atlas refine prompt-cleanup sheet should keep exact prompt path: {handjob_cleanup}", + ) + _expect( + handjob_cleanup.get("source_prompt_sha256") == noisy_handjob.get("prompt_sha256"), + f"Atlas refine prompt-cleanup sheet should preserve baseline source prompt hash: {handjob_cleanup}", + ) + _expect( + "Keep the visible partner" in handjob_cleanup.get("current_text", ""), + "Atlas refine prompt-cleanup sheet should preserve current noisy baseline text", + ) + _expect( + handjob_cleanup.get("current_text_sha256") == manifest_module._sha256_text(handjob_cleanup.get("current_text", "")), + f"Atlas refine prompt-cleanup sheet should hash current baseline text for drift checks: {handjob_cleanup}", + ) + _expect( + handjob_cleanup.get("replacement_text") == "", + "Atlas refine prompt-cleanup sheet should leave replacement text blank for manual cleanup", + ) + append_cleanup = cleanup_items.get(("pov_footjob_frontal_sole_stroke", "prompt_variant_append_cue", "noisy_option_wording")) or {} + _expect( + append_cleanup.get("source_type") == "sidecar_prompt_variant_append_cue", + f"Atlas refine prompt-cleanup sheet should point append-cue cleanup at sidecar variants: {append_cleanup}", + ) + _expect( + append_cleanup.get("cue_index") == 0, + "Atlas refine prompt-cleanup sheet should preserve append-cue index", + ) + _expect( + append_cleanup.get("sidecar_filename") == "pov_footjob_frontal_sole_stroke_00001_.json", + f"Atlas refine prompt-cleanup sheet should preserve same-stem sidecar filename: {append_cleanup}", + ) + _expect( + append_cleanup.get("source_prompt_sha256") == noisy_footjob.get("prompt_sha256"), + f"Atlas refine prompt-cleanup sheet should preserve sidecar baseline source prompt hash: {append_cleanup}", + ) + _expect( + append_cleanup.get("current_text_sha256") == manifest_module._sha256_text(append_cleanup.get("current_text", "")), + f"Atlas refine prompt-cleanup sheet should hash current append-cue text for drift checks: {append_cleanup}", + ) + invalid_cleanup_sheet = json.loads(json.dumps(noisy_cleanup_sheet)) + invalid_cleanup_sheet["cleanup_items"][0]["replacement_text"] = "" + invalid_cleanup_sheet["cleanup_items"][1]["replacement_text"] = "either noisy option remains" + invalid_cleanup_sheet["cleanup_items"][2]["current_text_sha256"] = "stale-hash" + invalid_cleanup_sheet["cleanup_items"][2]["source_prompt_sha256"] = "" + invalid_cleanup_validation = manifest_module.validate_prompt_cleanup_sheet(invalid_cleanup_sheet) + _expect( + invalid_cleanup_validation.get("schema") == "sxcp_atlas_refine_prompt_cleanup_validation_v1", + "Atlas refine prompt-cleanup validation lost schema", + ) + _expect( + invalid_cleanup_validation.get("valid") is False, + f"Atlas refine prompt-cleanup validation should reject blank or noisy replacements: {invalid_cleanup_validation}", + ) + _expect( + any("replacement_text is required" in error for error in invalid_cleanup_validation.get("errors", [])), + f"Atlas refine prompt-cleanup validation should reject blank replacements: {invalid_cleanup_validation}", + ) + _expect( + any("replacement_text still has prompt-noise issues" in error for error in invalid_cleanup_validation.get("errors", [])), + f"Atlas refine prompt-cleanup validation should reject noisy replacements: {invalid_cleanup_validation}", + ) + _expect( + any("current_text_sha256" in error for error in invalid_cleanup_validation.get("errors", [])), + f"Atlas refine prompt-cleanup validation should reject stale current-text hashes: {invalid_cleanup_validation}", + ) + _expect( + any("source_prompt_sha256" in error for error in invalid_cleanup_validation.get("errors", [])), + f"Atlas refine prompt-cleanup validation should reject missing source prompt hashes: {invalid_cleanup_validation}", + ) + cleanup_sheet_for_apply = json.loads(json.dumps(noisy_cleanup_sheet)) + cleanup_sheet_for_apply["cleanup_items"] = [ + json.loads(json.dumps(handjob_cleanup)), + json.loads(json.dumps(append_cleanup)), + ] + cleanup_sheet_for_apply["cleanup_items"][0]["replacement_text"] = ( + "A controlled same-subject handjob reference prompt with direct centered upright contact." + ) + cleanup_sheet_for_apply["cleanup_items"][0]["cleanup_notes"] = "manual direct rewrite" + cleanup_sheet_for_apply["cleanup_items"][1]["replacement_text"] = ( + "the woman's foot presses forward along the same contact line" + ) + cleanup_validation = manifest_module.validate_prompt_cleanup_sheet(cleanup_sheet_for_apply) + _expect( + cleanup_validation.get("valid") is True, + f"Atlas refine prompt-cleanup validation should accept clean manual replacements: {cleanup_validation}", + ) + stale_source_hash_cleanup_sheet = json.loads(json.dumps(cleanup_sheet_for_apply)) + stale_source_hash_cleanup_sheet["cleanup_items"][0]["source_prompt_sha256"] = "stale-source-hash" + stale_source_hash_validation = manifest_module.validate_prompt_cleanup_sheet(stale_source_hash_cleanup_sheet) + _expect( + stale_source_hash_validation.get("valid") is False + and any( + "source_prompt_sha256 must match current_text_sha256" + in error + for error in stale_source_hash_validation.get("errors", []) + ), + f"Atlas refine prompt-cleanup validation should reject stale baseline source prompt hashes: {stale_source_hash_validation}", + ) + with tempfile.TemporaryDirectory() as cleanup_tmpdir: + cleanup_root = Path(cleanup_tmpdir) + cleanup_handjob_prompt = cleanup_root / "pov_handjob_upright_centered_00001_.txt" + cleanup_handjob_image = cleanup_root / "pov_handjob_upright_centered_00001_.png" + cleanup_footjob_prompt = cleanup_root / "pov_footjob_frontal_sole_stroke_00001_.txt" + cleanup_footjob_image = cleanup_root / "pov_footjob_frontal_sole_stroke_00001_.png" + cleanup_footjob_sidecar = cleanup_root / "pov_footjob_frontal_sole_stroke_00001_.json" + cleanup_handjob_prompt.write_text(handjob_cleanup.get("current_text", ""), encoding="utf-8") + cleanup_handjob_image.write_bytes(b"fake-png") + cleanup_footjob_prompt.write_text(noisy_footjob.get("prompt_text", ""), encoding="utf-8") + cleanup_footjob_image.write_bytes(b"fake-png") + cleanup_footjob_sidecar.write_text( + json.dumps( + { + "notes": "preserve cleanup sidecar notes", + "prompt_variants": [ + { + "id": "noisy_option_wording", + "append_cues": [ + append_cleanup.get("current_text", ""), + "existing second cue stays", + ], + "cue_axes": {"foot_position": "ambiguous_option_axis"}, + } + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + cleanup_sheet_for_apply["cleanup_items"][0]["source_path"] = str(cleanup_handjob_prompt) + cleanup_sheet_for_apply["cleanup_items"][1]["source_path"] = str(cleanup_footjob_sidecar) + cleanup_apply_report = manifest_module.apply_prompt_cleanup_sheet(cleanup_sheet_for_apply, cleanup_root) + _expect( + cleanup_apply_report.get("schema") == "sxcp_atlas_refine_prompt_cleanup_apply_report_v1", + "Atlas refine prompt-cleanup apply report lost schema", + ) + _expect( + cleanup_apply_report.get("applied") is True, + f"Atlas refine prompt-cleanup apply should accept clean manual replacements: {cleanup_apply_report}", + ) + _expect( + cleanup_apply_report.get("updated_file_count") == 2, + "Atlas refine prompt-cleanup apply should update prompt file and sidecar file", + ) + _expect( + cleanup_handjob_prompt.read_text(encoding="utf-8") + == "A controlled same-subject handjob reference prompt with direct centered upright contact.", + "Atlas refine prompt-cleanup apply should rewrite the prompt file with manual replacement", + ) + applied_cleanup_sidecar = json.loads(cleanup_footjob_sidecar.read_text(encoding="utf-8")) + applied_cleanup_variant = (applied_cleanup_sidecar.get("prompt_variants") or [{}])[0] + _expect( + applied_cleanup_sidecar.get("notes") == "preserve cleanup sidecar notes", + "Atlas refine prompt-cleanup apply should preserve unrelated sidecar metadata", + ) + _expect( + applied_cleanup_variant.get("append_cues", [])[0] + == "the woman's foot presses forward along the same contact line", + "Atlas refine prompt-cleanup apply should replace the targeted append cue", + ) + _expect( + applied_cleanup_variant.get("append_cues", [])[1] == "existing second cue stays", + "Atlas refine prompt-cleanup apply should preserve untargeted append cues", + ) + cleanup_manifest = manifest_module.build_manifest(cleanup_root, subject_id="same_woman_001") + cleanup_noise_report = manifest_module.build_prompt_noise_report(cleanup_manifest) + _expect( + cleanup_noise_report.get("issue_count") == 0, + f"Atlas refine prompt-cleanup apply should remove prompt-noise issues from the cleaned temp deck: {cleanup_noise_report}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as cleanup_sheet_handle: + json.dump(cleanup_sheet_for_apply, cleanup_sheet_handle) + cli_cleanup_sheet_path = Path(cleanup_sheet_handle.name) + try: + cleanup_validation_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--validate-prompt-cleanup-sheet", + "--prompt-cleanup-sheet-json", + str(cli_cleanup_sheet_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_cleanup_sheet_path.unlink(missing_ok=True) + _expect( + cleanup_validation_cli_result.returncode == 0, + f"Atlas refine prompt-cleanup validation CLI failed: {cleanup_validation_cli_result.stderr}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as cleanup_apply_sheet_handle: + json.dump(cleanup_sheet_for_apply, cleanup_apply_sheet_handle) + cli_cleanup_apply_sheet_path = Path(cleanup_apply_sheet_handle.name) + try: + cleanup_apply_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--apply-prompt-cleanup-sheet", + "--prompt-cleanup-sheet-json", + str(cli_cleanup_apply_sheet_path), + "--folder", + str(cleanup_root), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_cleanup_apply_sheet_path.unlink(missing_ok=True) + _expect( + cleanup_apply_cli_result.returncode == 0, + f"Atlas refine prompt-cleanup apply CLI failed: {cleanup_apply_cli_result.stderr}", + ) + cli_cleanup_apply = json.loads(cleanup_apply_cli_result.stdout) + _expect( + cli_cleanup_apply.get("applied") is True, + "Atlas refine prompt-cleanup apply CLI should report applied", + ) + coverage_report = manifest_module.build_coverage_report(manifest) + _expect( + coverage_report.get("schema") == "sxcp_atlas_refine_coverage_report_v1", + "Atlas refine coverage report lost schema", + ) + _expect( + coverage_report.get("entry_count") == 3, + "Atlas refine coverage report should keep manifest entry count", + ) + coverage_by_variant = {entry.get("variant_key"): entry for entry in coverage_report.get("entries") or []} + footjob_coverage = coverage_by_variant.get("pov_footjob_frontal_sole_stroke") or {} + _expect( + footjob_coverage.get("state") == "needs_visual_score", + f"Atlas refine coverage should flag unscored prompt variants: {footjob_coverage}", + ) + _expect( + footjob_coverage.get("prompt_variant_count") == 1 and footjob_coverage.get("unscored_variant_count") == 1, + "Atlas refine coverage should count unscored prompt variants", + ) + unknown_coverage = coverage_by_variant.get("pov_unknown_pose_candidate") or {} + _expect( + unknown_coverage.get("state") == "unknown_variant", + "Atlas refine coverage should flag unknown variant entries before seed testing", + ) + handjob_coverage = coverage_by_variant.get("pov_handjob_upright_centered") or {} + _expect( + handjob_coverage.get("state") == "baseline_only", + "Atlas refine coverage should flag known entries with no sidecar variants as baseline-only", + ) + reference_pool_report = manifest_module.build_reference_pool_report( + "pov_blowjob_top_down_vertical_shaft", + supplemental_folders=["1.original/blowjob_top_view_1024"], + ) + _expect( + reference_pool_report.get("schema") == "sxcp_atlas_reference_pool_report_v1", + "Atlas reference pool report lost schema", + ) + _expect( + reference_pool_report.get("variant_key") == "pov_blowjob_top_down_vertical_shaft", + "Atlas reference pool report lost variant key", + ) + _expect( + reference_pool_report.get("canonical_image_count") == 17, + f"Atlas reference pool report should count curated top-view references: {reference_pool_report}", + ) + _expect( + reference_pool_report.get("supplemental_image_count") == 27, + f"Atlas reference pool report should count supplemental raw top-view references: {reference_pool_report}", + ) + _expect( + reference_pool_report.get("matched_image_count") == 17, + f"Atlas reference pool report should match curated refs to raw counterparts by image id: {reference_pool_report}", + ) + _expect( + reference_pool_report.get("supplemental_extra_count") == 10, + f"Atlas reference pool report should surface raw-only cue-expansion images: {reference_pool_report}", + ) + _expect( + "1.original/blowjob_top_view_1024/16.png" in reference_pool_report.get("supplemental_extra_images", []), + f"Atlas reference pool report should include raw-only image ids for cue mining: {reference_pool_report}", + ) + _expect( + "blowjob_top_view/22_blowjob_top_view.png" in reference_pool_report.get("catalog_reference_images", []), + f"Atlas reference pool report should keep curated catalog reference images: {reference_pool_report}", + ) + reference_pool_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-reference-pool-report", + "--variant-key", + "pov_blowjob_top_down_vertical_shaft", + "--reference-pool-folder", + "1.original/blowjob_top_view_1024", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect( + reference_pool_cli_result.returncode == 0, + f"Atlas reference pool report CLI failed: {reference_pool_cli_result.stderr}", + ) + cli_reference_pool_report = json.loads(reference_pool_cli_result.stdout) + _expect( + cli_reference_pool_report.get("supplemental_extra_count") == 10, + "Atlas reference pool report CLI should keep supplemental raw-only count", + ) + reference_cue_review_sheet = manifest_module.build_reference_cue_review_sheet( + "pov_blowjob_top_down_vertical_shaft", + supplemental_folders=["1.original/blowjob_top_view_1024"], + ) + _expect( + reference_cue_review_sheet.get("schema") == "sxcp_atlas_reference_cue_review_sheet_v1", + "Atlas reference cue-review sheet lost schema", + ) + _expect( + reference_cue_review_sheet.get("review_item_count") == 27, + f"Atlas reference cue-review sheet should cover curated refs plus raw-only extras: {reference_cue_review_sheet}", + ) + review_items_by_image = { + item.get("canonical_image") or item.get("supplemental_image"): item + for item in reference_cue_review_sheet.get("review_items") or [] + } + catalog_reference_item = review_items_by_image.get("blowjob_top_view/22_blowjob_top_view.png") or {} + _expect( + catalog_reference_item.get("role") == "catalog_reference", + f"Atlas reference cue-review sheet should mark curated catalog anchors: {catalog_reference_item}", + ) + _expect( + catalog_reference_item.get("supplemental_image") == "1.original/blowjob_top_view_1024/22.png", + f"Atlas reference cue-review sheet should keep matched raw counterpart provenance: {catalog_reference_item}", + ) + _expect( + catalog_reference_item.get("reference_images_template") == ["blowjob_top_view/22_blowjob_top_view.png"], + f"Atlas reference cue-review sheet should provide canonical sidecar reference template: {catalog_reference_item}", + ) + _expect( + catalog_reference_item.get("observed_positive_cues") == [], + "Atlas reference cue-review sheet must leave positive cue extraction blank for manual review", + ) + raw_extra_item = review_items_by_image.get("1.original/blowjob_top_view_1024/16.png") or {} + _expect( + raw_extra_item.get("role") == "supplemental_extra", + f"Atlas reference cue-review sheet should mark raw-only extras separately: {raw_extra_item}", + ) + _expect( + raw_extra_item.get("reference_images_template") == [], + f"Atlas reference cue-review sheet should not auto-promote raw-only extras into sidecar reference templates: {raw_extra_item}", + ) + _expect( + all(value == "" for value in (raw_extra_item.get("cue_axes") or {}).values()), + f"Atlas reference cue-review sheet should leave cue axes blank for human labeling: {raw_extra_item}", + ) + reference_cue_review_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-reference-cue-review-sheet", + "--variant-key", + "pov_blowjob_top_down_vertical_shaft", + "--reference-pool-folder", + "1.original/blowjob_top_view_1024", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect( + reference_cue_review_cli_result.returncode == 0, + f"Atlas reference cue-review sheet CLI failed: {reference_cue_review_cli_result.stderr}", + ) + cli_reference_cue_review_sheet = json.loads(reference_cue_review_cli_result.stdout) + _expect( + cli_reference_cue_review_sheet.get("review_item_count") == 27, + "Atlas reference cue-review sheet CLI should keep review item count", + ) + filled_reference_cue_review_sheet = json.loads(json.dumps(reference_cue_review_sheet)) + filled_review_items_by_image = { + item.get("canonical_image") or item.get("supplemental_image"): item + for item in filled_reference_cue_review_sheet.get("review_items") or [] + } + filled_catalog_item = filled_review_items_by_image["blowjob_top_view/22_blowjob_top_view.png"] + filled_catalog_item["prompt_variant_template"]["id"] = "atlas22_upper_body_stack" + filled_catalog_item["observed_positive_cues"] = [ + "face, hair crown, shoulders, upper chest, and one hand form the primary visible partner stack" + ] + filled_catalog_item["cue_axes"]["camera_height"] = "near_vertical_overhead" + filled_catalog_item["cue_axes"]["workspace_surface"] = "flat_floor_plane" + filled_catalog_item["cue_axes"]["hand_position"] = "one_hand_base_contact" + filled_catalog_item["review_notes"] = "canonical row reviewed from atlas image 22" + filled_exact_text_item = filled_review_items_by_image["blowjob_top_view/27_blowjob_top_view.png"] + filled_exact_text_item["prompt_variant_template"]["id"] = "atlas27_shaft_first_exact_text" + filled_exact_text_item["prompt_variant_template"]["text"] = ( + "A controlled same-subject prompt. Straight-down male POV oral close-up. " + "the centered shaft and mouth contact form the main vertical axis from the lower foreground to the woman's face. " + "the woman's face, hair crown, shoulders, upper chest, and one hand stack around the shaft-contact axis. " + "a flat floor plane fills the background as shallow overhead room evidence." + ) + filled_exact_text_item["observed_positive_cues"] = [ + "the centered shaft and mouth contact form the main vertical axis from the lower foreground to the woman's face" + ] + filled_exact_text_item["cue_axes"]["camera_height"] = "straight_down_overhead_shaft_first" + filled_exact_text_item["cue_axes"]["contact_depth"] = "shaft_contact_axis_centered_from_lower_foreground_to_mouth" + filled_exact_text_item["review_notes"] = "canonical row reviewed as exact-text shaft-first ordering" + filled_noisy_item = filled_review_items_by_image["blowjob_top_view/106_blowjob_top_view.png"] + filled_noisy_item["prompt_variant_template"]["id"] = "noisy_option_axis" + filled_noisy_item["observed_positive_cues"] = [ + "either a high top-down view or a flat top-down view" + ] + filled_raw_extra_item = filled_review_items_by_image["1.original/blowjob_top_view_1024/16.png"] + filled_raw_extra_item["prompt_variant_template"]["id"] = "raw16_floor_plane_axis" + filled_raw_extra_item["observed_positive_cues"] = [ + "carpet tile seams fill the support plane" + ] + filled_raw_extra_item["cue_axes"]["workspace_surface"] = "carpet_tile_floor_plane" + reference_cue_candidate_draft = manifest_module.build_reference_cue_candidate_draft( + filled_reference_cue_review_sheet + ) + _expect( + reference_cue_candidate_draft.get("schema") == "sxcp_atlas_reference_cue_candidate_draft_v1", + "Atlas reference cue candidate draft lost schema", + ) + _expect( + reference_cue_candidate_draft.get("ready_candidate_count") == 2, + f"Atlas reference cue candidate draft should only promote canonical filled cues: {reference_cue_candidate_draft}", + ) + candidates_by_id = { + candidate.get("prompt_variant_id"): candidate + for candidate in reference_cue_candidate_draft.get("candidates") or [] + } + candidate = candidates_by_id.get("atlas22_upper_body_stack") or {} + _expect( + candidate.get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + f"Atlas reference cue candidate draft should keep canonical reference image provenance: {candidate}", + ) + _expect( + candidate.get("prompt_variant", {}).get("append_cues") == [ + "face, hair crown, shoulders, upper chest, and one hand form the primary visible partner stack" + ], + f"Atlas reference cue candidate draft should use reviewed positive cues as append cues: {candidate}", + ) + _expect( + candidate.get("prompt_variant", {}).get("cue_axes", {}).get("camera_height") == "near_vertical_overhead", + f"Atlas reference cue candidate draft should carry reviewed cue axes: {candidate}", + ) + exact_text_candidate = candidates_by_id.get("atlas27_shaft_first_exact_text") or {} + _expect( + "text" in (exact_text_candidate.get("prompt_variant") or {}) + and "append_cues" not in (exact_text_candidate.get("prompt_variant") or {}), + f"Atlas reference cue candidate draft should preserve reviewed exact text instead of append cues: {exact_text_candidate}", + ) + _expect( + (exact_text_candidate.get("prompt_variant") or {}).get("text", "").startswith( + "A controlled same-subject prompt. Straight-down male POV oral close-up." + ), + f"Atlas reference cue candidate draft should keep exact-text wording order: {exact_text_candidate}", + ) + skipped_reasons = { + (item.get("id"), item.get("reason")) + for item in reference_cue_candidate_draft.get("skipped") or [] + } + _expect( + ("106", "prompt_noise_issue") in skipped_reasons, + f"Atlas reference cue candidate draft should skip noisy reviewed cues: {reference_cue_candidate_draft}", + ) + _expect( + ("16", "supplemental_extra_needs_canonical_reference") in skipped_reasons, + f"Atlas reference cue candidate draft should not auto-promote raw-only extras: {reference_cue_candidate_draft}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as cue_candidate_sheet_handle: + json.dump(filled_reference_cue_review_sheet, cue_candidate_sheet_handle) + cue_candidate_sheet_path = Path(cue_candidate_sheet_handle.name) + try: + reference_cue_candidate_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-reference-cue-candidate-draft", + "--reference-cue-review-sheet-json", + str(cue_candidate_sheet_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cue_candidate_sheet_path.unlink(missing_ok=True) + _expect( + reference_cue_candidate_cli_result.returncode == 0, + f"Atlas reference cue candidate draft CLI failed: {reference_cue_candidate_cli_result.stderr}", + ) + cli_reference_cue_candidate_draft = json.loads(reference_cue_candidate_cli_result.stdout) + _expect( + cli_reference_cue_candidate_draft.get("ready_candidate_count") == 2, + "Atlas reference cue candidate draft CLI should keep ready candidate count", + ) + with tempfile.TemporaryDirectory() as reference_author_tmpdir: + reference_author_root = Path(reference_author_tmpdir) + reference_author_prompt = reference_author_root / "pov_blowjob_top_down_vertical_shaft_00001_.txt" + reference_author_image = reference_author_root / "pov_blowjob_top_down_vertical_shaft_00001_.png" + reference_author_prompt.write_text( + "A controlled same-subject top-view oral baseline prompt awaiting atlas cue candidates.", + encoding="utf-8", + ) + reference_author_image.write_bytes(b"fake-png") + reference_author_manifest = manifest_module.build_manifest( + reference_author_root, + subject_id="same_woman_001", + ) + reference_author_draft = manifest_module.build_reference_cue_sidecar_author_draft( + reference_author_manifest, + reference_cue_candidate_draft, + variant_key="pov_blowjob_top_down_vertical_shaft", + ) + _expect( + reference_author_draft.get("schema") == "sxcp_atlas_reference_cue_sidecar_author_draft_v1", + "Atlas reference sidecar author draft lost schema", + ) + _expect( + reference_author_draft.get("update_count") == 1, + f"Atlas reference sidecar author draft should target the same-stem baseline entry: {reference_author_draft}", + ) + reference_author_update = (reference_author_draft.get("updates") or [{}])[0] + _expect( + reference_author_update.get("sidecar_filename") == "pov_blowjob_top_down_vertical_shaft_00001_.json", + f"Atlas reference sidecar author draft should use same-stem sidecar filenames: {reference_author_update}", + ) + _expect( + [variant.get("id") for variant in reference_author_update.get("prompt_variants") or []] + == ["atlas22_upper_body_stack", "atlas27_shaft_first_exact_text"], + f"Atlas reference sidecar author draft should carry reviewed prompt variants: {reference_author_update}", + ) + reference_author_variants_by_id = { + variant.get("id"): variant + for variant in reference_author_update.get("prompt_variants") or [] + } + _expect( + (reference_author_variants_by_id.get("atlas27_shaft_first_exact_text") or {}) + .get("prompt_source", {}) + .get("kind") + == "text", + f"Atlas reference sidecar author draft should mark exact-text provenance: {reference_author_update}", + ) + reference_author_validation = manifest_module.validate_reference_cue_sidecar_author_draft(reference_author_draft) + _expect( + reference_author_validation.get("valid") is True, + f"Atlas reference sidecar author draft should validate before apply: {reference_author_validation}", + ) + invalid_reference_author_draft = json.loads(json.dumps(reference_author_draft)) + invalid_reference_author_draft["updates"][0]["prompt_variants"][0]["append_cues"][0] = ( + "either the reviewed cue or another cue" + ) + invalid_reference_author_validation = manifest_module.validate_reference_cue_sidecar_author_draft( + invalid_reference_author_draft + ) + _expect( + invalid_reference_author_validation.get("valid") is False + and any("prompt_noise" in error for error in invalid_reference_author_validation.get("errors", [])), + f"Atlas reference sidecar author draft validation should reject noisy append cues: {invalid_reference_author_validation}", + ) + reference_author_apply_report = manifest_module.apply_reference_cue_sidecar_author_draft( + reference_author_draft, + reference_author_root, + ) + _expect( + reference_author_apply_report.get("schema") == "sxcp_atlas_reference_cue_sidecar_author_apply_report_v1", + "Atlas reference sidecar author apply lost schema", + ) + _expect( + reference_author_apply_report.get("applied") is True + and reference_author_apply_report.get("updated_file_count") == 1, + f"Atlas reference sidecar author apply should write one sidecar: {reference_author_apply_report}", + ) + authored_sidecar = json.loads( + (reference_author_root / "pov_blowjob_top_down_vertical_shaft_00001_.json").read_text(encoding="utf-8") + ) + _expect( + authored_sidecar.get("prompt_variants", [{}])[0].get("reference_images") == [ + "blowjob_top_view/22_blowjob_top_view.png" + ], + f"Atlas reference sidecar author apply should preserve reference provenance: {authored_sidecar}", + ) + rescanned_reference_author_manifest = manifest_module.build_manifest( + reference_author_root, + subject_id="same_woman_001", + ) + reference_author_batch = manifest_module.build_prompt_batch( + rescanned_reference_author_manifest, + "pov_blowjob_top_down_vertical_shaft", + sampler_seed=101, + ) + reference_author_batch_probes = reference_author_batch.get("probes") or [] + _expect( + len(reference_author_batch_probes) == 3 + and reference_author_batch_probes[1].get("reference_images") == [ + "blowjob_top_view/22_blowjob_top_view.png" + ], + f"Atlas reference sidecar author apply should rescan into testable prompt batches: {reference_author_batch}", + ) + exact_text_probe = reference_author_batch_probes[2] + _expect( + exact_text_probe.get("text", "").startswith( + "A controlled same-subject prompt. Straight-down male POV oral close-up." + ) + and "awaiting atlas cue candidates" not in exact_text_probe.get("text", ""), + f"Atlas exact-text sidecar variants should replace the baseline prompt in prompt batches: {exact_text_probe}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as candidate_draft_handle: + json.dump(reference_cue_candidate_draft, candidate_draft_handle) + candidate_draft_path = Path(candidate_draft_handle.name) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as author_draft_handle: + json.dump(reference_author_draft, author_draft_handle) + author_draft_path = Path(author_draft_handle.name) + try: + reference_author_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(reference_author_root), + "--subject-id", + "same_woman_001", + "--print-reference-cue-sidecar-author-draft", + "--reference-cue-candidate-draft-json", + str(candidate_draft_path), + "--variant-key", + "pov_blowjob_top_down_vertical_shaft", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + reference_author_validate_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--validate-reference-cue-sidecar-author-draft", + "--reference-cue-sidecar-author-draft-json", + str(author_draft_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + reference_author_apply_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--apply-reference-cue-sidecar-author-draft", + "--reference-cue-sidecar-author-draft-json", + str(author_draft_path), + "--folder", + str(reference_author_root), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + candidate_draft_path.unlink(missing_ok=True) + author_draft_path.unlink(missing_ok=True) + _expect( + reference_author_cli_result.returncode == 0, + f"Atlas reference sidecar author draft CLI failed: {reference_author_cli_result.stderr}", + ) + cli_reference_author_draft = json.loads(reference_author_cli_result.stdout) + _expect( + cli_reference_author_draft.get("update_count") == 1, + "Atlas reference sidecar author draft CLI should keep update count", + ) + _expect( + reference_author_validate_cli_result.returncode == 0, + f"Atlas reference sidecar author validation CLI failed: {reference_author_validate_cli_result.stderr}", + ) + _expect( + reference_author_apply_cli_result.returncode == 0, + f"Atlas reference sidecar author apply CLI failed: {reference_author_apply_cli_result.stderr}", + ) + cli_reference_author_apply_report = json.loads(reference_author_apply_cli_result.stdout) + _expect( + cli_reference_author_apply_report.get("applied") is True, + "Atlas reference sidecar author apply CLI should report applied", + ) + sidecar_scaffold = manifest_module.build_sidecar_scaffold(manifest) + _expect( + sidecar_scaffold.get("schema") == "sxcp_atlas_refine_sidecar_scaffold_v1", + "Atlas refine sidecar scaffold lost schema", + ) + _expect( + sidecar_scaffold.get("scaffold_count") == 1, + f"Atlas refine sidecar scaffold should include only known baseline-only entries: {sidecar_scaffold}", + ) + scaffold_entry = (sidecar_scaffold.get("scaffolds") or [{}])[0] + _expect( + scaffold_entry.get("variant_key") == "pov_handjob_upright_centered", + "Atlas refine sidecar scaffold should target the baseline-only known variant", + ) + _expect( + scaffold_entry.get("sidecar_filename") == "pov_handjob_upright_centered_00001_.json", + "Atlas refine sidecar scaffold should use the same-stem sidecar filename", + ) + _expect( + scaffold_entry.get("sidecar_json", {}).get("prompt_variants") == [], + "Atlas refine sidecar scaffold should not invent prompt variants", + ) + _expect( + scaffold_entry.get("prompt_variant_template", {}).get("append_cues") == [], + "Atlas refine sidecar scaffold should leave append cues blank for user-authored variants", + ) + _expect( + scaffold_entry.get("prompt_variant_template", {}).get("reference_images") == [], + "Atlas refine sidecar scaffold should include a blank nearest-reference image list", + ) + _expect(scaffold_cli_returncode == 0, f"Atlas refine sidecar scaffold CLI failed: {scaffold_cli_stderr}") + cli_scaffold = json.loads(scaffold_cli_stdout) + _expect( + cli_scaffold.get("scaffold_count") == 1, + "Atlas refine sidecar scaffold CLI should keep scaffold count", + ) + batch = manifest_module.build_prompt_batch(manifest, "pov_footjob_frontal_sole_stroke") + _expect(batch.get("schema") == "sxcp_atlas_refine_prompt_batch_v1", "Atlas refine batch lost schema") + _expect(batch.get("seed") == 101, "Atlas refine batch should use the entry sampler seed") + _expect(batch.get("variant_key") == "pov_footjob_frontal_sole_stroke", "Atlas refine batch lost variant key") + batch_probes = batch.get("probes") or [] + _expect([probe.get("id") for probe in batch_probes] == [ + "pov_footjob_frontal_sole_stroke_00001__baseline", + "pov_footjob_frontal_sole_stroke_00001__soles_more_forward", + ], f"Atlas refine batch should include baseline then sidecar variant probes: {batch_probes}") + _expect( + batch_probes[0].get("text") == footjob.get("prompt_text"), + "Atlas refine batch baseline should preserve the exact source prompt", + ) + _expect( + "the woman's soles press farther forward along the same contact line" in batch_probes[1].get("text", ""), + "Atlas refine batch should append explicit sidecar cue text", + ) + _expect( + batch_probes[1].get("prompt_source", {}).get("kind") == "append_cues", + "Atlas refine batch should preserve that the candidate came from append_cues", + ) + _expect( + batch_probes[1].get("prompt_source", {}).get("append_cues") == [ + "the woman's soles press farther forward along the same contact line" + ], + "Atlas refine batch should preserve append-cue deltas for later catalog review", + ) + _expect( + batch_probes[1].get("cue_axes", {}).get("contact_depth") == "contact_line_farther_forward", + "Atlas refine batch should preserve variant cue-axis metadata", + ) + _expect( + batch_probes[1].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine batch should preserve prompt-variant reference-image provenance", + ) + _expect( + batch_probes[1].get("seed_metadata", {}).get("micro_position_seed") == 303, + "Atlas refine batch should preserve variant micro-position seed metadata", + ) + override_batch = manifest_module.build_prompt_batch( + manifest, + "pov_footjob_frontal_sole_stroke", + sampler_seed=909, + ) + override_batch_probes = override_batch.get("probes") or [] + _expect( + override_batch.get("seed") == 909, + "Atlas refine batch should use explicit sampler seed overrides", + ) + _expect( + override_batch_probes + and all(probe.get("seed_metadata", {}).get("sampler_seed") == 909 for probe in override_batch_probes), + f"Atlas refine batch probe metadata should reflect the actual sampler seed override: {override_batch_probes}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as handle: + json.dump(batch, handle) + batch_path = Path(handle.name) + try: + loaded_batch = sxcp_prompt_batch.load_batch(batch_path) + finally: + batch_path.unlink(missing_ok=True) + _expect(loaded_batch.get("seed") == 101, "Atlas refine batch should load through sxcp prompt batch helper") + _expect(len(loaded_batch.get("probes") or []) == 2, "Atlas refine batch should keep both probes through batch loader") + _expect( + (loaded_batch.get("probes") or [])[1].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "SxCP prompt batch loader should preserve reference-image provenance metadata", + ) + results = { + "seed": 101, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "pov_footjob_frontal_sole_stroke_00001__baseline", + "prompt_order": "subject_first", + "turn": 11, + "image_path": "/tmp/pov_footjob_baseline.png", + "returned_seed": 101, + }, + { + "id": "pov_footjob_frontal_sole_stroke_00001__soles_more_forward", + "prompt_order": "subject_first", + "turn": 12, + "image_path": "/tmp/pov_footjob_soles_more_forward.png", + "returned_seed": 101, + }, + ], + } + result_sheet = manifest_module.build_result_sheet(batch, results, notes="visual scoring pending") + _expect(result_sheet.get("schema") == "sxcp_atlas_refine_result_sheet_v1", "Atlas refine result sheet lost schema") + _expect(result_sheet.get("seed") == 101, "Atlas refine result sheet should keep the fixed sampler seed") + _expect( + result_sheet.get("variant_key") == "pov_footjob_frontal_sole_stroke", + "Atlas refine result sheet lost variant key", + ) + sheet_probes = result_sheet.get("probes") or [] + _expect([probe.get("id") for probe in sheet_probes] == [ + "pov_footjob_frontal_sole_stroke_00001__baseline", + "pov_footjob_frontal_sole_stroke_00001__soles_more_forward", + ], "Atlas refine result sheet should preserve batch/result probe order") + _expect( + sheet_probes[0].get("image_path") == "/tmp/pov_footjob_baseline.png", + "Atlas refine result sheet should keep baseline image path", + ) + _expect( + sheet_probes[1].get("turn") == 12, + "Atlas refine result sheet should keep returned turn numbers", + ) + _expect( + sheet_probes[1].get("text") == batch_probes[1].get("text"), + "Atlas refine result sheet should keep the exact candidate prompt text", + ) + _expect( + sheet_probes[1].get("cue_axes", {}).get("contact_depth") == "contact_line_farther_forward", + "Atlas refine result sheet should keep candidate cue axes", + ) + _expect( + sheet_probes[1].get("prompt_source", {}).get("kind") == "append_cues", + "Atlas refine result sheet should keep append-cue provenance", + ) + _expect( + sheet_probes[1].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine result sheet should preserve nearest atlas reference-image provenance", + ) + _expect( + sheet_probes[1].get("score", {}).get("subject_identity") is None, + "Atlas refine result sheet should leave subject-identity score unfilled for visual analysis", + ) + for score_key in ( + "atlas_pose_match", + "contact_match", + "pose_ownership", + "workspace_continuity", + "clothing_visibility", + "subject_identity", + "expression_eye_control", + "anatomy_proportion", + "prompt_noise", + ): + _expect(score_key in sheet_probes[1].get("score", {}), f"Atlas refine result sheet missing score slot {score_key}") + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as batch_handle: + json.dump(batch, batch_handle) + cli_batch_path = Path(batch_handle.name) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as result_handle: + json.dump(results, result_handle) + cli_result_path = Path(result_handle.name) + try: + sheet_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-result-sheet", + "--batch-json", + str(cli_batch_path), + "--result-json", + str(cli_result_path), + "--notes", + "visual scoring pending", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_batch_path.unlink(missing_ok=True) + cli_result_path.unlink(missing_ok=True) + _expect(sheet_cli_result.returncode == 0, f"Atlas refine result-sheet CLI failed: {sheet_cli_result.stderr}") + cli_sheet = json.loads(sheet_cli_result.stdout) + _expect( + cli_sheet.get("schema") == "sxcp_atlas_refine_result_sheet_v1", + "Atlas refine result-sheet CLI lost schema", + ) + _expect( + len(cli_sheet.get("probes") or []) == 2, + "Atlas refine result-sheet CLI should keep both probes", + ) + scored_sheet = json.loads(json.dumps(result_sheet)) + scored_sheet["probes"][0]["score"].update( + { + "atlas_pose_match": "baseline", + "contact_match": "baseline", + "pose_ownership": "pass", + "workspace_continuity": "pass", + "clothing_visibility": "pass", + "subject_identity": "pass", + "expression_eye_control": "pass", + "anatomy_proportion": "pass", + "prompt_noise": "pass", + } + ) + scored_sheet["probes"][1]["score"].update( + { + "atlas_pose_match": "partial", + "contact_match": "pass", + "pose_ownership": "pass", + "workspace_continuity": "pass", + "clothing_visibility": "pass", + "subject_identity": "pass", + "expression_eye_control": "pass", + "anatomy_proportion": "pass", + "prompt_noise": "pass", + } + ) + scored_sheet["probes"][1]["analysis_notes"] = "Candidate keeps the same subject and moves the foot contact axis." + promotion_report = manifest_module.build_promotion_report(scored_sheet) + _expect( + promotion_report.get("schema") == "sxcp_atlas_refine_promotion_report_v1", + "Atlas refine promotion report lost schema", + ) + _expect(promotion_report.get("seed") == 101, "Atlas refine promotion report should keep the fixed sampler seed") + _expect(promotion_report.get("promotion_ready_count") == 1, "Atlas refine promotion report should count ready candidates") + _expect( + promotion_report.get("blocked_count") == 0, + "Atlas refine promotion report should not block a fully scored passing candidate", + ) + candidates = promotion_report.get("candidates") or [] + _expect(len(candidates) == 1, f"Atlas refine promotion report should include one non-baseline candidate: {candidates}") + _expect(candidates[0].get("decision") == "seedable_candidate", "Atlas refine promotion report should mark passing candidate seedable") + _expect( + candidates[0].get("prompt_variant_id") == "soles_more_forward", + "Atlas refine promotion report should recover sidecar prompt variant id", + ) + _expect( + candidates[0].get("cue_axes", {}).get("contact_depth") == "contact_line_farther_forward", + "Atlas refine promotion report should keep candidate cue axes", + ) + _expect( + candidates[0].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine promotion report should keep candidate reference-image provenance", + ) + _expect( + candidates[0].get("prompt_source", {}).get("append_cues") == [ + "the woman's soles press farther forward along the same contact line" + ], + "Atlas refine promotion report should keep append-cue provenance for catalog drafts", + ) + noisy_scored_sheet = json.loads(json.dumps(scored_sheet)) + noisy_scored_sheet["probes"][1]["text"] += " either the foot or hand moves while the contact stays." + noisy_promotion_report = manifest_module.build_promotion_report(noisy_scored_sheet) + noisy_candidate = (noisy_promotion_report.get("candidates") or [{}])[0] + _expect( + noisy_candidate.get("decision") == "rejected" + and "prompt_noise_issue" in noisy_candidate.get("blockers", []) + and any(issue.get("code") == "option_word" for issue in noisy_candidate.get("prompt_noise_issues", [])), + f"Atlas refine promotion report should reject noisy candidate text even when manual prompt_noise score passes: {noisy_candidate}", + ) + noisy_sidecar_draft = manifest_module.build_sidecar_update_draft(noisy_promotion_report) + _expect( + noisy_sidecar_draft.get("ready_candidate_count") == 0, + f"Atlas refine sidecar draft should skip noisy promoted candidates: {noisy_sidecar_draft}", + ) + scored_sheet["probes"][1]["score"]["subject_identity"] = "fail" + rejected_report = manifest_module.build_promotion_report(scored_sheet) + _expect( + rejected_report.get("promotion_ready_count") == 0, + "Atlas refine promotion report should block candidates that lose subject identity", + ) + _expect( + rejected_report.get("candidates", [{}])[0].get("decision") == "rejected", + "Atlas refine promotion report should reject failed preservation gates", + ) + scored_sheet["probes"][1]["score"]["subject_identity"] = "pass" + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as sheet_handle: + json.dump(scored_sheet, sheet_handle) + cli_sheet_path = Path(sheet_handle.name) + try: + promotion_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-promotion-report", + "--result-sheet-json", + str(cli_sheet_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_sheet_path.unlink(missing_ok=True) + _expect(promotion_cli_result.returncode == 0, f"Atlas refine promotion-report CLI failed: {promotion_cli_result.stderr}") + cli_promotion_report = json.loads(promotion_cli_result.stdout) + _expect( + cli_promotion_report.get("promotion_ready_count") == 1, + "Atlas refine promotion-report CLI should keep ready candidate count", + ) + sidecar_draft = manifest_module.build_sidecar_update_draft(promotion_report) + _expect( + sidecar_draft.get("schema") == "sxcp_atlas_refine_sidecar_update_draft_v1", + "Atlas refine sidecar update draft lost schema", + ) + _expect( + sidecar_draft.get("ready_candidate_count") == 1, + "Atlas refine sidecar update draft should count ready candidates", + ) + updates = sidecar_draft.get("updates") or [] + _expect(len(updates) == 1, f"Atlas refine sidecar update draft should include one sidecar update: {updates}") + _expect( + updates[0].get("sidecar_filename") == "pov_footjob_frontal_sole_stroke_00001_.json", + "Atlas refine sidecar update draft should preserve the original same-stem sidecar filename", + ) + drafted_variants = updates[0].get("prompt_variants") or [] + _expect(len(drafted_variants) == 1, "Atlas refine sidecar update draft should include one prompt variant") + _expect( + drafted_variants[0].get("id") == "soles_more_forward", + "Atlas refine sidecar update draft should preserve prompt variant id", + ) + _expect( + drafted_variants[0].get("text") == sheet_probes[1].get("text"), + "Atlas refine sidecar update draft should use the exact tested prompt text", + ) + _expect( + drafted_variants[0].get("prompt_source", {}).get("kind") == "append_cues", + "Atlas refine sidecar update draft should keep source prompt-variant kind", + ) + _expect( + drafted_variants[0].get("prompt_source", {}).get("append_cues") == [ + "the woman's soles press farther forward along the same contact line" + ], + "Atlas refine sidecar update draft should keep tested append-cue deltas", + ) + _expect( + drafted_variants[0].get("cue_axes", {}).get("contact_depth") == "contact_line_farther_forward", + "Atlas refine sidecar update draft should keep cue-axis metadata", + ) + _expect( + drafted_variants[0].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine sidecar update draft should keep nearest atlas reference-image provenance", + ) + _expect( + drafted_variants[0].get("evidence", {}).get("image_path") == "/tmp/pov_footjob_soles_more_forward.png", + "Atlas refine sidecar update draft should keep candidate evidence image path", + ) + _expect( + drafted_variants[0].get("evidence", {}).get("score", {}).get("subject_identity") == "pass", + "Atlas refine sidecar update draft should keep visual score evidence", + ) + rejected_draft = manifest_module.build_sidecar_update_draft(rejected_report) + _expect( + rejected_draft.get("ready_candidate_count") == 0, + "Atlas refine sidecar update draft should not include rejected candidates", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as report_handle: + json.dump(promotion_report, report_handle) + cli_report_path = Path(report_handle.name) + try: + sidecar_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-sidecar-update-draft", + "--promotion-report-json", + str(cli_report_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_report_path.unlink(missing_ok=True) + _expect(sidecar_cli_result.returncode == 0, f"Atlas refine sidecar-update CLI failed: {sidecar_cli_result.stderr}") + cli_sidecar_draft = json.loads(sidecar_cli_result.stdout) + _expect( + cli_sidecar_draft.get("ready_candidate_count") == 1, + "Atlas refine sidecar-update CLI should keep ready candidate count", + ) + draft_validation = manifest_module.validate_sidecar_update_draft(sidecar_draft) + _expect( + draft_validation.get("schema") == "sxcp_atlas_refine_sidecar_update_validation_v1", + "Atlas refine sidecar update validation lost schema", + ) + _expect(draft_validation.get("valid") is True, f"Atlas refine sidecar update draft should validate: {draft_validation}") + _expect(draft_validation.get("error_count") == 0, "Atlas refine sidecar update validation should have no errors") + _expect(draft_validation.get("validated_variant_count") == 1, "Atlas refine sidecar update validation should count variants") + invalid_draft = json.loads(json.dumps(sidecar_draft)) + invalid_draft["updates"][0]["prompt_variants"][0]["evidence"]["score"]["subject_identity"] = "fail" + invalid_draft["updates"][0]["prompt_variants"][0]["negative_prompt"] = "do not include this" + invalid_draft["updates"][0]["prompt_variants"][0]["prompt_source"]["prompt_variant_id"] = "wrong_axis" + invalid_validation = manifest_module.validate_sidecar_update_draft(invalid_draft) + _expect(invalid_validation.get("valid") is False, "Atlas refine sidecar update validation should reject failed evidence") + _expect( + any("subject_identity=fail" in error for error in invalid_validation.get("errors", [])), + f"Atlas refine sidecar update validation should report failed subject identity: {invalid_validation}", + ) + _expect( + any("negative_prompt" in error for error in invalid_validation.get("errors", [])), + f"Atlas refine sidecar update validation should reject negative prompt fields: {invalid_validation}", + ) + _expect( + any("prompt_source.prompt_variant_id" in error for error in invalid_validation.get("errors", [])), + f"Atlas refine sidecar update validation should reject mismatched prompt-source ids: {invalid_validation}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as draft_handle: + json.dump(sidecar_draft, draft_handle) + cli_draft_path = Path(draft_handle.name) + try: + validation_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--validate-sidecar-update-draft", + "--sidecar-update-draft-json", + str(cli_draft_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_draft_path.unlink(missing_ok=True) + _expect(validation_cli_result.returncode == 0, f"Atlas refine sidecar-update validation CLI failed: {validation_cli_result.stderr}") + cli_draft_validation = json.loads(validation_cli_result.stdout) + _expect( + cli_draft_validation.get("valid") is True, + "Atlas refine sidecar-update validation CLI should validate the draft", + ) + with tempfile.TemporaryDirectory() as duplicate_apply_tmpdir: + duplicate_apply_root = Path(duplicate_apply_tmpdir) + duplicate_apply_sidecar = duplicate_apply_root / "pov_footjob_frontal_sole_stroke_00001_.json" + duplicate_apply_sidecar.write_text( + json.dumps( + { + "notes": "ambiguous existing sidecar should not be silently rewritten", + "prompt_variants": [ + { + "id": "old_axis", + "text": "First old reviewed prompt variant.", + }, + { + "id": "old_axis", + "text": "Second old reviewed prompt variant.", + }, + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + try: + manifest_module.apply_sidecar_update_draft(sidecar_draft, duplicate_apply_root) + except ValueError as exc: + _expect( + "old_axis" in str(exc) and "duplicated" in str(exc), + f"Atlas refine sidecar apply duplicate existing id error should identify the duplicated id: {exc}", + ) + else: + raise AssertionError("Atlas refine sidecar apply should reject existing duplicate prompt_variant ids") + with tempfile.TemporaryDirectory() as apply_tmpdir: + apply_root = Path(apply_tmpdir) + sidecar_path = apply_root / "pov_footjob_frontal_sole_stroke_00001_.json" + sidecar_path.write_text( + json.dumps( + { + "notes": "preserve existing sidecar notes", + "seed_metadata": {"sampler_seed": 101}, + "prompt_variants": [ + { + "id": "old_axis", + "text": "Old reviewed prompt variant.", + "cue_axes": {"foot_position": "old"}, + } + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + apply_report = manifest_module.apply_sidecar_update_draft(sidecar_draft, apply_root) + _expect( + apply_report.get("schema") == "sxcp_atlas_refine_sidecar_apply_report_v1", + "Atlas refine sidecar apply report lost schema", + ) + _expect(apply_report.get("applied") is True, "Atlas refine sidecar apply should mark applied") + _expect(apply_report.get("updated_file_count") == 1, "Atlas refine sidecar apply should update one sidecar") + applied_sidecar = json.loads(sidecar_path.read_text(encoding="utf-8")) + _expect( + applied_sidecar.get("notes") == "preserve existing sidecar notes", + "Atlas refine sidecar apply should preserve unrelated sidecar metadata", + ) + applied_variants = applied_sidecar.get("prompt_variants") or [] + _expect( + [variant.get("id") for variant in applied_variants] == ["old_axis", "soles_more_forward"], + f"Atlas refine sidecar apply should append the ready variant without dropping existing variants: {applied_variants}", + ) + _expect( + applied_variants[1].get("text") == sheet_probes[1].get("text"), + "Atlas refine sidecar apply should write exact tested prompt text", + ) + _expect( + applied_variants[1].get("evidence", {}).get("score", {}).get("subject_identity") == "pass", + "Atlas refine sidecar apply should preserve evidence scores", + ) + _expect( + applied_variants[1].get("prompt_source", {}).get("append_cues") == [ + "the woman's soles press farther forward along the same contact line" + ], + "Atlas refine sidecar apply should preserve append-cue provenance", + ) + second_apply_report = manifest_module.apply_sidecar_update_draft(sidecar_draft, apply_root) + _expect( + second_apply_report.get("updated_file_count") == 1, + "Atlas refine sidecar apply should be idempotent and still report the touched sidecar", + ) + applied_again = json.loads(sidecar_path.read_text(encoding="utf-8")) + _expect( + [variant.get("id") for variant in applied_again.get("prompt_variants", [])].count("soles_more_forward") == 1, + "Atlas refine sidecar apply should upsert prompt variants by id instead of duplicating them", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as apply_draft_handle: + json.dump(sidecar_draft, apply_draft_handle) + cli_apply_draft_path = Path(apply_draft_handle.name) + try: + apply_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--apply-sidecar-update-draft", + "--sidecar-update-draft-json", + str(cli_apply_draft_path), + "--folder", + str(apply_root), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_apply_draft_path.unlink(missing_ok=True) + _expect(apply_cli_result.returncode == 0, f"Atlas refine sidecar apply CLI failed: {apply_cli_result.stderr}") + cli_apply_report = json.loads(apply_cli_result.stdout) + _expect( + cli_apply_report.get("applied") is True, + "Atlas refine sidecar apply CLI should report applied", + ) + # Add the paired prompt/image artifacts after apply so the manifest scanner + # can roundtrip the sidecar update through the normal atlas-refine path. + (apply_root / "pov_footjob_frontal_sole_stroke_00001_.txt").write_text( + footjob.get("prompt_text", ""), + encoding="utf-8", + ) + (apply_root / "pov_footjob_frontal_sole_stroke_00001_.png").write_bytes(b"fake-png") + applied_manifest = manifest_module.build_manifest(apply_root, subject_id="same_woman_001") + applied_entry = (applied_manifest.get("entries") or [{}])[0] + applied_prompt_variants = { + variant.get("id"): variant for variant in applied_entry.get("prompt_variants", []) + } + _expect( + applied_prompt_variants.get("soles_more_forward", {}).get("text") == sheet_probes[1].get("text"), + "Atlas refine applied sidecar should rescan with the exact tested prompt text", + ) + _expect( + applied_prompt_variants.get("soles_more_forward", {}).get("evidence", {}).get("image_path") + == "/tmp/pov_footjob_soles_more_forward.png", + "Atlas refine applied sidecar should rescan with evidence image provenance", + ) + _expect( + applied_prompt_variants.get("soles_more_forward", {}).get("prompt_source", {}).get("kind") == "append_cues", + "Atlas refine applied sidecar should rescan with append-cue provenance", + ) + _expect( + applied_prompt_variants.get("soles_more_forward", {}).get("reference_images") + == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine applied sidecar should rescan with reference-image provenance", + ) + catalog_cue_draft = manifest_module.build_catalog_cue_draft( + applied_manifest, + variant_key="pov_footjob_frontal_sole_stroke", + ) + _expect( + catalog_cue_draft.get("schema") == "sxcp_atlas_refine_catalog_cue_draft_v1", + "Atlas refine catalog cue draft lost schema", + ) + _expect( + catalog_cue_draft.get("ready_cue_count") == 1, + f"Atlas refine catalog cue draft should include one seedable append-cue candidate: {catalog_cue_draft}", + ) + catalog_cue_candidates = catalog_cue_draft.get("candidates") or [] + _expect( + catalog_cue_candidates[0].get("prompt_variant_cues") == [ + "the woman's soles press farther forward along the same contact line" + ], + "Atlas refine catalog cue draft should carry exact tested append cues", + ) + _expect( + catalog_cue_candidates[0].get("evidence", {}).get("score", {}).get("subject_identity") == "pass", + "Atlas refine catalog cue draft should require seedable visual evidence", + ) + _expect( + catalog_cue_candidates[0].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine catalog cue draft should carry nearest atlas reference-image provenance", + ) + catalog_cue_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(apply_root), + "--subject-id", + "same_woman_001", + "--print-catalog-cue-draft", + "--variant-key", + "pov_footjob_frontal_sole_stroke", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect(catalog_cue_cli_result.returncode == 0, f"Atlas refine catalog cue draft CLI failed: {catalog_cue_cli_result.stderr}") + cli_catalog_cue_draft = json.loads(catalog_cue_cli_result.stdout) + _expect( + cli_catalog_cue_draft.get("ready_cue_count") == 1, + "Atlas refine catalog cue draft CLI should keep ready cue count", + ) + applied_coverage = manifest_module.build_coverage_report(applied_manifest) + applied_footjob_coverage = (applied_coverage.get("entries") or [{}])[0] + _expect( + applied_footjob_coverage.get("state") == "ready_for_catalog_review", + f"Atlas refine coverage should mark seedable append-cue sidecars ready for catalog review: {applied_footjob_coverage}", + ) + _expect( + applied_footjob_coverage.get("seedable_variant_count") == 1, + "Atlas refine coverage should count seedable variants", + ) + _expect( + applied_footjob_coverage.get("catalog_cue_candidate_count") == 1, + "Atlas refine coverage should count seedable append-cue catalog candidates", + ) + coverage_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(apply_root), + "--subject-id", + "same_woman_001", + "--print-coverage-report", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect(coverage_cli_result.returncode == 0, f"Atlas refine coverage report CLI failed: {coverage_cli_result.stderr}") + cli_coverage = json.loads(coverage_cli_result.stdout) + _expect( + cli_coverage.get("ready_for_catalog_review_count") == 1, + "Atlas refine coverage report CLI should count catalog-review-ready entries", + ) + applied_batch = manifest_module.build_prompt_batch(applied_manifest, "pov_footjob_frontal_sole_stroke") + applied_batch_probes = applied_batch.get("probes") or [] + applied_variant_probe = next( + ( + probe + for probe in applied_batch_probes + if probe.get("id") == "pov_footjob_frontal_sole_stroke_00001__soles_more_forward" + ), + {}, + ) + _expect( + applied_variant_probe.get("text") == sheet_probes[1].get("text"), + "Atlas refine applied sidecar should regenerate the exact tested prompt in the next batch", + ) + _expect( + applied_variant_probe.get("evidence", {}).get("score", {}).get("subject_identity") == "pass", + "Atlas refine applied sidecar should carry evidence into regenerated batch probes", + ) + seed_selection = manifest_module.select_seeded_prompt_variant( + applied_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + seed_slot="atlas_cue_seed", + ) + _expect( + seed_selection.get("schema") == "sxcp_atlas_refine_seed_selection_v1", + "Atlas refine seed selection lost schema", + ) + _expect(seed_selection.get("selection_seed") == 202, "Atlas refine seed selection should keep the cue seed") + _expect(seed_selection.get("seed_slot") == "atlas_cue_seed", "Atlas refine seed selection should keep seed slot") + _expect(seed_selection.get("eligible_candidate_count") == 1, "Atlas refine seed selection should only use promoted candidates") + _expect( + seed_selection.get("selected", {}).get("prompt_variant_id") == "soles_more_forward", + "Atlas refine seed selection should select the promoted prompt variant", + ) + _expect( + seed_selection.get("selected", {}).get("text") == sheet_probes[1].get("text"), + "Atlas refine seed selection should preserve exact tested prompt text", + ) + _expect( + seed_selection.get("selected", {}).get("evidence", {}).get("score", {}).get("subject_identity") == "pass", + "Atlas refine seed selection should carry evidence score", + ) + _expect( + seed_selection.get("ineligible", [{}])[0].get("prompt_variant_id") == "old_axis", + "Atlas refine seed selection should report unproven variants as ineligible", + ) + _expect( + "missing_seedable_evidence" in seed_selection.get("ineligible", [{}])[0].get("reason", ""), + "Atlas refine seed selection should explain why unproven variants are ineligible", + ) + try: + manifest_module.select_seeded_prompt_variant( + applied_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + seed_slot="sampler_seed", + ) + except ValueError as exc: + _expect( + "seed_slot" in str(exc) and "sampler_seed" in str(exc), + f"Atlas refine seed selection should reject sampler_seed as a cue slot: {exc}", + ) + else: + raise AssertionError("Atlas refine seed selection should not allow cue selection through sampler_seed") + seed_selection_again = manifest_module.select_seeded_prompt_variant( + applied_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + seed_slot="atlas_cue_seed", + ) + _expect( + seed_selection_again.get("selected", {}).get("prompt_variant_id") + == seed_selection.get("selected", {}).get("prompt_variant_id"), + "Atlas refine seed selection should be deterministic for the same cue seed", + ) + order_manifest = json.loads(json.dumps(applied_manifest)) + order_entry = (order_manifest.get("entries") or [{}])[0] + soles_variant = json.loads(json.dumps(applied_prompt_variants.get("soles_more_forward") or {})) + ankle_variant = json.loads(json.dumps(soles_variant)) + ankle_variant["id"] = "ankle_angle_shift" + ankle_variant["text"] = ( + footjob.get("prompt_text", "") + + " the woman's ankle angle turns inward while the same contact line stays centered" + ) + ankle_variant["cue_axes"]["foot_position"] = "ankle_angle_shift" + ankle_variant["prompt_source"] = { + "kind": "append_cues", + "prompt_variant_id": "ankle_angle_shift", + "append_cues": [ + "the woman's ankle angle turns inward while the same contact line stays centered" + ], + "tested_text_sha256": manifest_module._sha256_text(ankle_variant["text"]), + } + ankle_variant["evidence"]["image_path"] = "/tmp/pov_footjob_ankle_angle_shift.png" + order_entry["prompt_variants"] = [soles_variant, ankle_variant] + reversed_order_manifest = json.loads(json.dumps(order_manifest)) + (reversed_order_manifest.get("entries") or [{}])[0]["prompt_variants"] = [ + ankle_variant, + soles_variant, + ] + ordered_selection = manifest_module.select_seeded_prompt_variant( + order_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=1, + seed_slot="atlas_cue_seed", + ) + reversed_order_selection = manifest_module.select_seeded_prompt_variant( + reversed_order_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=1, + seed_slot="atlas_cue_seed", + ) + _expect( + ordered_selection.get("selected", {}).get("prompt_variant_id") + == reversed_order_selection.get("selected", {}).get("prompt_variant_id") + == "soles_more_forward", + f"Atlas refine seed selection should be independent of sidecar prompt-variant order: {ordered_selection} vs {reversed_order_selection}", + ) + selection_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(apply_root), + "--subject-id", + "same_woman_001", + "--print-seed-selection", + "--variant-key", + "pov_footjob_frontal_sole_stroke", + "--selection-seed", + "202", + "--seed-slot", + "atlas_cue_seed", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect(selection_cli_result.returncode == 0, f"Atlas refine seed-selection CLI failed: {selection_cli_result.stderr}") + cli_seed_selection = json.loads(selection_cli_result.stdout) + _expect( + cli_seed_selection.get("selected", {}).get("prompt_variant_id") == "soles_more_forward", + "Atlas refine seed-selection CLI should select the promoted prompt variant", + ) + selected_batch = manifest_module.build_seed_selected_prompt_batch( + applied_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + sampler_seed=101, + seed_slot="atlas_cue_seed", + ) + _expect( + selected_batch.get("schema") == "sxcp_atlas_refine_prompt_batch_v1", + "Atlas refine seed-selected batch should use prompt batch schema", + ) + _expect(selected_batch.get("seed") == 101, "Atlas refine seed-selected batch should keep sampler seed") + _expect( + selected_batch.get("selection", {}).get("selected", {}).get("prompt_variant_id") == "soles_more_forward", + "Atlas refine seed-selected batch should include the seed selection report", + ) + selected_batch_probes = selected_batch.get("probes") or [] + _expect( + [probe.get("id") for probe in selected_batch_probes] + == [ + "pov_footjob_frontal_sole_stroke_00001__baseline", + "pov_footjob_frontal_sole_stroke_00001__soles_more_forward", + ], + f"Atlas refine seed-selected batch should contain baseline and selected candidate only: {selected_batch_probes}", + ) + _expect( + selected_batch_probes[1].get("text") == sheet_probes[1].get("text"), + "Atlas refine seed-selected batch should use exact selected prompt text", + ) + _expect( + selected_batch_probes[1].get("seed_metadata", {}).get("atlas_cue_seed") == 202, + "Atlas refine seed-selected batch should record selection seed in the requested seed slot", + ) + _expect( + selected_batch_probes[1].get("evidence", {}).get("image_path") == "/tmp/pov_footjob_soles_more_forward.png", + "Atlas refine seed-selected batch should preserve selected evidence", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as selected_batch_handle: + json.dump(selected_batch, selected_batch_handle) + selected_batch_path = Path(selected_batch_handle.name) + try: + loaded_selected_batch = sxcp_prompt_batch.load_batch(selected_batch_path) + finally: + selected_batch_path.unlink(missing_ok=True) + _expect( + len(loaded_selected_batch.get("probes") or []) == 2, + "Atlas refine seed-selected batch should load through sxcp prompt batch helper", + ) + seed_matrix = manifest_module.build_seed_matrix( + applied_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seeds=[202, 203], + sampler_seeds=[101, 102], + seed_slot="atlas_cue_seed", + ) + _expect( + seed_matrix.get("schema") == "sxcp_atlas_refine_seed_matrix_v1", + "Atlas refine seed matrix lost schema", + ) + _expect( + seed_matrix.get("job_count") == 4, + f"Atlas refine seed matrix should include every sampler/cue seed pair: {seed_matrix}", + ) + matrix_jobs = seed_matrix.get("jobs") or [] + _expect( + [(job.get("sampler_seed"), job.get("selection_seed")) for job in matrix_jobs] + == [(101, 202), (101, 203), (102, 202), (102, 203)], + f"Atlas refine seed matrix should keep deterministic sampler-major job order: {matrix_jobs}", + ) + _expect( + matrix_jobs[0].get("batch", {}).get("seed") == 101, + "Atlas refine seed matrix should embed sampler-seeded batches", + ) + sampler_102_job = next( + ( + job + for job in matrix_jobs + if job.get("sampler_seed") == 102 and job.get("selection_seed") == 202 + ), + {}, + ) + sampler_102_probes = sampler_102_job.get("batch", {}).get("probes") or [] + _expect( + sampler_102_job.get("batch", {}).get("seed") == 102 + and sampler_102_job.get("candidate_probe", {}).get("seed_metadata", {}).get("sampler_seed") == 102 + and sampler_102_probes + and all(probe.get("seed_metadata", {}).get("sampler_seed") == 102 for probe in sampler_102_probes), + f"Atlas refine seed matrix should propagate the actual per-job sampler seed into probe metadata: {sampler_102_job}", + ) + _expect( + matrix_jobs[0].get("batch", {}).get("selection", {}).get("selection_seed") == 202, + "Atlas refine seed matrix should embed cue-seed selection reports", + ) + _expect( + matrix_jobs[0].get("selected", {}).get("prompt_variant_id") == "soles_more_forward", + "Atlas refine seed matrix should preserve selected prompt variant ids", + ) + _expect( + matrix_jobs[0].get("candidate_probe", {}).get("text") == sheet_probes[1].get("text"), + "Atlas refine seed matrix should preserve exact selected candidate prompt text", + ) + _expect( + matrix_jobs[0].get("candidate_probe", {}).get("seed_metadata", {}).get("atlas_cue_seed") == 202, + "Atlas refine seed matrix should record cue seed on candidate probes", + ) + _expect( + matrix_jobs[0].get("candidate_probe", {}).get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine seed matrix should preserve selected candidate reference-image provenance", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as matrix_batch_handle: + json.dump(matrix_jobs[0].get("batch"), matrix_batch_handle) + matrix_batch_path = Path(matrix_batch_handle.name) + try: + loaded_matrix_batch = sxcp_prompt_batch.load_batch(matrix_batch_path) + finally: + matrix_batch_path.unlink(missing_ok=True) + _expect( + loaded_matrix_batch.get("seed") == 101 and len(loaded_matrix_batch.get("probes") or []) == 2, + "Atlas refine seed matrix embedded batches should load through sxcp prompt batch helper", + ) + _expect( + (loaded_matrix_batch.get("probes") or [])[1].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "SxCP prompt batch loader should preserve matrix candidate reference-image provenance", + ) + for duplicate_field, duplicate_kwargs in ( + ("sampler_seeds", {"selection_seeds": [202, 203], "sampler_seeds": [101, 101]}), + ("selection_seeds", {"selection_seeds": [202, 202], "sampler_seeds": [101, 102]}), + ): + try: + manifest_module.build_seed_matrix( + applied_manifest, + "pov_footjob_frontal_sole_stroke", + seed_slot="atlas_cue_seed", + **duplicate_kwargs, + ) + except ValueError as exc: + _expect( + duplicate_field in str(exc) and "duplicate" in str(exc), + f"Atlas refine seed matrix duplicate {duplicate_field} error should be explicit: {exc}", + ) + else: + raise AssertionError(f"Atlas refine seed matrix should reject duplicate {duplicate_field}") + seed_matrix_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(apply_root), + "--subject-id", + "same_woman_001", + "--print-seed-matrix", + "--variant-key", + "pov_footjob_frontal_sole_stroke", + "--selection-seeds", + "202,203", + "--sampler-seeds", + "101,102", + "--seed-slot", + "atlas_cue_seed", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect(seed_matrix_cli_result.returncode == 0, f"Atlas refine seed matrix CLI failed: {seed_matrix_cli_result.stderr}") + cli_seed_matrix = json.loads(seed_matrix_cli_result.stdout) + _expect( + cli_seed_matrix.get("job_count") == 4, + "Atlas refine seed matrix CLI should preserve matrix job count", + ) + matrix_results = { + "schema": "sxcp_atlas_refine_seed_matrix_results_v1", + "jobs": [ + { + "id": job.get("id"), + "results": { + "seed": job.get("sampler_seed"), + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": job.get("batch", {}).get("probes", [{}])[0].get("id"), + "prompt_order": "subject_first", + "turn": 300 + index * 2, + "image_path": f"/tmp/atlas_matrix_{index}_baseline.png", + "returned_seed": job.get("sampler_seed"), + }, + { + "id": job.get("batch", {}).get("probes", [{}, {}])[1].get("id"), + "prompt_order": "subject_first", + "turn": 301 + index * 2, + "image_path": f"/tmp/atlas_matrix_{index}_candidate.png", + "returned_seed": job.get("sampler_seed"), + }, + ], + }, + } + for index, job in enumerate(matrix_jobs) + ], + } + matrix_result_sheet = manifest_module.build_seed_matrix_result_sheet( + seed_matrix, + matrix_results, + notes="matrix scoring pending", + ) + _expect( + matrix_result_sheet.get("schema") == "sxcp_atlas_refine_seed_matrix_result_sheet_v1", + "Atlas refine seed matrix result sheet lost schema", + ) + _expect( + matrix_result_sheet.get("job_count") == 4, + "Atlas refine seed matrix result sheet should preserve every matrix job", + ) + matrix_sheet_jobs = matrix_result_sheet.get("jobs") or [] + _expect( + matrix_sheet_jobs[0].get("sampler_seed") == 101 + and matrix_sheet_jobs[0].get("selection_seed") == 202, + "Atlas refine seed matrix result sheet should keep sampler and cue seeds", + ) + _expect( + matrix_sheet_jobs[0].get("selected", {}).get("prompt_variant_id") == "soles_more_forward", + "Atlas refine seed matrix result sheet should keep selected prompt variant id", + ) + _expect( + matrix_sheet_jobs[0].get("result_sheet", {}).get("selection", {}).get("selection_seed") == 202, + "Atlas refine seed matrix result sheet should keep per-job selection reports", + ) + matrix_job_probes = matrix_sheet_jobs[0].get("result_sheet", {}).get("probes") or [] + _expect( + matrix_job_probes[1].get("text") == sheet_probes[1].get("text"), + "Atlas refine seed matrix result sheet should preserve exact candidate prompt text", + ) + _expect( + matrix_job_probes[1].get("score", {}).get("pose_ownership") is None, + "Atlas refine seed matrix result sheet should leave score slots empty for visual scoring", + ) + _expect( + matrix_job_probes[1].get("seed_metadata", {}).get("atlas_cue_seed") == 202, + "Atlas refine seed matrix result sheet should keep cue seed metadata on candidate probes", + ) + _expect( + matrix_job_probes[1].get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + "Atlas refine seed matrix result sheet should keep candidate reference-image provenance", + ) + duplicate_job_matrix = json.loads(json.dumps(seed_matrix)) + duplicate_job_matrix["jobs"][1]["id"] = duplicate_job_matrix["jobs"][0]["id"] + try: + manifest_module.build_seed_matrix_result_sheet( + duplicate_job_matrix, + matrix_results, + notes="duplicate matrix job id should fail", + ) + except ValueError as exc: + _expect( + "seed matrix jobs" in str(exc) and "duplicated" in str(exc), + f"Atlas refine seed matrix result sheet duplicate job-id error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix result sheet should reject duplicate matrix job ids") + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as matrix_handle: + json.dump(seed_matrix, matrix_handle) + cli_matrix_path = Path(matrix_handle.name) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as matrix_results_handle: + json.dump(matrix_results, matrix_results_handle) + cli_matrix_results_path = Path(matrix_results_handle.name) + try: + matrix_result_sheet_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-seed-matrix-result-sheet", + "--seed-matrix-json", + str(cli_matrix_path), + "--seed-matrix-results-json", + str(cli_matrix_results_path), + "--notes", + "matrix scoring pending", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_matrix_path.unlink(missing_ok=True) + cli_matrix_results_path.unlink(missing_ok=True) + _expect( + matrix_result_sheet_cli_result.returncode == 0, + f"Atlas refine seed matrix result-sheet CLI failed: {matrix_result_sheet_cli_result.stderr}", + ) + cli_matrix_result_sheet = json.loads(matrix_result_sheet_cli_result.stdout) + _expect( + cli_matrix_result_sheet.get("job_count") == 4, + "Atlas refine seed matrix result-sheet CLI should preserve job count", + ) + scored_matrix_result_sheet = json.loads(json.dumps(matrix_result_sheet)) + for index, job in enumerate(scored_matrix_result_sheet.get("jobs") or []): + probes = job.get("result_sheet", {}).get("probes") or [] + probes[0]["score"].update( + { + "atlas_pose_match": "baseline", + "contact_match": "baseline", + "pose_ownership": "pass", + "workspace_continuity": "pass", + "clothing_visibility": "pass", + "subject_identity": "pass", + "expression_eye_control": "pass", + "anatomy_proportion": "pass", + "prompt_noise": "pass", + } + ) + probes[1]["score"].update( + { + "atlas_pose_match": "pass", + "contact_match": "pass", + "pose_ownership": "pass", + "workspace_continuity": "pass", + "clothing_visibility": "pass", + "subject_identity": "fail" if index == 3 else "pass", + "expression_eye_control": "pass", + "anatomy_proportion": "pass", + "prompt_noise": "pass", + } + ) + probes[1]["analysis_notes"] = f"matrix visual score {index}" + matrix_promotion_report = manifest_module.build_seed_matrix_promotion_report(scored_matrix_result_sheet) + _expect( + matrix_promotion_report.get("schema") == "sxcp_atlas_refine_seed_matrix_promotion_report_v1", + "Atlas refine seed matrix promotion report lost schema", + ) + _expect( + matrix_promotion_report.get("job_count") == 4, + "Atlas refine seed matrix promotion report should preserve job count", + ) + _expect( + matrix_promotion_report.get("promotion_ready_job_count") == 3, + f"Atlas refine seed matrix promotion report should count passing matrix jobs: {matrix_promotion_report}", + ) + _expect( + matrix_promotion_report.get("blocked_job_count") == 1, + f"Atlas refine seed matrix promotion report should count blocked matrix jobs: {matrix_promotion_report}", + ) + matrix_groups = matrix_promotion_report.get("groups") or [] + _expect( + len(matrix_groups) == 2, + f"Atlas refine seed matrix promotion report should group by cue seed and selected variant: {matrix_groups}", + ) + first_group = matrix_groups[0] + _expect( + first_group.get("selection_seed") == 202 + and first_group.get("prompt_variant_id") == "soles_more_forward", + f"Atlas refine seed matrix promotion report should preserve cue seed and variant group identity: {first_group}", + ) + _expect( + first_group.get("sampler_seed_count") == 2 + and first_group.get("promotion_ready_count") == 2 + and first_group.get("stable") is True, + f"Atlas refine seed matrix promotion report should mark all-passing cue groups stable: {first_group}", + ) + first_matrix_job = (matrix_promotion_report.get("jobs") or [{}])[0] + _expect( + first_matrix_job.get("candidate", {}).get("reference_images") == ["blowjob_top_view/22_blowjob_top_view.png"], + f"Atlas refine seed matrix promotion report should preserve candidate reference-image provenance: {first_matrix_job}", + ) + second_group = matrix_groups[1] + _expect( + second_group.get("selection_seed") == 203 + and second_group.get("promotion_ready_count") == 1 + and second_group.get("blocked_count") == 1 + and second_group.get("stable") is False, + f"Atlas refine seed matrix promotion report should mark failed cue groups unstable: {second_group}", + ) + _expect( + any("subject_identity=fail" in blocker for blocker in second_group.get("blockers", [])), + f"Atlas refine seed matrix promotion report should aggregate blockers: {second_group}", + ) + duplicate_promotion_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + duplicate_promotion_result_sheet["jobs"][1]["id"] = duplicate_promotion_result_sheet["jobs"][0]["id"] + try: + manifest_module.build_seed_matrix_promotion_report(duplicate_promotion_result_sheet) + except ValueError as exc: + _expect( + "seed matrix result sheet jobs" in str(exc) and "duplicated" in str(exc), + f"Atlas refine seed matrix promotion duplicate job-id error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject duplicate result-sheet job ids") + mismatched_selected_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + mismatched_selected_result_sheet["jobs"][0]["selected"]["prompt_variant_id"] = "wrong_prompt_variant" + try: + manifest_module.build_seed_matrix_promotion_report(mismatched_selected_result_sheet) + except ValueError as exc: + _expect( + "selected.prompt_variant_id" in str(exc) and "wrong_prompt_variant" in str(exc), + f"Atlas refine seed matrix promotion selected/candidate mismatch error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject selected/candidate prompt variant mismatches") + mismatched_seed_slot_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + mismatched_seed_slot_result_sheet["jobs"][0]["seed_slot"] = "workspace_seed" + try: + manifest_module.build_seed_matrix_promotion_report(mismatched_seed_slot_result_sheet) + except ValueError as exc: + _expect( + "seed_slot" in str(exc) and "workspace_seed" in str(exc) and "atlas_cue_seed" in str(exc), + f"Atlas refine seed matrix promotion seed-slot mismatch error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject job seed-slot drift") + duplicate_declared_sampler_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + duplicate_declared_sampler_result_sheet["sampler_seeds"] = [101, 101] + try: + manifest_module.build_seed_matrix_promotion_report(duplicate_declared_sampler_result_sheet) + except ValueError as exc: + _expect( + "sampler_seeds" in str(exc) and "duplicate" in str(exc), + f"Atlas refine seed matrix promotion duplicate declared sampler-seed error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject duplicate declared sampler seeds") + undeclared_sampler_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + undeclared_sampler_result_sheet["jobs"][0]["sampler_seed"] = 999 + try: + manifest_module.build_seed_matrix_promotion_report(undeclared_sampler_result_sheet) + except ValueError as exc: + _expect( + "sampler_seed" in str(exc) and "999" in str(exc) and "sampler_seeds" in str(exc), + f"Atlas refine seed matrix promotion undeclared sampler-seed error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject jobs outside declared sampler seeds") + duplicate_sampler_job_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + duplicate_sampler_job = json.loads(json.dumps(duplicate_sampler_job_result_sheet["jobs"][0])) + duplicate_sampler_job["id"] = f"{duplicate_sampler_job['id']}__duplicate_sampler" + duplicate_sampler_job_result_sheet["jobs"].append(duplicate_sampler_job) + try: + manifest_module.build_seed_matrix_promotion_report(duplicate_sampler_job_result_sheet) + except ValueError as exc: + _expect( + "sampler_seed" in str(exc) and "duplicated" in str(exc), + f"Atlas refine seed matrix promotion duplicate sampler job error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject duplicate sampler jobs inside a cue group") + duplicate_declared_selection_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + duplicate_declared_selection_result_sheet["selection_seeds"] = [202, 202] + try: + manifest_module.build_seed_matrix_promotion_report(duplicate_declared_selection_result_sheet) + except ValueError as exc: + _expect( + "selection_seeds" in str(exc) and "duplicate" in str(exc), + f"Atlas refine seed matrix promotion duplicate declared cue-seed error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject duplicate declared cue seeds") + undeclared_selection_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + undeclared_selection_result_sheet["jobs"][0]["selection_seed"] = 999 + try: + manifest_module.build_seed_matrix_promotion_report(undeclared_selection_result_sheet) + except ValueError as exc: + _expect( + "selection_seed" in str(exc) and "999" in str(exc) and "selection_seeds" in str(exc), + f"Atlas refine seed matrix promotion undeclared cue-seed error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject jobs outside declared cue seeds") + mismatched_variant_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + mismatched_variant_job = mismatched_variant_result_sheet["jobs"][2] + mismatched_variant_job["result_sheet"]["variant_key"] = "pov_other_pose_candidate" + mismatched_variant_candidate = mismatched_variant_job["result_sheet"]["probes"][1] + mismatched_variant_candidate["variant_key"] = "pov_other_pose_candidate" + try: + manifest_module.build_seed_matrix_promotion_report(mismatched_variant_result_sheet) + except ValueError as exc: + _expect( + "variant_key" in str(exc) and "pov_other_pose_candidate" in str(exc), + f"Atlas refine seed matrix promotion variant-key mismatch error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject candidate variant-key drift inside a stable group") + mismatched_source_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + mismatched_source_job = mismatched_source_result_sheet["jobs"][2] + mismatched_source_job["result_sheet"]["source_entry_id"] = "pov_other_pose_00001" + mismatched_source_job["result_sheet"]["source_stem"] = "pov_other_pose_00001_" + mismatched_source_candidate = mismatched_source_job["result_sheet"]["probes"][1] + mismatched_source_candidate["source_entry_id"] = "pov_other_pose_00001" + mismatched_source_candidate["source_stem"] = "pov_other_pose_00001_" + try: + manifest_module.build_seed_matrix_promotion_report(mismatched_source_result_sheet) + except ValueError as exc: + _expect( + "source_stem" in str(exc) and "pov_other_pose_00001_" in str(exc), + f"Atlas refine seed matrix promotion source-stem mismatch error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject source-stem drift inside a stable group") + mismatched_prompt_text_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + mismatched_prompt_text_job = mismatched_prompt_text_result_sheet["jobs"][2] + mismatched_prompt_text_candidate = mismatched_prompt_text_job["result_sheet"]["probes"][1] + mismatched_prompt_text_candidate["text"] = ( + "A controlled same-subject footjob reference prompt with foreground soles. " + "the woman's heels stay farther back along the same contact line" + ) + try: + manifest_module.build_seed_matrix_promotion_report(mismatched_prompt_text_result_sheet) + except ValueError as exc: + _expect( + "prompt text" in str(exc) and "does not match" in str(exc), + f"Atlas refine seed matrix promotion prompt-text drift error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine seed matrix promotion should reject prompt-text drift inside a stable group") + incomplete_matrix_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + incomplete_matrix_result_sheet["jobs"] = [ + job + for job in incomplete_matrix_result_sheet.get("jobs", []) + if job.get("sampler_seed") == 101 + ] + incomplete_matrix_promotion_report = manifest_module.build_seed_matrix_promotion_report(incomplete_matrix_result_sheet) + incomplete_matrix_groups = incomplete_matrix_promotion_report.get("groups") or [] + _expect( + incomplete_matrix_promotion_report.get("stable_group_count") == 0, + f"Atlas refine seed matrix promotion should not mark incomplete sampler coverage stable: {incomplete_matrix_promotion_report}", + ) + _expect( + incomplete_matrix_groups + and all(102 in group.get("missing_sampler_seeds", []) for group in incomplete_matrix_groups) + and all("missing_sampler_coverage" in group.get("blockers", []) for group in incomplete_matrix_groups), + f"Atlas refine seed matrix promotion should expose missing sampler coverage per group: {incomplete_matrix_groups}", + ) + one_sampler_matrix_result_sheet = json.loads(json.dumps(scored_matrix_result_sheet)) + one_sampler_matrix_result_sheet["sampler_seeds"] = [101] + one_sampler_matrix_result_sheet["jobs"] = [ + job + for job in one_sampler_matrix_result_sheet.get("jobs", []) + if job.get("sampler_seed") == 101 + ] + one_sampler_matrix_promotion_report = manifest_module.build_seed_matrix_promotion_report(one_sampler_matrix_result_sheet) + one_sampler_matrix_groups = one_sampler_matrix_promotion_report.get("groups") or [] + _expect( + one_sampler_matrix_promotion_report.get("stable_group_count") == 0, + f"Atlas refine seed matrix promotion should not mark single-sampler matrices stable: {one_sampler_matrix_promotion_report}", + ) + _expect( + one_sampler_matrix_groups + and all("insufficient_sampler_coverage" in group.get("blockers", []) for group in one_sampler_matrix_groups), + f"Atlas refine seed matrix promotion should expose insufficient sampler coverage per group: {one_sampler_matrix_groups}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as scored_matrix_handle: + json.dump(scored_matrix_result_sheet, scored_matrix_handle) + cli_scored_matrix_path = Path(scored_matrix_handle.name) + try: + matrix_promotion_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-seed-matrix-promotion-report", + "--seed-matrix-result-sheet-json", + str(cli_scored_matrix_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_scored_matrix_path.unlink(missing_ok=True) + _expect( + matrix_promotion_cli_result.returncode == 0, + f"Atlas refine seed matrix promotion CLI failed: {matrix_promotion_cli_result.stderr}", + ) + cli_matrix_promotion_report = json.loads(matrix_promotion_cli_result.stdout) + _expect( + cli_matrix_promotion_report.get("promotion_ready_job_count") == 3, + "Atlas refine seed matrix promotion CLI should preserve ready job count", + ) + matrix_sidecar_draft = manifest_module.build_matrix_sidecar_update_draft(matrix_promotion_report) + _expect( + matrix_sidecar_draft.get("schema") == "sxcp_atlas_refine_matrix_sidecar_update_draft_v1", + "Atlas refine matrix sidecar draft lost schema", + ) + _expect( + matrix_sidecar_draft.get("ready_group_count") == 1 + and matrix_sidecar_draft.get("skipped_group_count") == 1, + f"Atlas refine matrix sidecar draft should include only stable cue groups: {matrix_sidecar_draft}", + ) + matrix_updates = matrix_sidecar_draft.get("updates") or [] + _expect( + len(matrix_updates) == 1 + and matrix_updates[0].get("sidecar_filename") == "pov_footjob_frontal_sole_stroke_00001_.json", + f"Atlas refine matrix sidecar draft should preserve same-stem sidecar filename: {matrix_updates}", + ) + matrix_variant = (matrix_updates[0].get("prompt_variants") or [{}])[0] + _expect( + matrix_variant.get("id") == "soles_more_forward", + "Atlas refine matrix sidecar draft should preserve prompt variant id", + ) + _expect( + matrix_variant.get("text") == sheet_probes[1].get("text"), + "Atlas refine matrix sidecar draft should keep exact selected prompt text", + ) + matrix_evidence = matrix_variant.get("matrix_evidence") or {} + _expect( + matrix_evidence.get("stable") is True + and matrix_evidence.get("selection_seed") == 202 + and matrix_evidence.get("sampler_seeds") == [101, 102], + f"Atlas refine matrix sidecar draft should preserve stable sampler/cue evidence: {matrix_evidence}", + ) + missing_job_promotion_report = json.loads(json.dumps(matrix_promotion_report)) + missing_job_promotion_report["groups"][0]["job_ids"].append("missing_matrix_job_id") + try: + manifest_module.build_matrix_sidecar_update_draft(missing_job_promotion_report) + except ValueError as exc: + _expect( + "job_ids" in str(exc) and "missing_matrix_job_id" in str(exc), + f"Atlas refine matrix sidecar draft missing job-id error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine matrix sidecar draft should reject stable groups with missing job ids") + duplicate_group_job_id_promotion_report = json.loads(json.dumps(matrix_promotion_report)) + duplicate_group_job_id = duplicate_group_job_id_promotion_report["groups"][0]["job_ids"][0] + duplicate_group_job_id_promotion_report["groups"][0]["job_ids"].append(duplicate_group_job_id) + duplicate_group_job_id_promotion_report["groups"][0]["job_count"] = 3 + duplicate_group_job_id_promotion_report["groups"][0]["promotion_ready_count"] = 3 + try: + manifest_module.build_matrix_sidecar_update_draft(duplicate_group_job_id_promotion_report) + except ValueError as exc: + _expect( + "job_ids" in str(exc) and "duplicated" in str(exc) and duplicate_group_job_id in str(exc), + f"Atlas refine matrix sidecar draft duplicate job-id error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine matrix sidecar draft should reject stable groups with duplicated job ids") + mismatched_group_job_promotion_report = json.loads(json.dumps(matrix_promotion_report)) + stable_group = mismatched_group_job_promotion_report["groups"][0] + mismatched_group_job = next( + job + for job in mismatched_group_job_promotion_report["jobs"] + if job.get("selection_seed") != stable_group.get("selection_seed") + and job.get("sampler_seed") == stable_group.get("sampler_seeds", [None])[0] + and job.get("decision") == "seedable_candidate" + ) + stable_group["job_ids"][0] = mismatched_group_job["id"] + try: + manifest_module.build_matrix_sidecar_update_draft(mismatched_group_job_promotion_report) + except ValueError as exc: + _expect( + "selection_seed" in str(exc) and str(mismatched_group_job["selection_seed"]) in str(exc), + f"Atlas refine matrix sidecar draft group identity drift error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine matrix sidecar draft should reject jobs outside the stable group identity") + mismatched_group_prompt_text_report = json.loads(json.dumps(matrix_promotion_report)) + mismatched_group_prompt_text_job = next( + job + for job in mismatched_group_prompt_text_report["jobs"] + if job.get("selection_seed") == 202 and job.get("sampler_seed") == 102 + ) + mismatched_group_prompt_text_job["candidate"]["text"] = ( + "A controlled same-subject footjob reference prompt with foreground soles. " + "the woman's heels stay farther back along the same contact line" + ) + try: + manifest_module.build_matrix_sidecar_update_draft(mismatched_group_prompt_text_report) + except ValueError as exc: + _expect( + "prompt text" in str(exc) and "expected" in str(exc), + f"Atlas refine matrix sidecar draft prompt-text drift error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine matrix sidecar draft should reject prompt-text drift inside stable group job ids") + incomplete_group_promotion_report = json.loads(json.dumps(matrix_promotion_report)) + incomplete_group_promotion_report["groups"][0]["job_ids"] = incomplete_group_promotion_report["groups"][0]["job_ids"][:1] + try: + manifest_module.build_matrix_sidecar_update_draft(incomplete_group_promotion_report) + except ValueError as exc: + _expect( + "sampler_seeds" in str(exc) and "job_ids" in str(exc), + f"Atlas refine matrix sidecar draft incomplete sampler coverage error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine matrix sidecar draft should reject stable groups whose job ids omit sampler coverage") + inflated_count_promotion_report = json.loads(json.dumps(matrix_promotion_report)) + inflated_count_promotion_report["groups"][0]["job_count"] = 99 + inflated_count_promotion_report["groups"][0]["promotion_ready_count"] = 99 + try: + manifest_module.build_matrix_sidecar_update_draft(inflated_count_promotion_report) + except ValueError as exc: + _expect( + "job_count" in str(exc) and "promotion_ready_count" in str(exc), + f"Atlas refine matrix sidecar draft count-drift error should be explicit: {exc}", + ) + else: + raise AssertionError("Atlas refine matrix sidecar draft should reject stable groups with inflated evidence counts") + _expect( + len(matrix_evidence.get("jobs") or []) == 2, + "Atlas refine matrix sidecar draft should keep every passing matrix job", + ) + _expect( + matrix_variant.get("evidence", {}).get("seed") == 101, + "Atlas refine matrix sidecar draft should keep representative single-image evidence for compatibility", + ) + skipped_matrix_groups = matrix_sidecar_draft.get("skipped") or [] + _expect( + skipped_matrix_groups + and skipped_matrix_groups[0].get("selection_seed") == 203 + and "subject_identity=fail" in skipped_matrix_groups[0].get("blockers", []), + f"Atlas refine matrix sidecar draft should skip unstable groups with blockers: {skipped_matrix_groups}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as matrix_promotion_handle: + json.dump(matrix_promotion_report, matrix_promotion_handle) + cli_matrix_promotion_path = Path(matrix_promotion_handle.name) + try: + matrix_sidecar_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--print-matrix-sidecar-update-draft", + "--seed-matrix-promotion-report-json", + str(cli_matrix_promotion_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_matrix_promotion_path.unlink(missing_ok=True) + _expect( + matrix_sidecar_cli_result.returncode == 0, + f"Atlas refine matrix sidecar draft CLI failed: {matrix_sidecar_cli_result.stderr}", + ) + cli_matrix_sidecar_draft = json.loads(matrix_sidecar_cli_result.stdout) + _expect( + cli_matrix_sidecar_draft.get("ready_group_count") == 1, + "Atlas refine matrix sidecar draft CLI should keep ready group count", + ) + matrix_validation = manifest_module.validate_matrix_sidecar_update_draft(matrix_sidecar_draft) + _expect( + matrix_validation.get("schema") == "sxcp_atlas_refine_matrix_sidecar_update_validation_v1", + "Atlas refine matrix sidecar validation lost schema", + ) + _expect( + matrix_validation.get("valid") is True, + f"Atlas refine matrix sidecar draft should validate: {matrix_validation}", + ) + _expect( + matrix_validation.get("validated_variant_count") == 1, + "Atlas refine matrix sidecar validation should count prompt variants", + ) + duplicate_matrix_job_draft = json.loads(json.dumps(matrix_sidecar_draft)) + duplicate_matrix_evidence = duplicate_matrix_job_draft["updates"][0]["prompt_variants"][0]["matrix_evidence"] + duplicate_matrix_evidence["jobs"].append(json.loads(json.dumps(duplicate_matrix_evidence["jobs"][0]))) + duplicate_matrix_evidence["job_count"] = len(duplicate_matrix_evidence["jobs"]) + duplicate_matrix_evidence["promotion_ready_count"] = len(duplicate_matrix_evidence["jobs"]) + duplicate_matrix_validation = manifest_module.validate_matrix_sidecar_update_draft(duplicate_matrix_job_draft) + _expect( + duplicate_matrix_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject duplicated matrix evidence jobs", + ) + _expect( + any("matrix_evidence.jobs" in error and "duplicated" in error for error in duplicate_matrix_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation duplicate matrix job error should be explicit: {duplicate_matrix_validation}", + ) + duplicate_declared_sampler_draft = json.loads(json.dumps(matrix_sidecar_draft)) + duplicate_declared_sampler_evidence = duplicate_declared_sampler_draft["updates"][0]["prompt_variants"][0]["matrix_evidence"] + duplicate_declared_sampler_evidence["sampler_seeds"] = [ + duplicate_declared_sampler_evidence["sampler_seeds"][0], + duplicate_declared_sampler_evidence["sampler_seeds"][0], + *duplicate_declared_sampler_evidence["sampler_seeds"][1:], + ] + duplicate_declared_sampler_validation = manifest_module.validate_matrix_sidecar_update_draft(duplicate_declared_sampler_draft) + _expect( + duplicate_declared_sampler_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject duplicated declared matrix sampler seeds", + ) + _expect( + any("matrix_evidence.sampler_seeds" in error and "duplicated" in error for error in duplicate_declared_sampler_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation duplicate declared sampler-seed error should be explicit: {duplicate_declared_sampler_validation}", + ) + single_sampler_matrix_draft = json.loads(json.dumps(matrix_sidecar_draft)) + single_sampler_matrix_evidence = single_sampler_matrix_draft["updates"][0]["prompt_variants"][0]["matrix_evidence"] + single_sampler_matrix_evidence["jobs"] = single_sampler_matrix_evidence["jobs"][:1] + single_sampler_matrix_evidence["sampler_seeds"] = [single_sampler_matrix_evidence["jobs"][0]["sampler_seed"]] + single_sampler_matrix_evidence["job_count"] = 1 + single_sampler_matrix_evidence["promotion_ready_count"] = 1 + single_sampler_matrix_validation = manifest_module.validate_matrix_sidecar_update_draft(single_sampler_matrix_draft) + _expect( + single_sampler_matrix_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject single-sampler stable matrix evidence", + ) + _expect( + any("matrix_evidence.sampler_seeds" in error and "at least 2" in error for error in single_sampler_matrix_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation single-sampler error should be explicit: {single_sampler_matrix_validation}", + ) + invalid_matrix_turn_draft = json.loads(json.dumps(matrix_sidecar_draft)) + invalid_matrix_turn_draft["updates"][0]["prompt_variants"][0]["matrix_evidence"]["jobs"][0]["turn"] = "not-an-int" + invalid_matrix_turn_validation = manifest_module.validate_matrix_sidecar_update_draft(invalid_matrix_turn_draft) + _expect( + invalid_matrix_turn_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject non-integer matrix job turns", + ) + _expect( + any("matrix_evidence.jobs[0].turn" in error and "integer" in error for error in invalid_matrix_turn_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation matrix job turn error should be explicit: {invalid_matrix_turn_validation}", + ) + invalid_representative_turn_draft = json.loads(json.dumps(matrix_sidecar_draft)) + invalid_representative_turn_draft["updates"][0]["prompt_variants"][0]["evidence"]["turn"] = "not-an-int" + invalid_representative_turn_validation = manifest_module.validate_matrix_sidecar_update_draft(invalid_representative_turn_draft) + _expect( + invalid_representative_turn_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject non-integer representative evidence turns", + ) + _expect( + any("evidence.turn" in error and "integer" in error for error in invalid_representative_turn_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation representative evidence turn error should be explicit: {invalid_representative_turn_validation}", + ) + mismatched_seed_metadata_draft = json.loads(json.dumps(matrix_sidecar_draft)) + mismatched_seed_metadata_variant = mismatched_seed_metadata_draft["updates"][0]["prompt_variants"][0] + mismatched_seed_metadata_variant["seed_metadata"]["atlas_cue_seed"] = 999 + mismatched_seed_metadata_validation = manifest_module.validate_matrix_sidecar_update_draft(mismatched_seed_metadata_draft) + _expect( + mismatched_seed_metadata_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject cue seed_metadata drift", + ) + _expect( + any("seed_metadata.atlas_cue_seed" in error and "999" in error and "202" in error for error in mismatched_seed_metadata_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation seed metadata drift error should be explicit: {mismatched_seed_metadata_validation}", + ) + mismatched_representative_evidence_draft = json.loads(json.dumps(matrix_sidecar_draft)) + mismatched_representative_variant = mismatched_representative_evidence_draft["updates"][0]["prompt_variants"][0] + mismatched_representative_variant["evidence"]["image_path"] = "/tmp/atlas_matrix_wrong_representative.png" + mismatched_representative_validation = manifest_module.validate_matrix_sidecar_update_draft( + mismatched_representative_evidence_draft + ) + _expect( + mismatched_representative_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject representative evidence drift", + ) + _expect( + any("evidence.image_path" in error and "matrix_evidence.jobs" in error for error in mismatched_representative_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation representative evidence drift error should be explicit: {mismatched_representative_validation}", + ) + invalid_matrix_draft = json.loads(json.dumps(matrix_sidecar_draft)) + invalid_matrix_variant = invalid_matrix_draft["updates"][0]["prompt_variants"][0] + invalid_matrix_variant["matrix_evidence"]["stable"] = False + invalid_matrix_variant["matrix_evidence"]["seed_slot"] = "sampler_seed" + invalid_matrix_variant["matrix_evidence"]["jobs"][0]["score"]["subject_identity"] = "fail" + invalid_matrix_variant["negative_prompt"] = "do not include this" + invalid_matrix_variant["prompt_source"]["prompt_variant_id"] = "wrong_axis" + invalid_matrix_validation = manifest_module.validate_matrix_sidecar_update_draft(invalid_matrix_draft) + _expect( + invalid_matrix_validation.get("valid") is False, + "Atlas refine matrix sidecar validation should reject unstable or failed matrix evidence", + ) + _expect( + any("matrix_evidence.stable" in error for error in invalid_matrix_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation should report unstable matrix evidence: {invalid_matrix_validation}", + ) + _expect( + any("matrix_evidence.seed_slot" in error and "sampler_seed" in error for error in invalid_matrix_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation should reject sampler_seed as a cue slot: {invalid_matrix_validation}", + ) + _expect( + any("matrix_evidence.jobs[0].score" in error for error in invalid_matrix_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation should report failed matrix job scores: {invalid_matrix_validation}", + ) + _expect( + any("negative_prompt" in error for error in invalid_matrix_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation should reject negative prompt fields: {invalid_matrix_validation}", + ) + _expect( + any("prompt_source.prompt_variant_id" in error for error in invalid_matrix_validation.get("errors", [])), + f"Atlas refine matrix sidecar validation should reject mismatched prompt-source ids: {invalid_matrix_validation}", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as matrix_draft_handle: + json.dump(matrix_sidecar_draft, matrix_draft_handle) + cli_matrix_draft_path = Path(matrix_draft_handle.name) + try: + matrix_validation_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--validate-matrix-sidecar-update-draft", + "--matrix-sidecar-update-draft-json", + str(cli_matrix_draft_path), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_matrix_draft_path.unlink(missing_ok=True) + _expect( + matrix_validation_cli_result.returncode == 0, + f"Atlas refine matrix sidecar validation CLI failed: {matrix_validation_cli_result.stderr}", + ) + cli_matrix_validation = json.loads(matrix_validation_cli_result.stdout) + _expect( + cli_matrix_validation.get("valid") is True, + "Atlas refine matrix sidecar validation CLI should validate the draft", + ) + with tempfile.TemporaryDirectory() as matrix_apply_tmpdir: + matrix_apply_root = Path(matrix_apply_tmpdir) + matrix_sidecar_path = matrix_apply_root / "pov_footjob_frontal_sole_stroke_00001_.json" + matrix_sidecar_path.write_text( + json.dumps( + { + "notes": "preserve existing matrix sidecar notes", + "prompt_variants": [ + { + "id": "old_axis", + "text": "Old reviewed prompt variant.", + "cue_axes": {"foot_position": "old"}, + } + ], + }, + ensure_ascii=True, + ), + encoding="utf-8", + ) + matrix_apply_report = manifest_module.apply_matrix_sidecar_update_draft( + matrix_sidecar_draft, + matrix_apply_root, + ) + _expect( + matrix_apply_report.get("schema") == "sxcp_atlas_refine_matrix_sidecar_apply_report_v1", + "Atlas refine matrix sidecar apply report lost schema", + ) + _expect( + matrix_apply_report.get("applied") is True, + "Atlas refine matrix sidecar apply should mark applied", + ) + _expect( + matrix_apply_report.get("updated_file_count") == 1, + "Atlas refine matrix sidecar apply should update one sidecar", + ) + matrix_applied_sidecar = json.loads(matrix_sidecar_path.read_text(encoding="utf-8")) + _expect( + matrix_applied_sidecar.get("notes") == "preserve existing matrix sidecar notes", + "Atlas refine matrix sidecar apply should preserve unrelated sidecar fields", + ) + matrix_applied_variants = matrix_applied_sidecar.get("prompt_variants") or [] + _expect( + [variant.get("id") for variant in matrix_applied_variants] == ["old_axis", "soles_more_forward"], + f"Atlas refine matrix sidecar apply should append stable variants: {matrix_applied_variants}", + ) + applied_matrix_evidence = matrix_applied_variants[1].get("matrix_evidence") or {} + _expect( + applied_matrix_evidence.get("stable") is True + and applied_matrix_evidence.get("selection_seed") == 202 + and len(applied_matrix_evidence.get("jobs") or []) == 2, + f"Atlas refine matrix sidecar apply should preserve full matrix evidence: {applied_matrix_evidence}", + ) + matrix_second_apply_report = manifest_module.apply_matrix_sidecar_update_draft( + matrix_sidecar_draft, + matrix_apply_root, + ) + _expect( + matrix_second_apply_report.get("updated_file_count") == 1, + "Atlas refine matrix sidecar apply should be idempotent and still report the touched sidecar", + ) + matrix_applied_again = json.loads(matrix_sidecar_path.read_text(encoding="utf-8")) + _expect( + [variant.get("id") for variant in matrix_applied_again.get("prompt_variants", [])].count("soles_more_forward") == 1, + "Atlas refine matrix sidecar apply should upsert matrix variants by id instead of duplicating them", + ) + with tempfile.NamedTemporaryFile("w", suffix=".json", delete=False, encoding="utf-8") as matrix_apply_draft_handle: + json.dump(matrix_sidecar_draft, matrix_apply_draft_handle) + cli_matrix_apply_draft_path = Path(matrix_apply_draft_handle.name) + try: + matrix_apply_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--apply-matrix-sidecar-update-draft", + "--matrix-sidecar-update-draft-json", + str(cli_matrix_apply_draft_path), + "--folder", + str(matrix_apply_root), + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + finally: + cli_matrix_apply_draft_path.unlink(missing_ok=True) + _expect( + matrix_apply_cli_result.returncode == 0, + f"Atlas refine matrix sidecar apply CLI failed: {matrix_apply_cli_result.stderr}", + ) + cli_matrix_apply_report = json.loads(matrix_apply_cli_result.stdout) + _expect( + cli_matrix_apply_report.get("applied") is True, + "Atlas refine matrix sidecar apply CLI should report applied", + ) + (matrix_apply_root / "pov_footjob_frontal_sole_stroke_00001_.txt").write_text( + footjob.get("prompt_text", ""), + encoding="utf-8", + ) + (matrix_apply_root / "pov_footjob_frontal_sole_stroke_00001_.png").write_bytes(b"fake-png") + matrix_applied_manifest = manifest_module.build_manifest(matrix_apply_root, subject_id="same_woman_001") + matrix_applied_entry = (matrix_applied_manifest.get("entries") or [{}])[0] + matrix_applied_prompt_variants = { + variant.get("id"): variant for variant in matrix_applied_entry.get("prompt_variants", []) + } + rescanned_matrix_evidence = matrix_applied_prompt_variants.get("soles_more_forward", {}).get("matrix_evidence") or {} + _expect( + rescanned_matrix_evidence.get("stable") is True + and rescanned_matrix_evidence.get("selection_seed") == 202, + f"Atlas refine applied matrix sidecar should rescan with matrix evidence: {rescanned_matrix_evidence}", + ) + matrix_prompt_batch = manifest_module.build_prompt_batch( + matrix_applied_manifest, + "pov_footjob_frontal_sole_stroke", + sampler_seed=101, + ) + matrix_batch_variant = next( + ( + probe + for probe in matrix_prompt_batch.get("probes", []) + if probe.get("id") == "pov_footjob_frontal_sole_stroke_00001__soles_more_forward" + ), + {}, + ) + _expect( + matrix_batch_variant.get("matrix_evidence", {}).get("stable") is True + and matrix_batch_variant.get("matrix_evidence", {}).get("selection_seed") == 202, + f"Atlas refine normal prompt batch should carry stable matrix evidence: {matrix_batch_variant}", + ) + matrix_batch_results = { + "seed": matrix_prompt_batch.get("seed"), + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": probe.get("id"), + "prompt_order": probe.get("prompt_order"), + "turn": 40 + index, + "image_path": f"/tmp/atlas_matrix_prompt_batch_{index}.png", + "returned_seed": matrix_prompt_batch.get("seed"), + } + for index, probe in enumerate(matrix_prompt_batch.get("probes") or []) + ], + } + matrix_batch_result_sheet = manifest_module.build_result_sheet( + matrix_prompt_batch, + matrix_batch_results, + notes="normal matrix batch scoring pending", + ) + matrix_sheet_variant = next( + ( + probe + for probe in matrix_batch_result_sheet.get("probes", []) + if probe.get("id") == "pov_footjob_frontal_sole_stroke_00001__soles_more_forward" + ), + {}, + ) + _expect( + matrix_sheet_variant.get("matrix_evidence", {}).get("stable") is True + and matrix_sheet_variant.get("matrix_evidence", {}).get("selection_seed") == 202, + f"Atlas refine normal result sheet should keep stable matrix evidence: {matrix_sheet_variant}", + ) + scored_matrix_batch_result_sheet = json.loads(json.dumps(matrix_batch_result_sheet)) + for probe in scored_matrix_batch_result_sheet.get("probes") or []: + if probe.get("id") == "pov_footjob_frontal_sole_stroke_00001__soles_more_forward": + probe["score"].update( + { + "atlas_pose_match": "pass", + "contact_match": "pass", + "pose_ownership": "pass", + "workspace_continuity": "pass", + "clothing_visibility": "pass", + "subject_identity": "pass", + "expression_eye_control": "pass", + "anatomy_proportion": "pass", + "prompt_noise": "pass", + } + ) + matrix_single_promotion = manifest_module.build_promotion_report(scored_matrix_batch_result_sheet) + matrix_single_candidate = next( + ( + candidate + for candidate in matrix_single_promotion.get("candidates", []) + if candidate.get("prompt_variant_id") == "soles_more_forward" + ), + {}, + ) + _expect( + matrix_single_candidate.get("matrix_evidence", {}).get("stable") is True + and matrix_single_candidate.get("matrix_evidence", {}).get("selection_seed") == 202, + f"Atlas refine normal promotion report should preserve matrix evidence: {matrix_single_candidate}", + ) + matrix_single_sidecar_draft = manifest_module.build_sidecar_update_draft(matrix_single_promotion) + matrix_single_variant = ((matrix_single_sidecar_draft.get("updates") or [{}])[0].get("prompt_variants") or [{}])[0] + _expect( + matrix_single_variant.get("matrix_evidence", {}).get("stable") is True + and matrix_single_variant.get("matrix_evidence", {}).get("selection_seed") == 202, + f"Atlas refine normal sidecar draft should preserve matrix evidence: {matrix_single_variant}", + ) + unstable_matrix_result_sheet = json.loads(json.dumps(scored_matrix_batch_result_sheet)) + for probe in unstable_matrix_result_sheet.get("probes") or []: + if probe.get("id") == "pov_footjob_frontal_sole_stroke_00001__soles_more_forward": + probe["matrix_evidence"]["stable"] = False + unstable_matrix_promotion = manifest_module.build_promotion_report(unstable_matrix_result_sheet) + unstable_matrix_candidate = next( + ( + candidate + for candidate in unstable_matrix_promotion.get("candidates", []) + if candidate.get("prompt_variant_id") == "soles_more_forward" + ), + {}, + ) + _expect( + unstable_matrix_candidate.get("decision") == "rejected" + and "unstable_matrix_evidence" in unstable_matrix_candidate.get("blockers", []), + f"Atlas refine promotion report should reject unstable matrix evidence: {unstable_matrix_candidate}", + ) + unstable_matrix_sidecar_draft = manifest_module.build_sidecar_update_draft(unstable_matrix_promotion) + _expect( + unstable_matrix_sidecar_draft.get("ready_candidate_count") == 0, + f"Atlas refine sidecar draft should skip unstable matrix evidence: {unstable_matrix_sidecar_draft}", + ) + matrix_selection = manifest_module.select_seeded_prompt_variant( + matrix_applied_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + seed_slot="atlas_cue_seed", + ) + selected_matrix_evidence = matrix_selection.get("selected", {}).get("matrix_evidence") or {} + _expect( + selected_matrix_evidence.get("stable") is True + and selected_matrix_evidence.get("selection_seed") == 202 + and selected_matrix_evidence.get("sampler_seeds") == [101, 102], + f"Atlas refine seed selection should preserve stable matrix evidence: {selected_matrix_evidence}", + ) + malformed_stable_matrix_manifest = json.loads(json.dumps(matrix_applied_manifest)) + malformed_entry = (malformed_stable_matrix_manifest.get("entries") or [{}])[0] + for malformed_variant in malformed_entry.get("prompt_variants") or []: + if malformed_variant.get("id") == "soles_more_forward": + malformed_matrix_evidence = malformed_variant["matrix_evidence"] + malformed_matrix_evidence["sampler_seeds"] = [ + malformed_matrix_evidence["sampler_seeds"][0], + malformed_matrix_evidence["sampler_seeds"][0], + *malformed_matrix_evidence["sampler_seeds"][1:], + ] + malformed_stable_matrix_selection = manifest_module.select_seeded_prompt_variant( + malformed_stable_matrix_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + seed_slot="atlas_cue_seed", + ) + _expect( + malformed_stable_matrix_selection.get("eligible_candidate_count") == 0 + and malformed_stable_matrix_selection.get("selected") == {}, + f"Atlas refine seed selection should reject malformed stable matrix evidence: {malformed_stable_matrix_selection}", + ) + _expect( + any("unstable_matrix_evidence" in item.get("reason", "") for item in malformed_stable_matrix_selection.get("ineligible", [])), + f"Atlas refine seed selection should explain malformed stable matrix evidence rejection: {malformed_stable_matrix_selection}", + ) + context_drift_matrix_manifest = json.loads(json.dumps(matrix_applied_manifest)) + context_drift_entry = (context_drift_matrix_manifest.get("entries") or [{}])[0] + for context_drift_variant in context_drift_entry.get("prompt_variants") or []: + if context_drift_variant.get("id") == "soles_more_forward": + context_drift_variant["seed_metadata"]["atlas_cue_seed"] = 999 + context_drift_matrix_selection = manifest_module.select_seeded_prompt_variant( + context_drift_matrix_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + seed_slot="atlas_cue_seed", + ) + _expect( + context_drift_matrix_selection.get("eligible_candidate_count") == 0 + and context_drift_matrix_selection.get("selected") == {}, + f"Atlas refine seed selection should reject context-drifted stable matrix evidence: {context_drift_matrix_selection}", + ) + _expect( + any("unstable_matrix_evidence" in item.get("reason", "") for item in context_drift_matrix_selection.get("ineligible", [])), + f"Atlas refine seed selection should explain context-drifted stable matrix evidence rejection: {context_drift_matrix_selection}", + ) + unstable_matrix_manifest = json.loads(json.dumps(matrix_applied_manifest)) + unstable_entry = (unstable_matrix_manifest.get("entries") or [{}])[0] + for unstable_variant in unstable_entry.get("prompt_variants") or []: + if unstable_variant.get("id") == "soles_more_forward": + unstable_variant["matrix_evidence"]["stable"] = False + unstable_matrix_selection = manifest_module.select_seeded_prompt_variant( + unstable_matrix_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + seed_slot="atlas_cue_seed", + ) + _expect( + unstable_matrix_selection.get("eligible_candidate_count") == 0 + and unstable_matrix_selection.get("selected") == {}, + f"Atlas refine seed selection should reject explicitly unstable matrix evidence: {unstable_matrix_selection}", + ) + _expect( + any("unstable_matrix_evidence" in item.get("reason", "") for item in unstable_matrix_selection.get("ineligible", [])), + f"Atlas refine seed selection should explain unstable matrix evidence rejection: {unstable_matrix_selection}", + ) + matrix_catalog_cue_draft = manifest_module.build_catalog_cue_draft( + matrix_applied_manifest, + variant_key="pov_footjob_frontal_sole_stroke", + ) + matrix_catalog_candidates = matrix_catalog_cue_draft.get("candidates") or [] + _expect( + matrix_catalog_cue_draft.get("ready_cue_count") == 1 + and matrix_catalog_candidates[0].get("matrix_evidence", {}).get("stable") is True, + f"Atlas refine catalog cue draft should preserve stable matrix evidence: {matrix_catalog_cue_draft}", + ) + unstable_catalog_cue_draft = manifest_module.build_catalog_cue_draft( + unstable_matrix_manifest, + variant_key="pov_footjob_frontal_sole_stroke", + ) + _expect( + unstable_catalog_cue_draft.get("ready_cue_count") == 0, + f"Atlas refine catalog cue draft should reject unstable matrix evidence: {unstable_catalog_cue_draft}", + ) + _expect( + any("unstable_matrix_evidence" in item.get("reason", "") for item in unstable_catalog_cue_draft.get("skipped", [])), + f"Atlas refine catalog cue draft should explain unstable matrix evidence rejection: {unstable_catalog_cue_draft}", + ) + unstable_coverage = manifest_module.build_coverage_report(unstable_matrix_manifest) + unstable_coverage_entry = (unstable_coverage.get("entries") or [{}])[0] + _expect( + unstable_coverage_entry.get("catalog_cue_candidate_count") == 0 + and unstable_coverage_entry.get("state") != "ready_for_catalog_review", + f"Atlas refine coverage should not mark unstable matrix evidence ready for catalog review: {unstable_coverage_entry}", + ) + unstable_variant_summary = next( + ( + variant + for variant in unstable_coverage_entry.get("prompt_variants", []) + if variant.get("prompt_variant_id") == "soles_more_forward" + ), + {}, + ) + _expect( + unstable_variant_summary.get("decision") == "rejected" + and "unstable_matrix_evidence" in unstable_variant_summary.get("blockers", []), + f"Atlas refine coverage should attach unstable matrix evidence blockers: {unstable_variant_summary}", + ) + matrix_selected_batch = manifest_module.build_seed_selected_prompt_batch( + matrix_applied_manifest, + "pov_footjob_frontal_sole_stroke", + selection_seed=202, + sampler_seed=101, + seed_slot="atlas_cue_seed", + ) + matrix_selected_candidate = (matrix_selected_batch.get("probes") or [{}])[-1] + candidate_matrix_evidence = matrix_selected_candidate.get("matrix_evidence") or {} + _expect( + candidate_matrix_evidence.get("stable") is True + and candidate_matrix_evidence.get("selection_seed") == 202, + f"Atlas refine selected batch should carry stable matrix evidence: {candidate_matrix_evidence}", + ) + matrix_selected_results = { + "seed": 101, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": (matrix_selected_batch.get("probes") or [{}])[0].get("id"), + "prompt_order": "subject_first", + "turn": 31, + "image_path": "/tmp/pov_footjob_matrix_selected_baseline.png", + "returned_seed": 101, + }, + { + "id": matrix_selected_candidate.get("id"), + "prompt_order": "subject_first", + "turn": 32, + "image_path": "/tmp/pov_footjob_matrix_selected_candidate.png", + "returned_seed": 101, + }, + ], + } + matrix_selected_result_sheet = manifest_module.build_result_sheet( + matrix_selected_batch, + matrix_selected_results, + notes="matrix-selected visual scoring pending", + ) + matrix_selected_sheet_candidate = (matrix_selected_result_sheet.get("probes") or [{}])[-1] + sheet_matrix_evidence = matrix_selected_sheet_candidate.get("matrix_evidence") or {} + _expect( + sheet_matrix_evidence.get("stable") is True + and sheet_matrix_evidence.get("selection_seed") == 202, + f"Atlas refine selected result sheet should preserve matrix evidence for scoring: {sheet_matrix_evidence}", + ) + selected_results = { + "seed": 101, + "channel_in": "sxcp_eval_in", + "probes": [ + { + "id": "pov_footjob_frontal_sole_stroke_00001__baseline", + "prompt_order": "subject_first", + "turn": 21, + "image_path": "/tmp/pov_footjob_selected_baseline.png", + "returned_seed": 101, + }, + { + "id": "pov_footjob_frontal_sole_stroke_00001__soles_more_forward", + "prompt_order": "subject_first", + "turn": 22, + "image_path": "/tmp/pov_footjob_selected_candidate.png", + "returned_seed": 101, + }, + ], + } + selected_result_sheet = manifest_module.build_result_sheet( + selected_batch, + selected_results, + notes="selected seed visual scoring pending", + ) + _expect( + selected_result_sheet.get("selection", {}).get("selection_seed") == 202, + "Atlas refine selected result sheet should keep cue seed selection metadata", + ) + selected_sheet_probes = selected_result_sheet.get("probes") or [] + _expect( + selected_sheet_probes[1].get("selection", {}).get("prompt_variant_id") == "soles_more_forward", + "Atlas refine selected result sheet should keep selected prompt variant id on the candidate", + ) + _expect( + selected_sheet_probes[1].get("seed_metadata", {}).get("atlas_cue_seed") == 202, + "Atlas refine selected result sheet should keep candidate cue seed metadata", + ) + selected_batch_cli_result = subprocess.run( + [ + sys.executable, + str(ROOT / "tools" / "krea2_atlas_refine_manifest.py"), + "--folder", + str(apply_root), + "--subject-id", + "same_woman_001", + "--print-seed-selected-batch", + "--variant-key", + "pov_footjob_frontal_sole_stroke", + "--selection-seed", + "202", + "--sampler-seed", + "101", + "--seed-slot", + "atlas_cue_seed", + ], + cwd=ROOT, + capture_output=True, + text=True, + check=False, + ) + _expect(selected_batch_cli_result.returncode == 0, f"Atlas refine seed-selected batch CLI failed: {selected_batch_cli_result.stderr}") + cli_selected_batch = json.loads(selected_batch_cli_result.stdout) + _expect( + cli_selected_batch.get("selection", {}).get("selected", {}).get("prompt_variant_id") == "soles_more_forward", + "Atlas refine seed-selected batch CLI should include the selected prompt variant", + ) + unknown = by_variant.get("pov_unknown_pose_candidate") or {} + _expect(unknown.get("known_variant") is False, "Atlas refine manifest should keep unknown pairs but flag them") + missing = manifest.get("missing_pairs") or [] + _expect( + missing and missing[0].get("stem") == "pov_blowjob_side_profile_oral_00002_", + f"Atlas refine manifest should report the orphan stem: {missing}", + ) + _expect(cli_manifest.get("entry_count") == 3, "Atlas refine manifest CLI lost paired entries") + _expect("pov_footjob_frontal_sole_stroke" in cli_stdout, "Atlas refine manifest CLI lost known variant key") + + def smoke_krea_pov_penetration_route() -> None: pair = pb.build_insta_of_pair( row_number=1, @@ -10831,11 +14556,31 @@ def smoke_sxcp_prompt_batch_cli_policy() -> None: "seed": 123456789, "channel_out": "sxcp_eval_out", "channel_in": "sxcp_eval_in", + "subject_id": "same_woman_001", + "variant_key": "pov_footjob_frontal_sole_stroke", + "source_entry_id": "pov_footjob_frontal_sole_stroke_00001", + "source_stem": "pov_footjob_frontal_sole_stroke_00001_", + "selection": {"selection_seed": 202, "seed_slot": "atlas_cue_seed"}, "probes": [ { "id": "subject_first_axis", "prompt_order": "subject_first", "text": "A 24-year-old adult woman with long wavy brunette hair. Subject-first controlled pose wording.", + "variant_key": "pov_footjob_frontal_sole_stroke", + "source_entry_id": "pov_footjob_frontal_sole_stroke_00001", + "source_stem": "pov_footjob_frontal_sole_stroke_00001_", + "cue_axes": {"foot_position": "soles_more_forward"}, + "seed_metadata": {"atlas_cue_seed": 202}, + "evidence": {"seed": 123456789, "image_path": "/tmp/subject_first_axis.png"}, + "matrix_evidence": { + "stable": True, + "selection_seed": 202, + "sampler_seeds": [123456789], + "jobs": [{"id": "job_1"}], + }, + "selection": {"selection_seed": 202, "seed_slot": "atlas_cue_seed"}, + "prompt_source": {"kind": "append_cues", "append_cues": ["soles farther forward"]}, + "notes": "metadata should survive helper loading", }, { "id": "geometry_only_axis", @@ -10848,6 +14593,19 @@ def smoke_sxcp_prompt_batch_cli_policy() -> None: json.dump(batch, handle) batch_path = Path(handle.name) try: + loaded_batch = sxcp_prompt_batch.load_batch(batch_path) + loaded_probe = loaded_batch.get("probes", [{}])[0] + _expect( + loaded_batch.get("variant_key") == "pov_footjob_frontal_sole_stroke" + and loaded_batch.get("selection", {}).get("selection_seed") == 202, + f"sxcp prompt batch loader should preserve atlas batch metadata: {loaded_batch}", + ) + _expect( + loaded_probe.get("matrix_evidence", {}).get("stable") is True + and loaded_probe.get("cue_axes", {}).get("foot_position") == "soles_more_forward" + and loaded_probe.get("seed_metadata", {}).get("atlas_cue_seed") == 202, + f"sxcp prompt batch loader should preserve atlas probe metadata: {loaded_probe}", + ) commands = subprocess.run( [sys.executable, str(helper_path), "print-push-commands", "--batch-json", str(batch_path)], cwd=ROOT, @@ -11568,6 +15326,103 @@ def smoke_node_hardcore_position_registration() -> None: _expect("torso bent forward" in avoid_cues, "Krea2 Pose Variant lost avoid cues output") _expect("variant=pov_boobjob_upright_cleavage" in variant_summary, "Krea2 Pose Variant summary lost key") + oral_filter = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2POVOralFilter"] + synthetic_variant = { + "key": "pov_synthetic_node_seeded_oral", + "family": "oral", + "status": "candidate", + "action_family": "oral", + "position_keys": ["top_down_oral"], + "canonical_geometry": "synthetic seeded oral atlas geometry", + "prompt_cues": ["synthetic baseline oral atlas cue"], + "prompt_variant_cues": [ + ["synthetic alternate oral atlas cue"], + {"prompt_cues": ["synthetic second oral atlas cue"]}, + ], + "avoid_cues": ["synthetic avoid cue"], + } + original_get_variant = krea2_pose_variant_catalog.get_variant + original_variants = krea2_pose_variant_catalog.variants + try: + def fake_get_variant(key: str, **kwargs): + if key == "pov_synthetic_node_seeded_oral": + return dict(synthetic_variant) + return original_get_variant(key, **kwargs) + + def fake_variants(*, status=None, family=None, action_family=None, path=None): + if action_family == "oral": + return [dict(synthetic_variant)] + return original_variants(status=status, family=family, action_family=action_family, path=path) + + krea2_pose_variant_catalog.get_variant = fake_get_variant + krea2_pose_variant_catalog.variants = fake_variants + + seeded_config_a, _seeded_key_a, _seeded_cues_a, _seeded_avoid_a, seeded_summary_a, _seeded_json_a = ( + variant_node().build( + "pov_synthetic_node_seeded_oral", + "replace", + "", + atlas_cue_seed=901, + ) + ) + seeded_config_b, _seeded_key_b, _seeded_cues_b, _seeded_avoid_b, seeded_summary_b, _seeded_json_b = ( + variant_node().build( + "pov_synthetic_node_seeded_oral", + "replace", + "", + atlas_cue_seed=901, + ) + ) + parsed_seeded_a = json.loads(seeded_config_a) + parsed_seeded_b = json.loads(seeded_config_b) + seeded_index = parsed_seeded_a.get("krea2_prompt_variant_indices", {}).get("pov_synthetic_node_seeded_oral") + _expect( + parsed_seeded_a.get("krea2_variant_keys") == ["pov_synthetic_node_seeded_oral"], + "Krea2 Pose Variant should write selected atlas variant metadata", + ) + _expect( + isinstance(seeded_index, int) and 0 <= seeded_index < 3, + f"Krea2 Pose Variant should store a seeded prompt cue index, got {seeded_index!r}", + ) + _expect( + parsed_seeded_b.get("krea2_prompt_variant_indices", {}).get("pov_synthetic_node_seeded_oral") == seeded_index, + "Krea2 Pose Variant atlas cue seed should be deterministic for the same seed", + ) + _expect( + parsed_seeded_a.get("krea2_prompt_variant_seed") == 901, + "Krea2 Pose Variant should record atlas cue seed provenance", + ) + _expect( + "cue_seed=901" in seeded_summary_a and "cue_indices=pov_synthetic_node_seeded_oral:" in seeded_summary_a, + "Krea2 Pose Variant summary should expose atlas cue seed and selected cue index", + ) + + include_synthetic = _atlas_variant_include_key("pov_synthetic_node_seeded_oral") + synthetic_filter_config, synthetic_filter_keys, _synthetic_positions, _synthetic_notes, synthetic_filter_summary, _ = ( + oral_filter().build( + "replace", + "", + atlas_cue_seed=902, + **{include_synthetic: True}, + ) + ) + parsed_filter_seeded = json.loads(synthetic_filter_config) + _expect( + synthetic_filter_keys == "pov_synthetic_node_seeded_oral", + "Krea2 POV filter should select the synthetic seeded variant", + ) + _expect( + parsed_filter_seeded.get("krea2_prompt_variant_seed") == 902, + "Krea2 POV filter should record atlas cue seed provenance", + ) + _expect( + "cue_seed=902" in synthetic_filter_summary, + "Krea2 POV filter summary should expose atlas cue seed", + ) + finally: + krea2_pose_variant_catalog.get_variant = original_get_variant + krea2_pose_variant_catalog.variants = original_variants + penetration_filter = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2POVPenetrationFilter"] penetration_inputs = penetration_filter.INPUT_TYPES().get("required") or {} _expect("include_doggy_top_down_rear_entry" in penetration_inputs, "POV Penetration Filter lost doggy atlas checkbox") @@ -11575,7 +15430,6 @@ def smoke_node_hardcore_position_registration() -> None: "include_blowjob_side_profile_oral" not in penetration_inputs, "POV Penetration Filter should not expose oral atlas checkboxes", ) - oral_filter = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPKrea2POVOralFilter"] oral_inputs = oral_filter.INPUT_TYPES().get("required") or {} _expect("include_blowjob_side_profile_oral" in oral_inputs, "POV Oral Filter lost blowjob side-profile checkbox") _expect( @@ -12718,6 +16572,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [ ("krea2_eval_log_policy", smoke_krea2_eval_log_policy), ("krea2_prompt_guide_policy", smoke_krea2_prompt_guide_policy), ("krea2_tuning_report_policy", smoke_krea2_tuning_report_policy), + ("krea2_atlas_refine_manifest_policy", smoke_krea2_atlas_refine_manifest_policy), ("krea_pov_penetration_route", smoke_krea_pov_penetration_route), ("pov_outercourse_position_routes", smoke_pov_outercourse_position_routes), ("pov_oral_position_routes", smoke_pov_oral_position_routes), diff --git a/tools/sxcp_prompt_batch.py b/tools/sxcp_prompt_batch.py index 641a2f1..1c4536b 100644 --- a/tools/sxcp_prompt_batch.py +++ b/tools/sxcp_prompt_batch.py @@ -26,6 +26,27 @@ 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"} +PROBE_METADATA_FIELDS = ( + "variant_key", + "source_entry_id", + "source_stem", + "cue_axes", + "seed_metadata", + "evidence", + "matrix_evidence", + "selection", + "prompt_source", + "reference_images", + "notes", +) +BATCH_METADATA_FIELDS = ( + "subject_id", + "variant_key", + "source_entry_id", + "source_stem", + "source_prompt_sha256", + "selection", +) class BatchError(ValueError): @@ -80,7 +101,11 @@ def _validate_probe(raw: Any, index: int) -> dict[str, str]: 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} + probe: dict[str, Any] = {"id": probe_id, "prompt_order": prompt_order, "text": text} + for field in PROBE_METADATA_FIELDS: + if field in raw: + probe[field] = raw[field] + return probe def _validate_image_path(value: Any, *, field: str) -> str: @@ -111,12 +136,16 @@ def load_batch(path: Path) -> dict[str, Any]: 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 { + loaded = { "seed": seed, "channel_out": channel_out, "channel_in": channel_in, "probes": probes, } + for field in BATCH_METADATA_FIELDS: + if field in batch: + loaded[field] = batch[field] + return loaded def load_results(path: Path) -> dict[str, Any]: