Plan clothing seed axis implementation
This commit is contained in:
@@ -0,0 +1,854 @@
|
||||
# Clothing Seed Axis Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add a first-class `clothing` seed axis so workflows can keep content, pose, role, person, scene, expression, and composition identical while rerolling only clothing/outfit choices.
|
||||
|
||||
**Architecture:** Extend the shared seed policy first, then route clothing selections through `axis_rng(..., "clothing", ...)` in prompt and pair flows. Keep `content` responsible for content item/template choices, use `outfit_seed` as a clothing alias, and keep `content_seed` as a compatibility fallback only when no explicit clothing/outfit seed exists.
|
||||
|
||||
**Tech Stack:** Python 3, ComfyUI custom nodes, local smoke tests in `tools/prompt_smoke.py`, scene nodes in `node_scene.py`, seed policy in `seed_config.py`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
- Modify `seed_config.py`: add the `clothing` seed axis, aliases, reroll groups, lock config emission, and trace support through the existing generic functions.
|
||||
- Modify `prompt_builder.py`: expose optional `clothing_seed` and `clothing_seed_mode` through the wrapper around `seed_config.build_seed_config_json`.
|
||||
- Modify `node_seed_resolution.py`: expose clothing controls in `SxCPSeedControl` and let `SxCPSeedLocker` pick up the new reroll choices from `seed_config`.
|
||||
- Modify `node_tooltips.py`: add help text for the new manual clothing seed controls and update the seed-locker tooltip.
|
||||
- Modify `builder_prompt_route.py`: use a clothing RNG for prompt clothing mode selection.
|
||||
- Modify `pair_rows.py`: use a clothing RNG for primary softcore outfit selection in scene pairs.
|
||||
- Modify `pair_cast.py`: use a clothing RNG for secondary pair participant outfits.
|
||||
- Modify `node_scene.py`: map scene layer seed axes to `clothing`, `content_clothing`, and `clothing_pose`.
|
||||
- Modify `tools/prompt_smoke.py`: add red/green smoke coverage for seed vocabulary, UI inputs, routing, and scene-pair clothes-only rerolls.
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Shared Seed Vocabulary And UI Surface
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/prompt_smoke.py`
|
||||
- Modify: `seed_config.py`
|
||||
- Modify: `prompt_builder.py`
|
||||
- Modify: `node_seed_resolution.py`
|
||||
- Modify: `node_tooltips.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing seed vocabulary smoke tests**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_seed_config_policy()`, after the existing `normalize_reroll_axis("content pose")` assertion, add:
|
||||
|
||||
```python
|
||||
reroll_choices = pb.seed_reroll_axis_choices()
|
||||
for expected_axis in ("clothing", "content_clothing", "clothing_pose"):
|
||||
_expect(expected_axis in reroll_choices, f"seed reroll axis choices missing {expected_axis}")
|
||||
_expect(pb.normalize_reroll_axis("clothing pose") == "clothing_pose", "reroll axis normalizer should accept clothing pose")
|
||||
_expect(pb.normalize_reroll_axis("content clothing") == "content_clothing", "reroll axis normalizer should accept content clothing")
|
||||
```
|
||||
|
||||
In the same function, replace:
|
||||
|
||||
```python
|
||||
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "bad": "nope"})
|
||||
_expect(parsed == {"item_seed": 44, "pose_seed": 55}, "seed parser should keep integer-like values only")
|
||||
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
|
||||
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
parsed = pb._parse_seed_config({"item_seed": "44", "pose_seed": "55", "outfit_seed": "66", "bad": "nope"})
|
||||
_expect(
|
||||
parsed == {"item_seed": 44, "pose_seed": 55, "outfit_seed": 66},
|
||||
"seed parser should keep integer-like values only",
|
||||
)
|
||||
_expect(pb._configured_axis_seed(parsed, "content") == 44, "content axis should honor item_seed alias")
|
||||
_expect(pb._configured_axis_seed(parsed, "clothing") == 66, "clothing axis should honor outfit_seed alias")
|
||||
_expect(
|
||||
pb._configured_axis_seed({"content_seed": 77}, "clothing") == 77,
|
||||
"clothing axis should keep content_seed as a legacy fallback",
|
||||
)
|
||||
_expect(
|
||||
pb._configured_axis_seed({"content_seed": 77, "clothing_seed": 88}, "clothing") == 88,
|
||||
"clothing_seed should override legacy content_seed fallback",
|
||||
)
|
||||
_expect(pb._configured_axis_seed(parsed, "role") == 55, "role axis should honor pose seed alias")
|
||||
```
|
||||
|
||||
In the same function, after the existing `locked = json.loads(...)` block and its three assertions, add:
|
||||
|
||||
```python
|
||||
clothing_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing", reroll_seed=777))
|
||||
_expect(clothing_locked["clothing_seed"] == 777, "clothing reroll should alter clothing seed")
|
||||
_expect(clothing_locked["content_seed"] == 100, "clothing reroll should leave content locked")
|
||||
_expect(clothing_locked["pose_seed"] == 100 and clothing_locked["role_seed"] == 100, "clothing reroll should leave pose and role locked")
|
||||
|
||||
content_clothing_locked = json.loads(
|
||||
pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_clothing", reroll_seed=778)
|
||||
)
|
||||
_expect(content_clothing_locked["content_seed"] == 778, "content_clothing reroll should alter content seed")
|
||||
_expect(content_clothing_locked["clothing_seed"] == 778, "content_clothing reroll should alter clothing seed")
|
||||
_expect(content_clothing_locked["pose_seed"] == 100, "content_clothing reroll should leave pose locked")
|
||||
|
||||
clothing_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="clothing_pose", reroll_seed=779))
|
||||
_expect(clothing_pose_locked["clothing_seed"] == 779, "clothing_pose reroll should alter clothing seed")
|
||||
_expect(clothing_pose_locked["pose_seed"] == 779 and clothing_pose_locked["role_seed"] == 779, "clothing_pose reroll should alter pose and role seeds")
|
||||
_expect(clothing_pose_locked["content_seed"] == 100, "clothing_pose reroll should leave content locked")
|
||||
|
||||
content_pose_locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_pose", reroll_seed=780))
|
||||
_expect(content_pose_locked["clothing_seed"] == 100, "content_pose reroll should not alter clothing seed")
|
||||
```
|
||||
|
||||
Change the `axis_trace` test block from:
|
||||
|
||||
```python
|
||||
axis_trace = seed_config.axis_seed_trace({"content_seed": 44}, 99, 3, axes=("content", "scene"))
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
axis_trace = seed_config.axis_seed_trace({"content_seed": 44, "clothing_seed": 66}, 99, 3, axes=("content", "clothing", "scene"))
|
||||
```
|
||||
|
||||
Then add this assertion after the existing content seed assertions:
|
||||
|
||||
```python
|
||||
_expect(axis_trace["clothing"]["source"] == "configured", "Seed axis trace lost clothing configured source")
|
||||
_expect(axis_trace["clothing"]["seed"] == 66, "Seed axis trace lost configured clothing seed")
|
||||
```
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_node_utility_registration()`, after:
|
||||
|
||||
```python
|
||||
_expect("category_seed_mode" in seed_inputs, "Seed Control lost category seed mode input")
|
||||
```
|
||||
|
||||
add:
|
||||
|
||||
```python
|
||||
_expect("clothing_seed_mode" in seed_inputs, "Seed Control lost clothing seed mode input")
|
||||
_expect("clothing_seed" in seed_inputs, "Seed Control lost clothing seed input")
|
||||
```
|
||||
|
||||
After the `category_seed_tooltip` assertion, add:
|
||||
|
||||
```python
|
||||
clothing_seed_tooltip = node_tooltips._tooltip_for_input("SxCPSeedControl", "clothing_seed_mode")
|
||||
_expect("clothing/outfit" in clothing_seed_tooltip, "Node tooltip policy lost Seed Control clothing override")
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```python
|
||||
_expect(int(parsed_seed_control.get("content_seed", -1)) >= 0, "Seed Control random mode did not emit resolved seed")
|
||||
```
|
||||
|
||||
add this assertion after updating the call in Step 3:
|
||||
|
||||
```python
|
||||
_expect(parsed_seed_control.get("clothing_seed") == 222, "Seed Control fixed clothing seed changed")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused smoke tests and verify they fail for the new behavior**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with `seed reroll axis choices missing clothing`.
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with `Seed Control lost clothing seed mode input`.
|
||||
|
||||
- [ ] **Step 3: Implement the shared seed axis**
|
||||
|
||||
In `seed_config.py`, update the top-level seed definitions:
|
||||
|
||||
```python
|
||||
SEED_AXIS_SALTS = {
|
||||
"category": 31,
|
||||
"subcategory": 37,
|
||||
"content": 41,
|
||||
"clothing": 41,
|
||||
"person": 43,
|
||||
"scene": 47,
|
||||
"pose": 53,
|
||||
"role": 57,
|
||||
"expression": 59,
|
||||
"composition": 61,
|
||||
}
|
||||
|
||||
SEED_AXIS_ALIASES = {
|
||||
"category": ("category_seed", "category"),
|
||||
"subcategory": ("subcategory_seed", "subcategory"),
|
||||
"content": ("content_seed", "item_seed", "sexual_pose_seed", "content"),
|
||||
"clothing": ("clothing_seed", "outfit_seed", "wardrobe_seed", "content_seed", "content"),
|
||||
"person": ("person_seed", "appearance_seed", "cast_seed", "person"),
|
||||
"scene": ("scene_seed", "scene"),
|
||||
"pose": ("pose_seed", "sexual_pose_seed", "pose"),
|
||||
"role": ("role_seed", "role", "pose_seed", "sexual_pose_seed"),
|
||||
"expression": ("expression_seed", "face_seed", "expression"),
|
||||
"composition": ("composition_seed", "camera_seed", "composition"),
|
||||
}
|
||||
|
||||
SEED_LOCK_AXES = (
|
||||
"category",
|
||||
"subcategory",
|
||||
"content",
|
||||
"clothing",
|
||||
"person",
|
||||
"scene",
|
||||
"pose",
|
||||
"role",
|
||||
"expression",
|
||||
"composition",
|
||||
)
|
||||
```
|
||||
|
||||
In the same file, update `SEED_REROLL_GROUPS`:
|
||||
|
||||
```python
|
||||
SEED_REROLL_GROUPS = {
|
||||
"none": (),
|
||||
"category": ("category",),
|
||||
"subcategory": ("subcategory",),
|
||||
"content": ("content",),
|
||||
"clothing": ("clothing",),
|
||||
"person": ("person",),
|
||||
"scene": ("scene",),
|
||||
"pose": ("pose", "role"),
|
||||
"role": ("role",),
|
||||
"expression": ("expression",),
|
||||
"composition": ("composition",),
|
||||
"content_pose": ("content", "pose", "role"),
|
||||
"content_clothing": ("content", "clothing"),
|
||||
"clothing_pose": ("clothing", "pose", "role"),
|
||||
"scene_pose": ("scene", "pose", "role"),
|
||||
}
|
||||
```
|
||||
|
||||
Update `normalize_reroll_axis()` aliases:
|
||||
|
||||
```python
|
||||
aliases = {
|
||||
"contentpose": "content_pose",
|
||||
"contentclothing": "content_clothing",
|
||||
"clothingpose": "clothing_pose",
|
||||
"scenepose": "scene_pose",
|
||||
}
|
||||
```
|
||||
|
||||
Update `build_seed_config_json()` in `seed_config.py` by adding parameters between `content_seed` and `person_seed`:
|
||||
|
||||
```python
|
||||
clothing_seed: int = -1,
|
||||
```
|
||||
|
||||
and between `content_seed_mode` and `person_seed_mode`:
|
||||
|
||||
```python
|
||||
clothing_seed_mode: str = "auto",
|
||||
```
|
||||
|
||||
Then add the emitted field after `content_seed`:
|
||||
|
||||
```python
|
||||
"clothing_seed": axis_seed(clothing_seed, clothing_seed_mode),
|
||||
```
|
||||
|
||||
In `prompt_builder.py`, update `build_seed_config_json()` with the same optional `clothing_seed` and `clothing_seed_mode` parameters, and pass them to `seed_policy.build_seed_config_json(...)`:
|
||||
|
||||
```python
|
||||
clothing_seed=clothing_seed,
|
||||
clothing_seed_mode=clothing_seed_mode,
|
||||
```
|
||||
|
||||
In `node_seed_resolution.py`, update `SxCPSeedControl.SEED_AXES`:
|
||||
|
||||
```python
|
||||
SEED_AXES = (
|
||||
"category",
|
||||
"subcategory",
|
||||
"content",
|
||||
"clothing",
|
||||
"person",
|
||||
"scene",
|
||||
"pose",
|
||||
"role",
|
||||
"expression",
|
||||
"composition",
|
||||
)
|
||||
```
|
||||
|
||||
Update `SxCPSeedControl.build(...)` by adding `clothing_seed_mode, clothing_seed` after `content_seed`, and pass both into `build_seed_config_json(...)`:
|
||||
|
||||
```python
|
||||
clothing_seed=clothing_seed,
|
||||
clothing_seed_mode=clothing_seed_mode,
|
||||
```
|
||||
|
||||
In `tools/prompt_smoke.py`, update the `seed_control().build(...)` call in `smoke_node_utility_registration()` by inserting these two arguments after the current content seed pair:
|
||||
|
||||
```python
|
||||
"fixed",
|
||||
222,
|
||||
```
|
||||
|
||||
In `node_tooltips.py`, update `NODE_INPUT_TOOLTIPS["SxCPSeedControl"]` with:
|
||||
|
||||
```python
|
||||
"clothing_seed_mode": "Controls clothing/outfit selection separately from content item selection.",
|
||||
"clothing_seed": "Seed used when clothing_seed_mode is fixed or auto with a non-negative value.",
|
||||
```
|
||||
|
||||
Update `NODE_INPUT_TOOLTIPS["SxCPSeedLocker"]["reroll_axis"]` to mention clothing:
|
||||
|
||||
```python
|
||||
"reroll_axis": "Choose the one axis to change while the rest stays locked. Use clothing for outfit-only rerolls, pose for sexual pose, scene for location, person for appearance.",
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused smoke tests and verify they pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 5: Commit Task 1**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. add seed_config.py prompt_builder.py node_seed_resolution.py node_tooltips.py tools/prompt_smoke.py
|
||||
git --git-dir=.git-real --work-tree=. commit -m "Add clothing seed axis vocabulary"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Route Prompt And Pair Clothing Through The Clothing Axis
|
||||
|
||||
**Files:**
|
||||
- Modify: `tools/prompt_smoke.py`
|
||||
- Modify: `builder_prompt_route.py`
|
||||
- Modify: `pair_rows.py`
|
||||
- Modify: `pair_cast.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing prompt-routing smoke test**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_seed_config_policy()`, after the existing `pose_changed` assertion block, add:
|
||||
|
||||
```python
|
||||
clothes_base_seed = 52001
|
||||
clothes_base_config = json.loads(pb.build_seed_lock_config_json(base_seed=clothes_base_seed))
|
||||
|
||||
def clothes_row(clothing_seed: int) -> dict[str, Any]:
|
||||
seed_config_for_row = dict(clothes_base_config)
|
||||
seed_config_for_row["clothing_seed"] = clothing_seed
|
||||
return _prompt_row(
|
||||
name=f"seed_config_policy_clothing_seed_{clothing_seed}",
|
||||
category="woman",
|
||||
subcategory="",
|
||||
seed=clothes_base_seed,
|
||||
seed_config=seed_config_for_row,
|
||||
clothing="random",
|
||||
minimal_clothing_ratio=0.5,
|
||||
character_cast=_character_cast(),
|
||||
location_config=_coworking_location_config(),
|
||||
)
|
||||
|
||||
clothes_locked_a = clothes_row(53001)
|
||||
clothes_changed = False
|
||||
for clothing_seed in range(53002, 53100):
|
||||
clothes_candidate = clothes_row(clothing_seed)
|
||||
_expect(
|
||||
clothes_candidate.get("scene_text") == clothes_locked_a.get("scene_text"),
|
||||
"clothing reroll should keep scene text stable",
|
||||
)
|
||||
_expect(
|
||||
clothes_candidate.get("pose") == clothes_locked_a.get("pose"),
|
||||
"clothing reroll should keep pose stable",
|
||||
)
|
||||
_expect(
|
||||
clothes_candidate.get("cast_descriptor_text") == clothes_locked_a.get("cast_descriptor_text"),
|
||||
"clothing reroll should keep cast descriptors stable",
|
||||
)
|
||||
if clothes_candidate.get("clothing") != clothes_locked_a.get("clothing"):
|
||||
clothes_changed = True
|
||||
break
|
||||
_expect(clothes_changed, "clothing_seed reroll should change prompt clothing mode")
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run the focused smoke test and verify it fails for the current routing**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with `clothing_seed reroll should change prompt clothing mode`.
|
||||
|
||||
- [ ] **Step 3: Implement clothing RNG routing in normal prompt rows**
|
||||
|
||||
In `builder_prompt_route.py`, inside `build_prompt_result(...)`, replace:
|
||||
|
||||
```python
|
||||
content_rng = deps.axis_rng(parsed_seed_config, "content", seed, row_number)
|
||||
pose_axis_rng = deps.axis_rng(parsed_seed_config, "pose", seed, row_number)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
content_rng = deps.axis_rng(parsed_seed_config, "content", seed, row_number)
|
||||
clothing_rng = deps.axis_rng(parsed_seed_config, "clothing", seed, row_number)
|
||||
pose_axis_rng = deps.axis_rng(parsed_seed_config, "pose", seed, row_number)
|
||||
```
|
||||
|
||||
Then replace:
|
||||
|
||||
```python
|
||||
clothing = deps.pick_clothing_mode(content_rng, clothing, minimal_ratio)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
clothing = deps.pick_clothing_mode(clothing_rng, clothing, minimal_ratio)
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run the focused smoke test and verify it passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 5: Write the failing pair-routing smoke tests**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_node_scene_chain_registration()`, find:
|
||||
|
||||
```python
|
||||
soft_content_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"fixed",
|
||||
6679,
|
||||
"content",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
```
|
||||
|
||||
Replace it with:
|
||||
|
||||
```python
|
||||
soft_clothing_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"fixed",
|
||||
6679,
|
||||
"clothing",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
```
|
||||
|
||||
In the same block, replace `seed_options=soft_content_seed_options` with:
|
||||
|
||||
```python
|
||||
seed_options=soft_clothing_seed_options,
|
||||
```
|
||||
|
||||
Update the expected failure message from:
|
||||
|
||||
```python
|
||||
"Scene softcore branch content seed fixture no longer selects the expected outfit",
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```python
|
||||
"Scene softcore branch clothing seed fixture no longer selects the expected outfit",
|
||||
```
|
||||
|
||||
After the existing `soft_pose_pair` assertions and before the choice-board block, add:
|
||||
|
||||
```python
|
||||
def _soft_clothing_pair(soft_clothing_seed: int) -> dict[str, Any]:
|
||||
soft_clothing_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build(
|
||||
"softcore_branch",
|
||||
"fixed",
|
||||
soft_clothing_seed,
|
||||
"clothing",
|
||||
"same_for_all_rows",
|
||||
"replace_layer",
|
||||
)[0]
|
||||
soft_scene_clothing, hard_scene_clothing, _summary, _metadata = nodes["SxCPSceneBranchPair"]().build(
|
||||
scene,
|
||||
"same_creator_same_room",
|
||||
"hybrid",
|
||||
branch_options=branch_options,
|
||||
seed_options=soft_clothing_seed_options,
|
||||
)
|
||||
soft_scene_clothing = nodes["SxCPSoftcoreBranchOptions"]().build(
|
||||
soft_scene_clothing,
|
||||
"same_as_hardcore",
|
||||
"lingerie_tease",
|
||||
True,
|
||||
0.45,
|
||||
"from_camera_config",
|
||||
"compact",
|
||||
"",
|
||||
branch_options=branch_options,
|
||||
seed_options=soft_clothing_seed_options,
|
||||
)[0]
|
||||
hard_scene_clothing = nodes["SxCPHardcoreBranchOptions"]().build(
|
||||
hard_scene_clothing,
|
||||
"couple",
|
||||
1,
|
||||
1,
|
||||
"hardcore",
|
||||
True,
|
||||
0.85,
|
||||
"partially_removed",
|
||||
"from_camera_config",
|
||||
"compact",
|
||||
"balanced",
|
||||
"",
|
||||
branch_options=branch_options,
|
||||
)[0]
|
||||
return json.loads(nodes["SxCPScenePairOutput"]().build(soft_scene_clothing, hard_scene_clothing)[7])
|
||||
|
||||
soft_clothing_pairs = [_soft_clothing_pair(seed) for seed in (6677, 6678, 6679, 6680)]
|
||||
soft_clothing_items = {pair.get("softcore_row", {}).get("item") for pair in soft_clothing_pairs}
|
||||
soft_clothing_poses = {pair.get("softcore_row", {}).get("pose") for pair in soft_clothing_pairs}
|
||||
soft_clothing_hard_states = {pair.get("hardcore_clothing_state") for pair in soft_clothing_pairs}
|
||||
_expect(len(soft_clothing_items) > 1, "Softcore branch clothing reroll should change softcore outfit")
|
||||
_expect(len(soft_clothing_hard_states) > 1, "Softcore branch clothing reroll should change inherited hard clothing")
|
||||
_expect(len(soft_clothing_poses) == 1, "Softcore branch clothing reroll should keep softcore pose stable")
|
||||
for expected_seed, clothing_pair in zip((6677, 6678, 6679, 6680), soft_clothing_pairs):
|
||||
soft_seed_config = clothing_pair.get("softcore_row", {}).get("seed_config") if isinstance(clothing_pair.get("softcore_row"), dict) else {}
|
||||
hard_seed_config = clothing_pair.get("hardcore_row", {}).get("seed_config") if isinstance(clothing_pair.get("hardcore_row"), dict) else {}
|
||||
_expect(
|
||||
soft_seed_config.get("clothing_seed") == expected_seed,
|
||||
"Softcore branch clothing seed did not reach softcore generator seed config",
|
||||
)
|
||||
_expect(
|
||||
soft_seed_config.get("content_seed") != expected_seed,
|
||||
"Softcore branch clothing seed should not overwrite content seed",
|
||||
)
|
||||
_expect(
|
||||
hard_seed_config.get("clothing_seed") != expected_seed,
|
||||
"Softcore branch clothing seed leaked into hardcore generator seed config",
|
||||
)
|
||||
```
|
||||
|
||||
In the `content_pair` assertions, add:
|
||||
|
||||
```python
|
||||
_expect(
|
||||
content_hard_seed_config.get("clothing_seed") != 8899,
|
||||
"Hardcore branch content_pose reroll should not reach hardcore clothing seed",
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 6: Run the scene-chain smoke test and verify it fails for the missing scene axis/routing**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with either `Scene softcore branch clothing seed fixture no longer selects the expected outfit` or `Softcore branch clothing reroll should change softcore outfit`.
|
||||
|
||||
- [ ] **Step 7: Implement clothing RNG routing in pair rows**
|
||||
|
||||
In `pair_rows.py`, inside `build_insta_pair_rows_result(...)`, replace:
|
||||
|
||||
```python
|
||||
soft_content_rng = axis_rng(soft_seed_config, "content", seed, row_number + 311)
|
||||
soft_pose_rng = axis_rng(soft_seed_config, "pose", seed, row_number + 313)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
soft_content_rng = axis_rng(soft_seed_config, "content", seed, row_number + 311)
|
||||
soft_clothing_rng = axis_rng(soft_seed_config, "clothing", seed, row_number + 311)
|
||||
soft_pose_rng = axis_rng(soft_seed_config, "pose", seed, row_number + 313)
|
||||
```
|
||||
|
||||
Then replace:
|
||||
|
||||
```python
|
||||
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_content_rng)
|
||||
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_content_rng, softcore_level_key)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
primary_softcore_outfit = slot_softcore_outfit(primary_slot, soft_clothing_rng)
|
||||
soft_row["item"] = primary_softcore_outfit or softcore_outfit(soft_clothing_rng, softcore_level_key)
|
||||
```
|
||||
|
||||
In `pair_cast.py`, inside `softcore_partner_styling(...)`, replace:
|
||||
|
||||
```python
|
||||
content_rng = axis_rng(seed_config, "content", seed, row_number + 421)
|
||||
pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421)
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
content_rng = axis_rng(seed_config, "content", seed, row_number + 421)
|
||||
clothing_rng = axis_rng(seed_config, "clothing", seed, row_number + 421)
|
||||
pose_rng = axis_rng(seed_config, "pose", seed, row_number + 421)
|
||||
```
|
||||
|
||||
Then replace both `slot_softcore_outfit(..., content_rng)` calls with `slot_softcore_outfit(..., clothing_rng)`, and replace both outfit `choose(content_rng, ...)` calls with `choose(clothing_rng, ...)`.
|
||||
|
||||
- [ ] **Step 8: Run focused smoke tests and verify Task 2 behavior passes**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected at this point: FAIL only on missing scene layer mapping, with a message involving the softcore branch clothing seed.
|
||||
|
||||
- [ ] **Step 9: Commit Task 2**
|
||||
|
||||
If `seed_config_policy` passes and `node_scene_chain_registration` now fails only because scene layer axes do not apply `clothing`, commit the prompt and pair routing work:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. add builder_prompt_route.py pair_rows.py pair_cast.py tools/prompt_smoke.py
|
||||
git --git-dir=.git-real --work-tree=. commit -m "Route clothing choices through clothing seed"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Scene Layer Clothing Axis Mapping
|
||||
|
||||
**Files:**
|
||||
- Modify: `node_scene.py`
|
||||
- Modify: `tools/prompt_smoke.py`
|
||||
|
||||
- [ ] **Step 1: Write the failing scene-axis smoke assertions**
|
||||
|
||||
In `tools/prompt_smoke.py`, inside `smoke_node_scene_chain_registration()`, replace:
|
||||
|
||||
```python
|
||||
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "content", "same_for_all_rows", "replace_layer")[0]
|
||||
```
|
||||
|
||||
with:
|
||||
|
||||
```python
|
||||
wardrobe_seed_options = nodes["SxCPSceneLayerSeedOptions"]().build("wardrobe", "fixed", 9981, "clothing", "same_for_all_rows", "replace_layer")[0]
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```python
|
||||
_expect(json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("seed") == 9981, "Scene Wardrobe seed options did not write seed trace")
|
||||
```
|
||||
|
||||
add:
|
||||
|
||||
```python
|
||||
_expect(
|
||||
json.loads(scene).get("seed_trace", {}).get("wardrobe", {}).get("axes") == ["clothing"],
|
||||
"Scene Wardrobe seed options should target clothing axis",
|
||||
)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run scene-chain smoke and verify it fails for missing scene layer clothing mapping**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: FAIL with a message involving wardrobe or softcore branch clothing axis mapping.
|
||||
|
||||
- [ ] **Step 3: Implement scene layer clothing mappings**
|
||||
|
||||
In `node_scene.py`, update `SCENE_LAYER_SEED_AXES`:
|
||||
|
||||
```python
|
||||
SCENE_LAYER_SEED_AXES = {
|
||||
"cast": ("category",),
|
||||
"character": ("person",),
|
||||
"wardrobe": ("clothing",),
|
||||
"location": ("scene",),
|
||||
"set_dressing": ("scene",),
|
||||
"blocking": ("pose",),
|
||||
"action": ("pose", "role"),
|
||||
"performance": ("expression",),
|
||||
"camera": ("composition",),
|
||||
"composition": ("composition",),
|
||||
"lighting": ("composition",),
|
||||
"softcore_branch": ("clothing", "pose", "role"),
|
||||
"hardcore_branch": ("pose", "role"),
|
||||
}
|
||||
```
|
||||
|
||||
In the same file, update `SCENE_REROLL_GROUPS`:
|
||||
|
||||
```python
|
||||
SCENE_REROLL_GROUPS = {
|
||||
"none": (),
|
||||
"category": ("category",),
|
||||
"subcategory": ("subcategory",),
|
||||
"content": ("content",),
|
||||
"clothing": ("clothing",),
|
||||
"person": ("person",),
|
||||
"scene": ("scene",),
|
||||
"pose": ("pose", "role"),
|
||||
"role": ("role",),
|
||||
"expression": ("expression",),
|
||||
"composition": ("composition",),
|
||||
"content_pose": ("content", "pose", "role"),
|
||||
"content_clothing": ("content", "clothing"),
|
||||
"clothing_pose": ("clothing", "pose", "role"),
|
||||
"scene_pose": ("scene", "pose", "role"),
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run focused smoke tests and verify they pass**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
```
|
||||
|
||||
Expected: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 5: Commit Task 3**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. add node_scene.py tools/prompt_smoke.py
|
||||
git --git-dir=.git-real --work-tree=. commit -m "Map scene clothing seeds to clothing axis"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Final Verification And Push
|
||||
|
||||
**Files:**
|
||||
- No production file edits expected.
|
||||
- Verify all files touched by Tasks 1-3.
|
||||
|
||||
- [ ] **Step 1: Run compilation checks**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python -m py_compile seed_config.py prompt_builder.py node_seed_resolution.py node_tooltips.py builder_prompt_route.py pair_rows.py pair_cast.py node_scene.py tools/prompt_smoke.py
|
||||
```
|
||||
|
||||
Expected: exit code 0.
|
||||
|
||||
- [ ] **Step 2: Run focused smoke tests**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --case seed_config_policy --quiet
|
||||
python tools/prompt_smoke.py --case node_utility_registration --quiet
|
||||
python tools/prompt_smoke.py --case node_scene_chain_registration --quiet
|
||||
```
|
||||
|
||||
Expected for each command: `OK: smoke passed (1 cases).`
|
||||
|
||||
- [ ] **Step 3: Run the full smoke suite**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
python tools/prompt_smoke.py --quiet
|
||||
```
|
||||
|
||||
Expected: all cases pass except the known unrelated `krea2_prompt_guide_policy` failure if it is still present. If any new failure mentions seed policy, scene layer seed, clothing state, prompt routing, pair rows, or node utility registration, fix it before committing or pushing.
|
||||
|
||||
- [ ] **Step 4: Check git status**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. status --short --branch
|
||||
git --git-dir=.git-real --work-tree=. log -5 --oneline
|
||||
```
|
||||
|
||||
Expected: branch contains the three implementation commits from this plan and no unstaged edits.
|
||||
|
||||
- [ ] **Step 5: Push the branch**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
git --git-dir=.git-real --work-tree=. push
|
||||
```
|
||||
|
||||
Expected: push succeeds to `origin/hardcore-interaction-expansion`.
|
||||
|
||||
---
|
||||
|
||||
## Self-Review
|
||||
|
||||
- Spec coverage: Task 1 covers seed vocabulary, aliases, seed-lock config, seed trace, seed-control UI, and tooltips. Task 2 covers normal prompt clothing mode routing, primary softcore outfit routing, partner outfit routing, and legacy content fallback. Task 3 covers scene layer mappings, wardrobe axis behavior, softcore branch clothing-only rerolls, hard-branch continuity, and `content_pose` not touching clothing. Task 4 covers compilation, focused smoke tests, full smoke, status, and push.
|
||||
- Scope check: this plan avoids rewriting category data and keeps custom content item selection on the `content` axis, matching the spec's out-of-scope section.
|
||||
- Type consistency: all new seed keys use `clothing_seed`; all new axis names use `clothing`, `content_clothing`, and `clothing_pose`; compatibility aliases use `outfit_seed`, `wardrobe_seed`, and fallback `content_seed`.
|
||||
Reference in New Issue
Block a user