diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index d4e0192..860b3fd 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -228,6 +228,12 @@ Common trap: `row_number` participates in `seed_config.axis_rng`. If two workflows have the same seeds but different `row_number`, they are not expected to match. +Character `slot_seed` is more specific than the `person` axis. A fixed +`slot_seed` owns that slot's random age/body/appearance/hair choices, so +rerolling `person_seed` will not drift that character. Leave `slot_seed=-1` +when the slot should follow `person_seed` or the global seed like the fallback +cast generator. + Each generated row stores `generation_trace.seed_axes` in `metadata_json`. Use it to verify whether an axis followed the main seed or a configured seed, and to compare the exact per-axis RNG seed used for the row. diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 256a932..898f8c4 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -7458,6 +7458,70 @@ def smoke_seed_config_policy() -> None: deterministic_b = _prompt_row(**deterministic_kwargs) _expect(deterministic_a == deterministic_b, "locked seed config should reproduce identical prompt row output") + slot_descriptor_keys = ("age_band", "body_phrase", "skin", "hair", "eyes", "figure") + + def slot_seed_cast(slot_seed: int) -> str: + return pb.build_character_slot_json( + subject_type="woman", + label="A", + slot_seed=slot_seed, + age="random", + ethnicity="random", + figure="random", + body="random", + hair_color="random", + hair_length="random", + hair_style="random", + descriptor_detail="full", + )["character_cast"] + + def slot_seed_row(character_cast: str, reroll_seed: int) -> dict[str, Any]: + return _prompt_row( + name=f"seed_config_policy_slot_seed_{reroll_seed}", + category="Casual clothes", + subcategory="Casual clothes / Streetwear", + seed=41001, + seed_config=pb.build_seed_lock_config_json( + base_seed=41001, + reroll_axis="person", + reroll_seed=reroll_seed, + ), + character_cast=character_cast, + women_count=1, + men_count=0, + ) + + seeded_slot_cast = slot_seed_cast(50123) + seeded_slot_a = slot_seed_row(seeded_slot_cast, 60001) + seeded_slot_b = slot_seed_row(seeded_slot_cast, 60002) + _expect( + tuple(seeded_slot_a.get(key) for key in slot_descriptor_keys) + == tuple(seeded_slot_b.get(key) for key in slot_descriptor_keys), + "slot_seed should keep random character-slot descriptors stable across person-axis rerolls", + ) + seeded_trace_a = seeded_slot_a.get("generation_trace") if isinstance(seeded_slot_a.get("generation_trace"), dict) else {} + seeded_trace_b = seeded_slot_b.get("generation_trace") if isinstance(seeded_slot_b.get("generation_trace"), dict) else {} + _expect( + seeded_trace_a.get("seed_axes", {}).get("person", {}).get("seed") + != seeded_trace_b.get("seed_axes", {}).get("person", {}).get("seed"), + "slot_seed regression check should actually reroll the person axis", + ) + seeded_krea_a = krea_formatter.format_krea2_prompt("", metadata_json=_json(seeded_slot_a), target="single") + seeded_krea_b = krea_formatter.format_krea2_prompt("", metadata_json=_json(seeded_slot_b), target="single") + _expect( + seeded_krea_a.get("krea_prompt") == seeded_krea_b.get("krea_prompt"), + "slot_seed should keep final Krea prompt stable across person-axis rerolls", + ) + + unseeded_slot_cast = slot_seed_cast(-1) + unseeded_slot_a = slot_seed_row(unseeded_slot_cast, 60001) + unseeded_slot_b = slot_seed_row(unseeded_slot_cast, 60002) + _expect( + tuple(unseeded_slot_a.get(key) for key in slot_descriptor_keys) + != tuple(unseeded_slot_b.get(key) for key in slot_descriptor_keys), + "unseeded random character slot should still follow person-axis rerolls", + ) + pose_changed = False for reroll_seed in range(deterministic_seed + 1, deterministic_seed + 10): pose_reroll = _prompt_row(