diff --git a/categories/krea2_pov_pose_variants.json b/categories/krea2_pov_pose_variants.json index 8a0393e..35a26cb 100644 --- a/categories/krea2_pov_pose_variants.json +++ b/categories/krea2_pov_pose_variants.json @@ -118,9 +118,10 @@ "atlas_folders": ["ballsucking"], "action_family": "outercourse", "position_keys": ["testicle_sucking", "ballsucking"], - "canonical_geometry": "Low first-person pelvis view: the woman's head is below the shaft at testicle height, mouth and tongue at the balls, with the penis pointing upward above or in front of her face.", + "canonical_geometry": "Low first-person pelvis view: the woman bends forward between the viewer's open thighs with her chest low over his pelvis, head below the shaft at testicle height, mouth and tongue at the balls, and the penis pointing upward above or in front of her face.", "prompt_cues": [ "woman bends forward and kneels very low between the viewer's open thighs", + "chest low over the viewer's pelvis", "face is below the viewer's penis at testicle height", "mouth and tongue licking the viewer's balls", "penis points upward in the lower foreground above her forehead" @@ -140,7 +141,7 @@ }, "evidence": { "fixed_seed_tests": [], - "guide_section": "", + "guide_section": "docs/krea2-prompt-guide.md#ballsucking--testicle-sucking", "notes": "Atlas supports low-head geometry, but this route still needs controlled fixed-seed tests before promotion to proven." } }, diff --git a/docs/krea2-prompt-guide.md b/docs/krea2-prompt-guide.md index 779f86d..0b667b3 100644 --- a/docs/krea2-prompt-guide.md +++ b/docs/krea2-prompt-guide.md @@ -148,6 +148,29 @@ semen` wording for this path before the prompt reaches Krea2. ## POV Outercourse +### Ballsucking / Testicle Sucking + +The atlas examples are low first-person pelvis views. The visible partner should +read as bent forward between the viewer's open thighs, with the chest low over +the viewer's pelvis and the face placed below the shaft at testicle height. +Starting from the viewer or using only generic kneeling language can make the +head float too high. + +Works better: + +- `the woman bends forward and kneels very low between the viewer's open thighs` +- `chest low over the viewer's pelvis` +- `face is below the viewer's penis at testicle height` +- `mouth and tongue licking the viewer's balls` +- `penis points upward in the lower foreground above her forehead` + +Avoid: + +- `head tucked under the penis shaft` without testicle-height wording +- generic `kneels in front of him` +- making the viewer the main subject before the visible woman is established +- mid-height head placement + ### Boobjob / Titjob The atlas examples are frontal and upright: the visible partner faces the viewer, diff --git a/hardcore_role_outercourse.py b/hardcore_role_outercourse.py index a6d8916..fb82709 100644 --- a/hardcore_role_outercourse.py +++ b/hardcore_role_outercourse.py @@ -42,12 +42,12 @@ def build_outercourse_role_graph( if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE: if man_is_pov: return ( - f"{woman} bends forward and kneels very low between the POV viewer's open thighs with her shoulders between his knees, " + f"{woman} bends forward and kneels very low between the POV viewer's open thighs with her chest low over the POV viewer's pelvis and shoulders between his knees, " "her face below the POV viewer's penis at testicle height, mouth and tongue on the POV viewer's balls, " "while his penis points upward in the lower foreground above her forehead." ) return ( - f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her torso bent forward and shoulders between his knees, " + f"{man} sits with legs apart while {woman} kneels very low between his open thighs with her chest low over his pelvis and shoulders between his knees, " f"{woman}'s face below {man}'s penis at testicle height, mouth and tongue on his balls, while {man}'s penis points upward above her forehead." ) if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING: diff --git a/krea_pov_actions.py b/krea_pov_actions.py index 096390c..e8e421c 100644 --- a/krea_pov_actions.py +++ b/krea_pov_actions.py @@ -286,7 +286,7 @@ def pov_hardcore_pose_sentence( ) if action_kind == outercourse_policy.OUTERCOURSE_TESTICLE: return outercourse_sentence( - "The woman bends forward and kneels very low between the viewer's open thighs with her shoulders between his knees; " + "The woman bends forward and kneels very low between the viewer's open thighs with her chest low over the viewer's pelvis and shoulders between his knees; " "her face is below the viewer's penis at testicle height, mouth and tongue licking the viewer's balls while his penis points upward in the lower foreground above her forehead" ) if action_kind == outercourse_policy.OUTERCOURSE_PENIS_LICKING: diff --git a/tools/prompt_route_simulation.py b/tools/prompt_route_simulation.py index 1ffbc76..e7cc339 100644 --- a/tools/prompt_route_simulation.py +++ b/tools/prompt_route_simulation.py @@ -1661,6 +1661,8 @@ def run_simulation(seed: int = 3901, *, include_prompts: bool = False) -> dict[s cases.extend(_pair_reports("insta_pair.penetration", penetration_pair, include_prompts=include_prompts)) pov_pair = _insta_pair_case(seed + 2, pov=True, position="penis_licking", focus="outercourse_only", family="outercourse") cases.extend(_pair_reports("insta_pair.pov_outercourse", pov_pair, include_prompts=include_prompts)) + ballsucking_pair = _insta_pair_case(seed + 5, pov=True, position="testicle_sucking", focus="outercourse_only", family="outercourse") + cases.extend(_pair_reports("insta_pair.pov_ballsucking", ballsucking_pair, include_prompts=include_prompts)) coverage_checks = _route_family_coverage_checks(cases) axis_checks = _seed_axis_checks(seed + 3) pair_seed_checks = _pair_seed_checks(seed + 4) diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index af1db42..45fb1ce 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -6845,6 +6845,11 @@ def smoke_krea2_pose_variant_catalog_policy() -> None: handjob["prompt_cues"].append("mutation should not leak") clean_handjob = krea2_pose_variant_catalog.get_variant("pov_handjob_upright_centered") _expect("mutation should not leak" not in clean_handjob.get("prompt_cues", []), "Catalog loader leaked caller mutation") + ballsucking = krea2_pose_variant_catalog.get_variant("pov_ballsucking_low_head") + _expect( + any("chest low over the viewer's pelvis" in str(cue) for cue in ballsucking.get("prompt_cues", [])), + "Ballsucking variant lost low-body pelvis cue", + ) footjob = krea2_pose_variant_catalog.get_variant("pov_footjob_frontal_sole_stroke") _expect(footjob.get("status") == "candidate", "Footjob variant should remain a candidate until fixed-seed evidence exists") _expect( @@ -7084,6 +7089,8 @@ def smoke_krea2_prompt_guide_policy() -> None: guide = (ROOT / "docs" / "krea2-prompt-guide.md").read_text(encoding="utf-8") _expect("## Climax / Ejaculation Wording" in guide, "Krea2 prompt guide lost climax wording section") _expect("ejaculates semen" in guide, "Krea2 prompt guide lost explicit semen wording rule") + _expect("### Ballsucking / Testicle Sucking" in guide, "Krea2 prompt guide lost ballsucking section") + _expect("chest low over the viewer's pelvis" in guide, "Krea2 prompt guide lost low-body ballsucking cue") _expect("## Stronger-Control / Low-Priority Cases" in guide, "Krea2 prompt guide lost stronger-control section") _expect("pov_sixty_nine_close_reversed_oral" in guide, "Krea2 prompt guide lost sixty-nine unstable route") _expect("hardest" in guide and "low-priority" in guide, "Krea2 prompt guide lost hardest low-priority wording") @@ -7527,8 +7534,8 @@ def smoke_pov_outercourse_position_routes() -> None: ( "pov_outercourse_testicle", "testicle_sucking", - ("face below the pov viewer's penis at testicle height", "penis points upward"), - ("face is below the viewer's penis at testicle height", "mouth and tongue licking", "penis points upward"), + ("chest low over the pov viewer's pelvis", "face below the pov viewer's penis at testicle height", "penis points upward"), + ("chest low over the viewer's pelvis", "face is below the viewer's penis at testicle height", "mouth and tongue licking", "penis points upward"), ), ( "pov_outercourse_penis_licking", @@ -9027,17 +9034,17 @@ def smoke_prompt_route_simulation_policy() -> None: report = prompt_route_simulation.run_simulation(seed=3901, include_prompts=False) summary = report.get("summary") or {} quality = report.get("quality") or {} - _expect(summary.get("cases") == 14, "Prompt route simulation case count changed unexpectedly") + _expect(summary.get("cases") == 16, "Prompt route simulation case count changed unexpectedly") _expect(summary.get("coverage_checks") == 2, "Prompt route simulation lost family coverage checks") _expect(summary.get("axis_checks") == 6, "Prompt route simulation lost axis check coverage") _expect(summary.get("pair_seed_checks") == 7, "Prompt route simulation lost pair seed check coverage") _expect(summary.get("issues") == 0, f"Prompt route simulation reported issues: {report.get('issues')}") - _expect(quality.get("route_cases") == 14, "Prompt route simulation quality summary lost route case count") + _expect(quality.get("route_cases") == 16, "Prompt route simulation quality summary lost route case count") _expect(quality.get("route_issues") == 0, f"Prompt route simulation quality reported route issues: {quality}") _expect(quality.get("check_issues") == 0, f"Prompt route simulation quality reported check issues: {quality}") _expect((quality.get("targets") or {}).get("single", {}).get("cases") == 10, "Prompt route simulation quality lost single target count") - _expect((quality.get("targets") or {}).get("softcore", {}).get("cases") == 2, "Prompt route simulation quality lost softcore target count") - _expect((quality.get("targets") or {}).get("hardcore", {}).get("cases") == 2, "Prompt route simulation quality lost hardcore target count") + _expect((quality.get("targets") or {}).get("softcore", {}).get("cases") == 3, "Prompt route simulation quality lost softcore target count") + _expect((quality.get("targets") or {}).get("hardcore", {}).get("cases") == 3, "Prompt route simulation quality lost hardcore target count") _expect(not quality.get("issue_buckets"), "Prompt route simulation quality should have no issue buckets on clean baseline") _expect(not quality.get("weakest_cases"), "Prompt route simulation quality should have no weak cases on clean baseline") cases = {case.get("name"): case for case in report.get("cases") or []} @@ -9052,6 +9059,7 @@ def smoke_prompt_route_simulation_policy() -> None: "hardcore.single.group", "hardcore.single.climax", "insta_pair.penetration.hardcore", + "insta_pair.pov_ballsucking.hardcore", ): _expect(route_name in cases, f"Prompt route simulation lost route family case {route_name}") coverage_checks = {check.get("name"): check for check in report.get("coverage_checks") or []} @@ -9079,6 +9087,16 @@ def smoke_prompt_route_simulation_policy() -> None: "penis_licking" in (pov_summary.get("position_keys") or []), "Prompt route simulation lost selected outercourse key from position_keys", ) + ballsucking_hard = cases.get("insta_pair.pov_ballsucking.hardcore") or {} + ballsucking_summary = ballsucking_hard.get("summary") or {} + _expect( + ballsucking_summary.get("position_key") == "testicle_sucking", + "Prompt route simulation should include a dedicated ballsucking/testicle POV route", + ) + _expect( + "testicle_sucking" in (ballsucking_summary.get("position_keys") or []), + "Prompt route simulation ballsucking route lost selected testicle_sucking key", + ) axis_checks = {check.get("name"): check for check in report.get("axis_checks") or []} for check_name in ( "seed_axis.locked_determinism", @@ -9144,14 +9162,14 @@ def smoke_prompt_route_simulation_policy() -> None: sweep_quality = sweep.get("quality") or {} _expect(sweep_summary.get("runs") == 3, "Prompt route simulation sweep lost run coverage") _expect(sweep_summary.get("seeds") == [3901, 4002, 4103], "Prompt route simulation sweep seed sequence changed") - _expect(sweep_summary.get("cases") == 42, "Prompt route simulation sweep case count changed") + _expect(sweep_summary.get("cases") == 48, "Prompt route simulation sweep case count changed") _expect(sweep_summary.get("issues") == 0, f"Prompt route simulation sweep reported issues: {sweep.get('issues')}") - _expect(sweep_quality.get("route_cases") == 42, "Prompt route simulation sweep quality lost route case count") + _expect(sweep_quality.get("route_cases") == 48, "Prompt route simulation sweep quality lost route case count") _expect(sweep_quality.get("route_issues") == 0, f"Prompt route simulation sweep quality reported route issues: {sweep_quality}") _expect(sweep_quality.get("check_issues") == 0, f"Prompt route simulation sweep quality reported check issues: {sweep_quality}") _expect((sweep_quality.get("targets") or {}).get("single", {}).get("cases") == 30, "Prompt route simulation sweep quality lost single target count") - _expect((sweep_quality.get("targets") or {}).get("softcore", {}).get("cases") == 6, "Prompt route simulation sweep quality lost softcore target count") - _expect((sweep_quality.get("targets") or {}).get("hardcore", {}).get("cases") == 6, "Prompt route simulation sweep quality lost hardcore target count") + _expect((sweep_quality.get("targets") or {}).get("softcore", {}).get("cases") == 9, "Prompt route simulation sweep quality lost softcore target count") + _expect((sweep_quality.get("targets") or {}).get("hardcore", {}).get("cases") == 9, "Prompt route simulation sweep quality lost hardcore target count") def smoke_node_camera_registration() -> None: