diff --git a/tools/prompt_route_simulation.py b/tools/prompt_route_simulation.py index 3110be8..bc1f45f 100644 --- a/tools/prompt_route_simulation.py +++ b/tools/prompt_route_simulation.py @@ -93,6 +93,30 @@ def _character_cast(*, pov_man: bool = False) -> str: )["character_cast"] +def _random_character_cast() -> str: + cast = pb.build_character_slot_json( + subject_type="woman", + label="A", + age="random", + ethnicity="random", + figure="random", + body="random", + hair_color="random", + hair_length="random", + hair_style="random", + descriptor_detail="full", + )["character_cast"] + return pb.build_character_slot_json( + subject_type="man", + label="A", + age="random", + ethnicity="random", + body="random", + descriptor_detail="compact", + character_cast=cast, + )["character_cast"] + + def _coworking_location_config() -> str: return pb.build_location_pool_json( enabled=True, @@ -106,6 +130,32 @@ def _coworking_location_config() -> str: ) +def _seed_probe_location_config() -> str: + return pb.build_location_pool_json( + enabled=True, + combine_mode="replace", + preset="custom_only", + custom_locations=( + "seed_coworking_desk: coworking desk row with warm lamps and laptops\n" + "seed_coworking_glass: coworking glass office with plants and partition seams\n" + "seed_coworking_windows: coworking window lounge with repeated desks and city light" + ), + ) + + +def _seed_probe_composition_config() -> str: + return pb.build_composition_pool_json( + enabled=True, + combine_mode="replace", + preset="custom_only", + custom_compositions=( + "seed composition near a desk edge\n" + "seed composition through a glass partition\n" + "seed composition down repeating desk rows" + ), + ) + + def _orbit_camera(horizontal_angle: int = 45, vertical_angle: int = 0, zoom: float = 6.0) -> str: return pb.build_camera_orbit_config_json( enabled=True, @@ -602,8 +652,8 @@ def _insta_pair_case(seed: int, *, pov: bool, position: str, focus: str, family: ) -def _seed_axis_check(seed: int) -> dict[str, Any]: - base = pb.build_prompt( +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", subcategory="Penetrative sex", row_number=1, @@ -622,64 +672,146 @@ def _seed_axis_check(seed: int) -> dict[str, Any]: prepend_trigger_to_prompt=True, extra_positive="", extra_negative="", - seed_config=pb.build_seed_lock_config_json(base_seed=seed), + seed_config=pb.build_seed_lock_config_json(base_seed=seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed), women_count=1, men_count=1, - character_cast=_character_cast(), + character_cast=_random_character_cast(), hardcore_position_config=_position_filter("penetration_only", "penetration", ["missionary", "doggy", "cowgirl"]), - location_config=_coworking_location_config(), + location_config=_seed_probe_location_config(), + composition_config=_seed_probe_composition_config(), + expression_intensity=0.75, ) - changed = False - mismatches: list[str] = [] - for reroll_seed in range(seed + 1, seed + 10): - rerolled = pb.build_prompt( - category="Hardcore sexual poses", - subcategory="Penetrative sex", - row_number=1, - start_index=1, - seed=seed, - clothing="random", - ethnicity="any", - poses="random", - backside_bias=0.0, - figure="random", - no_plus_women=False, - no_black=False, - minimal_clothing_ratio=-1, - standard_pose_ratio=-1, - trigger=TRIGGER, - prepend_trigger_to_prompt=True, - extra_positive="", - extra_negative="", - seed_config=pb.build_seed_lock_config_json(base_seed=seed, reroll_axis="pose", reroll_seed=reroll_seed), - women_count=1, - men_count=1, - character_cast=_character_cast(), - hardcore_position_config=_position_filter("penetration_only", "penetration", ["missionary", "doggy", "cowgirl"]), - location_config=_coworking_location_config(), - ) - if rerolled.get("cast_descriptor_text") != base.get("cast_descriptor_text"): - mismatches.append(f"cast changed on pose reroll {reroll_seed}") - if rerolled.get("scene_text") != base.get("scene_text"): - mismatches.append(f"scene changed on pose reroll {reroll_seed}") - if ( - rerolled.get("position_key") != base.get("position_key") - or rerolled.get("source_role_graph") != base.get("source_role_graph") - or rerolled.get("item") != base.get("item") - ): - changed = True - break - issues = list(mismatches) - if not changed: - issues.append("pose reroll did not change pose/action metadata within 9 attempts") + + +def _seed_probe_snapshot(row: dict[str, Any]) -> dict[str, Any]: return { - "name": "seed_axis.pose_reroll", - "base": _row_summary(base), - "changed": changed, + "cast_descriptor_text": row.get("cast_descriptor_text"), + "scene": row.get("scene"), + "scene_text": row.get("scene_text"), + "position_key": row.get("position_key"), + "position_keys": row.get("position_keys") or [], + "item": row.get("item"), + "source_role_graph": row.get("source_role_graph"), + "character_expression_text": row.get("character_expression_text"), + "composition": row.get("composition"), + } + + +def _same_fields_issues( + name: str, + base: dict[str, Any], + rerolled: dict[str, Any], + fields: tuple[str, ...], + reroll_seed: int, +) -> list[str]: + return [ + f"{name}: stable_field_changed:{field}:reroll_seed={reroll_seed}" + for field in fields + if base.get(field) != rerolled.get(field) + ] + + +def _formatter_output_texts(row: dict[str, Any]) -> dict[str, str]: + formatted = _format_metadata(row, "single") + return { + "krea": str(formatted["krea"].get("krea_prompt") or ""), + "sdxl": str(formatted["sdxl"].get("sdxl_prompt") or ""), + "caption": str(formatted["caption"].get("natural_caption") or ""), + } + + +def _seed_determinism_check(seed: int) -> dict[str, Any]: + first = _seed_probe_row(seed) + second = _seed_probe_row(seed) + issues: list[str] = [] + if first != second: + issues.append("locked seed config did not reproduce identical row metadata") + if _formatter_output_texts(first) != _formatter_output_texts(second): + issues.append("locked seed config did not reproduce identical formatter outputs") + return { + "name": "seed_axis.locked_determinism", + "base": _row_summary(first), + "changed": False, "issues": issues, } +def _seed_reroll_check( + seed: int, + *, + reroll_axis: str, + changed_fields: tuple[str, ...], + stable_fields: tuple[str, ...], +) -> dict[str, Any]: + name = f"seed_axis.{reroll_axis}_reroll" + base = _seed_probe_row(seed) + base_snapshot = _seed_probe_snapshot(base) + changed = False + changed_seed = None + changed_field_names: list[str] = [] + issues: list[str] = [] + for reroll_seed in range(seed + 1, seed + 16): + rerolled = _seed_probe_row(seed, reroll_axis=reroll_axis, reroll_seed=reroll_seed) + rerolled_snapshot = _seed_probe_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(f"{reroll_axis} reroll did not change {', '.join(changed_fields)} within 15 attempts") + return { + "name": name, + "base": _row_summary(base), + "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), + _seed_reroll_check( + seed, + reroll_axis="person", + changed_fields=("cast_descriptor_text",), + stable_fields=("scene_text", "position_key", "item", "source_role_graph", "character_expression_text", "composition"), + ), + _seed_reroll_check( + seed, + reroll_axis="scene", + changed_fields=("scene", "scene_text"), + stable_fields=("cast_descriptor_text", "position_key", "item", "source_role_graph", "character_expression_text", "composition"), + ), + _seed_reroll_check( + seed, + reroll_axis="pose", + changed_fields=("position_key", "item", "source_role_graph"), + stable_fields=("cast_descriptor_text", "scene_text", "character_expression_text", "composition"), + ), + _seed_reroll_check( + seed, + reroll_axis="expression", + changed_fields=("character_expression_text",), + stable_fields=("cast_descriptor_text", "scene_text", "position_key", "item", "source_role_graph", "composition"), + ), + _seed_reroll_check( + seed, + reroll_axis="composition", + changed_fields=("composition",), + stable_fields=("cast_descriptor_text", "scene_text", "position_key", "item", "source_role_graph", "character_expression_text"), + ), + ] + + def run_simulation(seed: int = 3901, *, include_prompts: bool = False) -> dict[str, Any]: cases: list[dict[str, Any]] = [] regular = _regular_single_case(seed) @@ -705,7 +837,7 @@ 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)) - axis_checks = [_seed_axis_check(seed + 3)] + axis_checks = _seed_axis_checks(seed + 3) issues = [ {"case": case["name"], "issue": issue} for case in cases diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 29dae03..7c2f564 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -7834,7 +7834,7 @@ def smoke_prompt_route_simulation_policy() -> None: report = prompt_route_simulation.run_simulation(seed=3901, include_prompts=False) summary = report.get("summary") or {} _expect(summary.get("cases") == 11, "Prompt route simulation case count changed unexpectedly") - _expect(summary.get("axis_checks") == 1, "Prompt route simulation lost axis check coverage") + _expect(summary.get("axis_checks") == 6, "Prompt route simulation lost axis 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 ( @@ -7856,6 +7856,27 @@ 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", ) + axis_checks = {check.get("name"): check for check in report.get("axis_checks") or []} + for check_name in ( + "seed_axis.locked_determinism", + "seed_axis.person_reroll", + "seed_axis.scene_reroll", + "seed_axis.pose_reroll", + "seed_axis.expression_reroll", + "seed_axis.composition_reroll", + ): + check = axis_checks.get(check_name) or {} + _expect(check, f"Prompt route simulation lost seed-axis check {check_name}") + _expect(not check.get("issues"), f"Prompt route simulation seed-axis check reported issues: {check_name}") + _expect(axis_checks["seed_axis.locked_determinism"].get("changed") is False, "Locked determinism check should not be a reroll") + for check_name in ( + "seed_axis.person_reroll", + "seed_axis.scene_reroll", + "seed_axis.pose_reroll", + "seed_axis.expression_reroll", + "seed_axis.composition_reroll", + ): + _expect(axis_checks[check_name].get("changed") is True, f"{check_name} should prove its axis can reroll") def smoke_node_camera_registration() -> None: