From 098721504d1267afa22cd24089bc86055a2e19d0 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 19:34:43 +0200 Subject: [PATCH] Validate pair seed simulation behavior --- docs/prompt-architecture-improvement-plan.md | 4 + docs/prompt-pool-routing-map.md | 3 + tools/prompt_map_audit.py | 8 + tools/prompt_route_simulation.py | 145 ++++++++++++++++++- tools/prompt_smoke.py | 14 ++ 5 files changed, 173 insertions(+), 1 deletion(-) diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 0c7987b..f7ecae2 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -26,6 +26,8 @@ The map audit currently sees: - Route simulation family coverage, so representative generated rows exercise every registered action and position family except documented special cases covered by dedicated smoke fixtures. +- Pair seed simulation, so Insta/OF soft/hard metadata and formatter outputs + prove locked determinism and pose-only reroll behavior. ## Architectural Finding @@ -643,6 +645,8 @@ Near-term: scene/camera/clothing fields. - Keep same-room pair continuity synchronized in both assembled prompt text and `hardcore_row.scene_text`; `tools/prompt_smoke.py` covers this drift case. +- Keep pair seed behavior synchronized across soft/hard rows; the route + simulator now checks locked pair determinism and pose-only hard-action rerolls. Medium-term: diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index aa29662..aecfabc 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -1005,6 +1005,9 @@ issues for: the primary `position_key`; - route-family coverage for registered action and position families, excluding only documented special cases that have their own smoke fixtures; +- pair seed determinism for Insta/OF metadata and formatted soft/hard outputs; +- pair pose rerolls changing hardcore action metadata while keeping cast, + scene, soft outfit, and composition axes stable; - pose-axis rerolls changing cast/scene metadata or failing to move pose/action metadata. diff --git a/tools/prompt_map_audit.py b/tools/prompt_map_audit.py index 0245025..d42aa2c 100644 --- a/tools/prompt_map_audit.py +++ b/tools/prompt_map_audit.py @@ -90,6 +90,14 @@ AUDIT_DOC_SNIPPETS: tuple[tuple[str, str], ...] = ( "docs/prompt-pool-routing-map.md", "route-family coverage for registered action and position families", ), + ( + "docs/prompt-pool-routing-map.md", + "pair seed determinism for Insta/OF metadata", + ), + ( + "docs/prompt-pool-routing-map.md", + "pair pose rerolls changing hardcore action metadata", + ), ) PROMPT_ROW_READ_SCAN_GLOBS: tuple[str, ...] = ( diff --git a/tools/prompt_route_simulation.py b/tools/prompt_route_simulation.py index aa78813..cbb68d5 100644 --- a/tools/prompt_route_simulation.py +++ b/tools/prompt_route_simulation.py @@ -858,6 +858,29 @@ def _insta_pair_case(seed: int, *, pov: bool, position: str, focus: str, family: ) +def _pair_seed_probe(seed: int, *, reroll_axis: str = "none", reroll_seed: int = -1) -> dict[str, Any]: + return pb.build_insta_of_pair( + row_number=1, + start_index=1, + seed=seed, + ethnicity="any", + figure="random", + no_plus_women=False, + no_black=False, + trigger=TRIGGER, + prepend_trigger_to_prompt=True, + seed_config=pb.build_seed_lock_config_json(base_seed=seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed), + options_json=_insta_options(), + character_cast=_random_character_cast(), + hardcore_position_config=_position_filter("penetration_only", "penetration", ["missionary", "doggy", "cowgirl"]), + location_config=_seed_probe_location_config(), + composition_config=_seed_probe_composition_config(), + camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=6.0), + softcore_camera_config=_orbit_camera(horizontal_angle=45, vertical_angle=0, zoom=5.5), + hardcore_camera_config=_orbit_camera(horizontal_angle=135, vertical_angle=20, zoom=7.5), + ) + + def _seed_probe_row(seed: int, *, reroll_axis: str = "none", reroll_seed: int = -1) -> dict[str, Any]: return pb.build_prompt( category="Hardcore sexual poses", @@ -903,6 +926,27 @@ def _seed_probe_snapshot(row: dict[str, Any]) -> dict[str, Any]: } +def _pair_seed_snapshot(pair: dict[str, Any]) -> dict[str, Any]: + soft_row = pair.get("softcore_row") if isinstance(pair.get("softcore_row"), dict) else {} + hard_row = pair.get("hardcore_row") if isinstance(pair.get("hardcore_row"), dict) else {} + return { + "shared_cast_descriptors": pair.get("shared_cast_descriptors"), + "soft_cast_descriptor_text": soft_row.get("cast_descriptor_text"), + "hard_cast_descriptor_text": hard_row.get("cast_descriptor_text"), + "soft_scene_text": soft_row.get("scene_text"), + "hard_scene_text": hard_row.get("scene_text"), + "soft_item": soft_row.get("item"), + "hard_item": hard_row.get("item"), + "hard_position_key": hard_row.get("position_key"), + "hard_position_keys": hard_row.get("position_keys") or [], + "hard_source_role_graph": hard_row.get("source_role_graph"), + "soft_composition": soft_row.get("composition"), + "hard_composition": hard_row.get("composition"), + "soft_expression": soft_row.get("character_expression_text"), + "hard_expression": hard_row.get("character_expression_text"), + } + + def _same_fields_issues( name: str, base: dict[str, Any], @@ -926,6 +970,24 @@ def _formatter_output_texts(row: dict[str, Any]) -> dict[str, str]: } +def _pair_formatter_output_texts(pair: dict[str, Any]) -> dict[str, str]: + texts: dict[str, str] = {} + for target in ("softcore", "hardcore"): + formatted = _format_metadata(pair, target) + texts[f"{target}.krea"] = str( + formatted["krea"].get(f"krea_{target}_prompt") + or formatted["krea"].get("krea_prompt") + or "" + ) + texts[f"{target}.sdxl"] = str( + formatted["sdxl"].get(f"sdxl_{target}_prompt") + or formatted["sdxl"].get("sdxl_prompt") + or "" + ) + texts[f"{target}.caption"] = str(formatted["caption"].get("natural_caption") or "") + return texts + + def _seed_determinism_check(seed: int) -> dict[str, Any]: first = _seed_probe_row(seed) second = _seed_probe_row(seed) @@ -942,6 +1004,22 @@ def _seed_determinism_check(seed: int) -> dict[str, Any]: } +def _pair_seed_determinism_check(seed: int) -> dict[str, Any]: + first = _pair_seed_probe(seed) + second = _pair_seed_probe(seed) + issues: list[str] = [] + if first != second: + issues.append("locked seed config did not reproduce identical pair metadata") + if _pair_formatter_output_texts(first) != _pair_formatter_output_texts(second): + issues.append("locked seed config did not reproduce identical pair formatter outputs") + return { + "name": "pair_seed.locked_determinism", + "base": _row_summary(first.get("hardcore_row") or {}), + "changed": False, + "issues": issues, + } + + def _seed_reroll_check( seed: int, *, @@ -982,6 +1060,51 @@ def _seed_reroll_check( } +def _pair_seed_pose_reroll_check(seed: int) -> dict[str, Any]: + name = "pair_seed.pose_reroll" + base = _pair_seed_probe(seed) + base_snapshot = _pair_seed_snapshot(base) + changed = False + changed_seed = None + changed_field_names: list[str] = [] + issues: list[str] = [] + stable_fields = ( + "shared_cast_descriptors", + "soft_cast_descriptor_text", + "hard_cast_descriptor_text", + "soft_scene_text", + "hard_scene_text", + "soft_item", + "soft_composition", + "hard_composition", + ) + changed_fields = ("hard_position_key", "hard_item", "hard_source_role_graph") + for reroll_seed in range(seed + 1, seed + 16): + rerolled = _pair_seed_probe(seed, reroll_axis="pose", reroll_seed=reroll_seed) + rerolled_snapshot = _pair_seed_snapshot(rerolled) + field_issues = _same_fields_issues(name, base_snapshot, rerolled_snapshot, stable_fields, reroll_seed) + if field_issues: + issues.extend(field_issues) + break + changed_field_names = [ + field for field in changed_fields if base_snapshot.get(field) != rerolled_snapshot.get(field) + ] + if changed_field_names: + changed = True + changed_seed = reroll_seed + break + if not changed: + issues.append("pair pose reroll did not change hard_position_key, hard_item, or hard_source_role_graph within 15 attempts") + return { + "name": name, + "base": _row_summary(base.get("hardcore_row") or {}), + "changed": changed, + "changed_seed": changed_seed, + "changed_fields": changed_field_names, + "issues": issues, + } + + def _seed_axis_checks(seed: int) -> list[dict[str, Any]]: return [ _seed_determinism_check(seed), @@ -1018,6 +1141,13 @@ def _seed_axis_checks(seed: int) -> list[dict[str, Any]]: ] +def _pair_seed_checks(seed: int) -> list[dict[str, Any]]: + return [ + _pair_seed_determinism_check(seed), + _pair_seed_pose_reroll_check(seed), + ] + + def _route_family_coverage_check( name: str, *, @@ -1100,6 +1230,7 @@ def run_simulation(seed: int = 3901, *, include_prompts: bool = False) -> dict[s cases.extend(_pair_reports("insta_pair.pov_outercourse", pov_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) issues = [ {"case": case["name"], "issue": issue} for case in cases @@ -1115,18 +1246,25 @@ def run_simulation(seed: int = 3901, *, include_prompts: bool = False) -> dict[s for check in axis_checks for issue in check.get("issues", []) ) + issues.extend( + {"case": check["name"], "issue": issue} + for check in pair_seed_checks + for issue in check.get("issues", []) + ) return { "summary": { "seed": seed, "cases": len(cases), "coverage_checks": len(coverage_checks), "axis_checks": len(axis_checks), + "pair_seed_checks": len(pair_seed_checks), "issues": len(issues), }, "issues": issues, "cases": cases, "coverage_checks": coverage_checks, "axis_checks": axis_checks, + "pair_seed_checks": pair_seed_checks, } @@ -1135,7 +1273,8 @@ def _print_text_report(report: dict[str, Any]) -> None: print( f"Prompt route simulation: seed={summary.get('seed')} " f"cases={summary.get('cases')} coverage_checks={summary.get('coverage_checks')} " - f"axis_checks={summary.get('axis_checks')} issues={summary.get('issues')}" + f"axis_checks={summary.get('axis_checks')} pair_seed_checks={summary.get('pair_seed_checks')} " + f"issues={summary.get('issues')}" ) for case in report.get("cases") or []: summary_text = case.get("summary") or {} @@ -1154,6 +1293,10 @@ def _print_text_report(report: dict[str, Any]) -> None: print(f"- {check.get('name')}: changed={check.get('changed')}") for issue in check.get("issues") or []: print(f" ISSUE {issue}") + for check in report.get("pair_seed_checks") or []: + print(f"- {check.get('name')}: changed={check.get('changed')}") + for issue in check.get("issues") or []: + print(f" ISSUE {issue}") def main(argv: list[str] | None = None) -> int: diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 4747423..76396d8 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -7918,6 +7918,7 @@ def smoke_prompt_route_simulation_policy() -> None: _expect(summary.get("cases") == 14, "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") == 2, "Prompt route simulation lost pair seed check coverage") _expect(summary.get("issues") == 0, f"Prompt route simulation reported issues: {report.get('issues')}") cases = {case.get("name"): case for case in report.get("cases") or []} for route_name in ( @@ -7979,6 +7980,19 @@ def smoke_prompt_route_simulation_policy() -> None: "seed_axis.composition_reroll", ): _expect(axis_checks[check_name].get("changed") is True, f"{check_name} should prove its axis can reroll") + pair_seed_checks = {check.get("name"): check for check in report.get("pair_seed_checks") or []} + for check_name in ("pair_seed.locked_determinism", "pair_seed.pose_reroll"): + check = pair_seed_checks.get(check_name) or {} + _expect(check, f"Prompt route simulation lost pair seed check {check_name}") + _expect(not check.get("issues"), f"Prompt route simulation pair seed check reported issues: {check_name}") + _expect( + pair_seed_checks["pair_seed.locked_determinism"].get("changed") is False, + "Pair locked determinism check should not be a reroll", + ) + _expect( + pair_seed_checks["pair_seed.pose_reroll"].get("changed") is True, + "Pair pose reroll should prove hard action can reroll while soft/cast/scene axes stay locked", + ) def smoke_node_camera_registration() -> None: