Validate pair seed simulation behavior

This commit is contained in:
2026-06-27 19:34:43 +02:00
parent a50b9272fe
commit 098721504d
5 changed files with 173 additions and 1 deletions
@@ -26,6 +26,8 @@ The map audit currently sees:
- Route simulation family coverage, so representative generated rows exercise - Route simulation family coverage, so representative generated rows exercise
every registered action and position family except documented special cases every registered action and position family except documented special cases
covered by dedicated smoke fixtures. 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 ## Architectural Finding
@@ -643,6 +645,8 @@ Near-term:
scene/camera/clothing fields. scene/camera/clothing fields.
- Keep same-room pair continuity synchronized in both assembled prompt text and - Keep same-room pair continuity synchronized in both assembled prompt text and
`hardcore_row.scene_text`; `tools/prompt_smoke.py` covers this drift case. `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: Medium-term:
+3
View File
@@ -1005,6 +1005,9 @@ issues for:
the primary `position_key`; the primary `position_key`;
- route-family coverage for registered action and position families, excluding - route-family coverage for registered action and position families, excluding
only documented special cases that have their own smoke fixtures; 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 - pose-axis rerolls changing cast/scene metadata or failing to move pose/action
metadata. metadata.
+8
View File
@@ -90,6 +90,14 @@ AUDIT_DOC_SNIPPETS: tuple[tuple[str, str], ...] = (
"docs/prompt-pool-routing-map.md", "docs/prompt-pool-routing-map.md",
"route-family coverage for registered action and position families", "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, ...] = ( PROMPT_ROW_READ_SCAN_GLOBS: tuple[str, ...] = (
+144 -1
View File
@@ -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]: def _seed_probe_row(seed: int, *, reroll_axis: str = "none", reroll_seed: int = -1) -> dict[str, Any]:
return pb.build_prompt( return pb.build_prompt(
category="Hardcore sexual poses", 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( def _same_fields_issues(
name: str, name: str,
base: dict[str, Any], 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]: def _seed_determinism_check(seed: int) -> dict[str, Any]:
first = _seed_probe_row(seed) first = _seed_probe_row(seed)
second = _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( def _seed_reroll_check(
seed: int, 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]]: def _seed_axis_checks(seed: int) -> list[dict[str, Any]]:
return [ return [
_seed_determinism_check(seed), _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( def _route_family_coverage_check(
name: str, 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)) cases.extend(_pair_reports("insta_pair.pov_outercourse", pov_pair, include_prompts=include_prompts))
coverage_checks = _route_family_coverage_checks(cases) coverage_checks = _route_family_coverage_checks(cases)
axis_checks = _seed_axis_checks(seed + 3) axis_checks = _seed_axis_checks(seed + 3)
pair_seed_checks = _pair_seed_checks(seed + 4)
issues = [ issues = [
{"case": case["name"], "issue": issue} {"case": case["name"], "issue": issue}
for case in cases 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 check in axis_checks
for issue in check.get("issues", []) 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 { return {
"summary": { "summary": {
"seed": seed, "seed": seed,
"cases": len(cases), "cases": len(cases),
"coverage_checks": len(coverage_checks), "coverage_checks": len(coverage_checks),
"axis_checks": len(axis_checks), "axis_checks": len(axis_checks),
"pair_seed_checks": len(pair_seed_checks),
"issues": len(issues), "issues": len(issues),
}, },
"issues": issues, "issues": issues,
"cases": cases, "cases": cases,
"coverage_checks": coverage_checks, "coverage_checks": coverage_checks,
"axis_checks": axis_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( print(
f"Prompt route simulation: seed={summary.get('seed')} " f"Prompt route simulation: seed={summary.get('seed')} "
f"cases={summary.get('cases')} coverage_checks={summary.get('coverage_checks')} " 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 []: for case in report.get("cases") or []:
summary_text = case.get("summary") 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')}") print(f"- {check.get('name')}: changed={check.get('changed')}")
for issue in check.get("issues") or []: for issue in check.get("issues") or []:
print(f" ISSUE {issue}") 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: def main(argv: list[str] | None = None) -> int:
+14
View File
@@ -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("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("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("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')}") _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 []} cases = {case.get("name"): case for case in report.get("cases") or []}
for route_name in ( for route_name in (
@@ -7979,6 +7980,19 @@ def smoke_prompt_route_simulation_policy() -> None:
"seed_axis.composition_reroll", "seed_axis.composition_reroll",
): ):
_expect(axis_checks[check_name].get("changed") is True, f"{check_name} should prove its axis can 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: def smoke_node_camera_registration() -> None: