Extract seed config policy
This commit is contained in:
@@ -304,8 +304,12 @@ Already isolated:
|
|||||||
|
|
||||||
- direct and config-driven prompt builder nodes live in `node_builder.py`, with
|
- direct and config-driven prompt builder nodes live in `node_builder.py`, with
|
||||||
registration maps imported by `__init__.py`.
|
registration maps imported by `__init__.py`.
|
||||||
- seed/global-seed/seed-locker and SDXL/Krea2 resolution utility nodes live in
|
- seed axis salts/aliases, seed mode choices, lock builders, seed config
|
||||||
`node_seed_resolution.py`, with registration maps imported by `__init__.py`.
|
parsing, row seed math, and deterministic axis RNG live in `seed_config.py`;
|
||||||
|
seed/global-seed/seed-locker nodes live in `node_seed_resolution.py`, with
|
||||||
|
registration maps imported by `__init__.py`.
|
||||||
|
- SDXL/Krea2 resolution utility nodes live in `node_seed_resolution.py`, with
|
||||||
|
registration maps imported by `__init__.py`.
|
||||||
- camera/orbit/Qwen translator utility nodes live in `node_camera.py`, using
|
- camera/orbit/Qwen translator utility nodes live in `node_camera.py`, using
|
||||||
`camera_config.py` for option lists and JSON builders, with registration maps
|
`camera_config.py` for option lists and JSON builders, with registration maps
|
||||||
imported by `__init__.py`.
|
imported by `__init__.py`.
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ Core helper ownership:
|
|||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
|
| `category_library.py` | JSON category loading, subcategory normalization, named scene/expression/composition pool loading, cast compatibility filtering, exact subcategory lookup, and inheritance-based pool merging. |
|
||||||
| `camera_config.py` | Camera option schema, direct/orbit/Qwen camera JSON builders, camera config parsing, plain camera directive text, and camera caption labels. |
|
| `camera_config.py` | Camera option schema, direct/orbit/Qwen camera JSON builders, camera config parsing, plain camera directive text, and camera caption labels. |
|
||||||
|
| `seed_config.py` | Seed axis salts/aliases, seed mode choices, global/axis lock JSON builders, seed config parsing, row seed math, and deterministic axis RNG construction. |
|
||||||
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, and hardcore cast count policy. |
|
| `pair_options.py` | Insta/OF option schema/defaults, softcore category/outfit/pose pools, partner outfit pools, clothing-continuity labels, negatives, and hardcore cast count policy. |
|
||||||
| `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. |
|
| `pair_rows.py` | Insta/OF soft/hard row creation, softcore expression override resolution, Woman A slot context application, soft outfit/pose overrides, and POV row fields. |
|
||||||
| `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. |
|
| `pair_camera.py` | Insta/OF soft/hard camera route resolution, same-as-softcore camera mode, camera-detail override, camera-aware composition mutation, POV camera suppression, and synchronized row/root camera metadata. |
|
||||||
@@ -124,8 +125,8 @@ These recipes identify the intended road before editing prompt text.
|
|||||||
|
|
||||||
## Seed Axes
|
## Seed Axes
|
||||||
|
|
||||||
Seed routing is centralized around `SEED_AXIS_SALTS`, `SEED_AXIS_ALIASES`, and
|
Seed routing is centralized in `seed_config.py` around `SEED_AXIS_SALTS`,
|
||||||
`_axis_rng` in `prompt_builder.py`.
|
`SEED_AXIS_ALIASES`, and `axis_rng`.
|
||||||
|
|
||||||
| Axis | Controls |
|
| Axis | Controls |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
@@ -161,8 +162,9 @@ axes change.
|
|||||||
| Same soft/hard pair but different hardcore action | In pair mode, keep `person_seed`, `scene_seed`, `content_seed` if clothing must stay; change `pose_seed`/`role_seed`. |
|
| Same soft/hard pair but different hardcore action | In pair mode, keep `person_seed`, `scene_seed`, `content_seed` if clothing must stay; change `pose_seed`/`role_seed`. |
|
||||||
| Debug expression only | Fix everything except `expression_seed` or expression intensity. |
|
| Debug expression only | Fix everything except `expression_seed` or expression intensity. |
|
||||||
|
|
||||||
Common trap: `row_number` participates in `_axis_rng`. If two workflows have the
|
Common trap: `row_number` participates in `seed_config.axis_rng`. If two
|
||||||
same seeds but different `row_number`, they are not expected to match.
|
workflows have the same seeds but different `row_number`, they are not expected
|
||||||
|
to match.
|
||||||
|
|
||||||
## Category Sources
|
## Category Sources
|
||||||
|
|
||||||
@@ -699,7 +701,7 @@ These do not own prompt pool wording, but they affect execution and review:
|
|||||||
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
| Accumulator | `loop_nodes.py`, `web/accumulator_preview.js` | Stores generated values/images during workflow execution and previews/reorders/deletes them. |
|
||||||
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
|
||||||
| Builder node wrappers | `node_builder.py`, imported by `__init__.py` | Direct prompt builder and config-driven prompt builder ComfyUI declarations. |
|
| Builder node wrappers | `node_builder.py`, imported by `__init__.py` | Direct prompt builder and config-driven prompt builder ComfyUI declarations. |
|
||||||
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | Global/per-axis seed configs plus SDXL/Krea width/height helpers. |
|
| Seed and resolution utility nodes | `node_seed_resolution.py`, imported by `__init__.py` | UI wrappers for global/per-axis seed configs via `seed_config.py`, plus SDXL/Krea width/height helpers. |
|
||||||
| Camera utility nodes | `node_camera.py`, imported by `__init__.py` | UI wrappers for direct camera config, orbit-to-camera config, and Qwen MultiAngle camera translation via `camera_config.py`. |
|
| Camera utility nodes | `node_camera.py`, imported by `__init__.py` | UI wrappers for direct camera config, orbit-to-camera config, and Qwen MultiAngle camera translation via `camera_config.py`. |
|
||||||
| Character utility nodes | `node_character.py`, imported by `__init__.py` | Hair, age/body/eyes/clothing pools, manual details, character slots, and profile save/load nodes. |
|
| Character utility nodes | `node_character.py`, imported by `__init__.py` | Hair, age/body/eyes/clothing pools, manual details, character slots, and profile save/load nodes. |
|
||||||
| Hardcore position utility nodes | `node_hardcore_position.py`, imported by `__init__.py` | Position-family pool and action/filter gates for hardcore routes. |
|
| Hardcore position utility nodes | `node_hardcore_position.py`, imported by `__init__.py` | Position-family pool and action/filter gates for hardcore routes. |
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ import math
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from .prompt_builder import (
|
from .seed_config import (
|
||||||
build_seed_config_json,
|
build_seed_config_json,
|
||||||
build_seed_lock_config_json,
|
build_seed_lock_config_json,
|
||||||
seed_mode_choices,
|
seed_mode_choices,
|
||||||
)
|
)
|
||||||
except ImportError: # Allows local smoke tests from the repository root.
|
except ImportError: # Allows local smoke tests from the repository root.
|
||||||
from prompt_builder import (
|
from seed_config import (
|
||||||
build_seed_config_json,
|
build_seed_config_json,
|
||||||
build_seed_lock_config_json,
|
build_seed_lock_config_json,
|
||||||
seed_mode_choices,
|
seed_mode_choices,
|
||||||
|
|||||||
+35
-112
@@ -32,6 +32,7 @@ try:
|
|||||||
from . import pair_rows
|
from . import pair_rows
|
||||||
from . import pair_options
|
from . import pair_options
|
||||||
from . import scene_camera_adapters
|
from . import scene_camera_adapters
|
||||||
|
from . import seed_config as seed_policy
|
||||||
from .hardcore_text_cleanup import (
|
from .hardcore_text_cleanup import (
|
||||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||||
@@ -68,6 +69,7 @@ except ImportError: # Allows local smoke tests with `python -c`.
|
|||||||
import pair_rows
|
import pair_rows
|
||||||
import pair_options
|
import pair_options
|
||||||
import scene_camera_adapters
|
import scene_camera_adapters
|
||||||
|
import seed_config as seed_policy
|
||||||
from hardcore_text_cleanup import (
|
from hardcore_text_cleanup import (
|
||||||
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
sanitize_hardcore_axis_values as _sanitize_hardcore_axis_values,
|
||||||
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
sanitize_hardcore_environment_anchors as _sanitize_hardcore_environment_anchors,
|
||||||
@@ -94,41 +96,10 @@ BUILTIN_CATEGORIES = [
|
|||||||
"custom_random",
|
"custom_random",
|
||||||
]
|
]
|
||||||
RANDOM_SUBCATEGORY = "random"
|
RANDOM_SUBCATEGORY = "random"
|
||||||
SEED_AXIS_SALTS = {
|
SEED_AXIS_SALTS = seed_policy.SEED_AXIS_SALTS
|
||||||
"category": 31,
|
SEED_AXIS_ALIASES = seed_policy.SEED_AXIS_ALIASES
|
||||||
"subcategory": 37,
|
SEED_LOCK_AXES = seed_policy.SEED_LOCK_AXES
|
||||||
"content": 41,
|
SEED_MODE_CHOICES = seed_policy.SEED_MODE_CHOICES
|
||||||
"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", "outfit_seed", "sexual_pose_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",
|
|
||||||
"person",
|
|
||||||
"scene",
|
|
||||||
"pose",
|
|
||||||
"role",
|
|
||||||
"expression",
|
|
||||||
"composition",
|
|
||||||
)
|
|
||||||
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
|
|
||||||
|
|
||||||
ETHNICITY_FILTER_CHOICES = [
|
ETHNICITY_FILTER_CHOICES = [
|
||||||
"any",
|
"any",
|
||||||
@@ -1266,7 +1237,7 @@ def subcategory_choices() -> list[str]:
|
|||||||
|
|
||||||
|
|
||||||
def seed_mode_choices() -> list[str]:
|
def seed_mode_choices() -> list[str]:
|
||||||
return list(SEED_MODE_CHOICES)
|
return seed_policy.seed_mode_choices()
|
||||||
|
|
||||||
|
|
||||||
CATEGORY_PRESETS = {
|
CATEGORY_PRESETS = {
|
||||||
@@ -2510,32 +2481,25 @@ def build_seed_config_json(
|
|||||||
expression_seed_mode: str = "auto",
|
expression_seed_mode: str = "auto",
|
||||||
composition_seed_mode: str = "auto",
|
composition_seed_mode: str = "auto",
|
||||||
) -> str:
|
) -> str:
|
||||||
rng = random.SystemRandom()
|
return seed_policy.build_seed_config_json(
|
||||||
|
category_seed=category_seed,
|
||||||
def axis_seed(value: int, mode: str) -> int:
|
subcategory_seed=subcategory_seed,
|
||||||
mode = mode if mode in SEED_MODE_CHOICES else "auto"
|
content_seed=content_seed,
|
||||||
if mode == "auto":
|
person_seed=person_seed,
|
||||||
return int(value)
|
scene_seed=scene_seed,
|
||||||
if mode == "random":
|
pose_seed=pose_seed,
|
||||||
return rng.randint(0, 0xFFFFFFFF)
|
role_seed=role_seed,
|
||||||
if mode == "fixed":
|
expression_seed=expression_seed,
|
||||||
return max(0, int(value))
|
composition_seed=composition_seed,
|
||||||
return -1
|
category_seed_mode=category_seed_mode,
|
||||||
|
subcategory_seed_mode=subcategory_seed_mode,
|
||||||
return json.dumps(
|
content_seed_mode=content_seed_mode,
|
||||||
{
|
person_seed_mode=person_seed_mode,
|
||||||
"category_seed": axis_seed(category_seed, category_seed_mode),
|
scene_seed_mode=scene_seed_mode,
|
||||||
"subcategory_seed": axis_seed(subcategory_seed, subcategory_seed_mode),
|
pose_seed_mode=pose_seed_mode,
|
||||||
"content_seed": axis_seed(content_seed, content_seed_mode),
|
role_seed_mode=role_seed_mode,
|
||||||
"person_seed": axis_seed(person_seed, person_seed_mode),
|
expression_seed_mode=expression_seed_mode,
|
||||||
"scene_seed": axis_seed(scene_seed, scene_seed_mode),
|
composition_seed_mode=composition_seed_mode,
|
||||||
"pose_seed": axis_seed(pose_seed, pose_seed_mode),
|
|
||||||
"role_seed": axis_seed(role_seed, role_seed_mode),
|
|
||||||
"expression_seed": axis_seed(expression_seed, expression_seed_mode),
|
|
||||||
"composition_seed": axis_seed(composition_seed, composition_seed_mode),
|
|
||||||
},
|
|
||||||
ensure_ascii=True,
|
|
||||||
sort_keys=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -2544,64 +2508,23 @@ def build_seed_lock_config_json(
|
|||||||
reroll_axis: str = "none",
|
reroll_axis: str = "none",
|
||||||
reroll_seed: int = -1,
|
reroll_seed: int = -1,
|
||||||
) -> str:
|
) -> str:
|
||||||
base_seed = int(base_seed)
|
return seed_policy.build_seed_lock_config_json(
|
||||||
reroll_seed = int(reroll_seed)
|
base_seed=base_seed,
|
||||||
reroll_groups = {
|
reroll_axis=reroll_axis,
|
||||||
"none": (),
|
reroll_seed=reroll_seed,
|
||||||
"category": ("category",),
|
)
|
||||||
"subcategory": ("subcategory",),
|
|
||||||
"content": ("content",),
|
|
||||||
"person": ("person",),
|
|
||||||
"scene": ("scene",),
|
|
||||||
"pose": ("pose", "role"),
|
|
||||||
"role": ("role",),
|
|
||||||
"expression": ("expression",),
|
|
||||||
"composition": ("composition",),
|
|
||||||
"content_pose": ("content", "pose", "role"),
|
|
||||||
"scene_pose": ("scene", "pose", "role"),
|
|
||||||
}
|
|
||||||
reroll = set(reroll_groups.get(str(reroll_axis or "none"), ()))
|
|
||||||
config: dict[str, int] = {}
|
|
||||||
for axis in SEED_LOCK_AXES:
|
|
||||||
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
|
|
||||||
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
|
def _parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
|
||||||
if not seed_config:
|
return seed_policy.parse_seed_config(seed_config)
|
||||||
return {}
|
|
||||||
if isinstance(seed_config, dict):
|
|
||||||
raw = seed_config
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
raw = json.loads(str(seed_config))
|
|
||||||
except json.JSONDecodeError as exc:
|
|
||||||
raise ValueError(f"Invalid seed_config JSON: {exc}") from exc
|
|
||||||
if not isinstance(raw, dict):
|
|
||||||
raise ValueError("seed_config must be a JSON object")
|
|
||||||
parsed: dict[str, int] = {}
|
|
||||||
for key, value in raw.items():
|
|
||||||
try:
|
|
||||||
parsed[str(key)] = int(value)
|
|
||||||
except (TypeError, ValueError):
|
|
||||||
continue
|
|
||||||
return parsed
|
|
||||||
|
|
||||||
|
|
||||||
def _configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
|
def _configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
|
||||||
for key in SEED_AXIS_ALIASES.get(axis, (axis,)):
|
return seed_policy.configured_axis_seed(seed_config, axis)
|
||||||
value = seed_config.get(key)
|
|
||||||
if value is not None and value >= 0:
|
|
||||||
return value
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
|
def _axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
|
||||||
configured = _configured_axis_seed(seed_config, axis)
|
return seed_policy.axis_rng(seed_config, axis, base_seed, row_number)
|
||||||
salt = SEED_AXIS_SALTS.get(axis, 0)
|
|
||||||
if configured is None:
|
|
||||||
return random.Random(_row_seed(base_seed, row_number, salt))
|
|
||||||
return random.Random(_row_seed(configured, row_number, salt))
|
|
||||||
|
|
||||||
|
|
||||||
def _is_pose_content_category(category: dict[str, Any], subcategory: dict[str, Any]) -> bool:
|
def _is_pose_content_category(category: dict[str, Any], subcategory: dict[str, Any]) -> bool:
|
||||||
@@ -3085,7 +3008,7 @@ def _apply_camera_config(row: dict[str, Any], camera_config: str | dict[str, Any
|
|||||||
|
|
||||||
|
|
||||||
def _row_seed(seed: int, row_number: int, salt: int = 0) -> int:
|
def _row_seed(seed: int, row_number: int, salt: int = 0) -> int:
|
||||||
return int(seed) + int(row_number) * 1009 + salt * 9176
|
return seed_policy.row_seed(seed, row_number, salt)
|
||||||
|
|
||||||
|
|
||||||
def _pick_clothing_mode(rng: random.Random, clothing: str, minimal_ratio: float | None) -> str:
|
def _pick_clothing_mode(rng: random.Random, clothing: str, minimal_ratio: float | None) -> str:
|
||||||
|
|||||||
+165
@@ -0,0 +1,165 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
|
||||||
|
SEED_AXIS_SALTS = {
|
||||||
|
"category": 31,
|
||||||
|
"subcategory": 37,
|
||||||
|
"content": 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", "outfit_seed", "sexual_pose_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",
|
||||||
|
"person",
|
||||||
|
"scene",
|
||||||
|
"pose",
|
||||||
|
"role",
|
||||||
|
"expression",
|
||||||
|
"composition",
|
||||||
|
)
|
||||||
|
SEED_MODE_CHOICES = ["auto", "follow_main", "fixed", "random"]
|
||||||
|
|
||||||
|
|
||||||
|
def seed_mode_choices() -> list[str]:
|
||||||
|
return list(SEED_MODE_CHOICES)
|
||||||
|
|
||||||
|
|
||||||
|
def row_seed(seed: int, row_number: int, salt: int = 0) -> int:
|
||||||
|
return int(seed) + int(row_number) * 1009 + salt * 9176
|
||||||
|
|
||||||
|
|
||||||
|
def build_seed_config_json(
|
||||||
|
category_seed: int = -1,
|
||||||
|
subcategory_seed: int = -1,
|
||||||
|
content_seed: int = -1,
|
||||||
|
person_seed: int = -1,
|
||||||
|
scene_seed: int = -1,
|
||||||
|
pose_seed: int = -1,
|
||||||
|
role_seed: int = -1,
|
||||||
|
expression_seed: int = -1,
|
||||||
|
composition_seed: int = -1,
|
||||||
|
category_seed_mode: str = "auto",
|
||||||
|
subcategory_seed_mode: str = "auto",
|
||||||
|
content_seed_mode: str = "auto",
|
||||||
|
person_seed_mode: str = "auto",
|
||||||
|
scene_seed_mode: str = "auto",
|
||||||
|
pose_seed_mode: str = "auto",
|
||||||
|
role_seed_mode: str = "auto",
|
||||||
|
expression_seed_mode: str = "auto",
|
||||||
|
composition_seed_mode: str = "auto",
|
||||||
|
) -> str:
|
||||||
|
rng = random.SystemRandom()
|
||||||
|
|
||||||
|
def axis_seed(value: int, mode: str) -> int:
|
||||||
|
mode = mode if mode in SEED_MODE_CHOICES else "auto"
|
||||||
|
if mode == "auto":
|
||||||
|
return int(value)
|
||||||
|
if mode == "random":
|
||||||
|
return rng.randint(0, 0xFFFFFFFF)
|
||||||
|
if mode == "fixed":
|
||||||
|
return max(0, int(value))
|
||||||
|
return -1
|
||||||
|
|
||||||
|
return json.dumps(
|
||||||
|
{
|
||||||
|
"category_seed": axis_seed(category_seed, category_seed_mode),
|
||||||
|
"subcategory_seed": axis_seed(subcategory_seed, subcategory_seed_mode),
|
||||||
|
"content_seed": axis_seed(content_seed, content_seed_mode),
|
||||||
|
"person_seed": axis_seed(person_seed, person_seed_mode),
|
||||||
|
"scene_seed": axis_seed(scene_seed, scene_seed_mode),
|
||||||
|
"pose_seed": axis_seed(pose_seed, pose_seed_mode),
|
||||||
|
"role_seed": axis_seed(role_seed, role_seed_mode),
|
||||||
|
"expression_seed": axis_seed(expression_seed, expression_seed_mode),
|
||||||
|
"composition_seed": axis_seed(composition_seed, composition_seed_mode),
|
||||||
|
},
|
||||||
|
ensure_ascii=True,
|
||||||
|
sort_keys=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build_seed_lock_config_json(
|
||||||
|
base_seed: int = 20260614,
|
||||||
|
reroll_axis: str = "none",
|
||||||
|
reroll_seed: int = -1,
|
||||||
|
) -> str:
|
||||||
|
base_seed = int(base_seed)
|
||||||
|
reroll_seed = int(reroll_seed)
|
||||||
|
reroll_groups = {
|
||||||
|
"none": (),
|
||||||
|
"category": ("category",),
|
||||||
|
"subcategory": ("subcategory",),
|
||||||
|
"content": ("content",),
|
||||||
|
"person": ("person",),
|
||||||
|
"scene": ("scene",),
|
||||||
|
"pose": ("pose", "role"),
|
||||||
|
"role": ("role",),
|
||||||
|
"expression": ("expression",),
|
||||||
|
"composition": ("composition",),
|
||||||
|
"content_pose": ("content", "pose", "role"),
|
||||||
|
"scene_pose": ("scene", "pose", "role"),
|
||||||
|
}
|
||||||
|
reroll = set(reroll_groups.get(str(reroll_axis or "none"), ()))
|
||||||
|
config: dict[str, int] = {}
|
||||||
|
for axis in SEED_LOCK_AXES:
|
||||||
|
config[f"{axis}_seed"] = reroll_seed if axis in reroll else base_seed
|
||||||
|
return json.dumps(config, ensure_ascii=True, sort_keys=True)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_seed_config(seed_config: str | dict[str, Any] | None) -> dict[str, int]:
|
||||||
|
if not seed_config:
|
||||||
|
return {}
|
||||||
|
if isinstance(seed_config, dict):
|
||||||
|
raw = seed_config
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
raw = json.loads(str(seed_config))
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise ValueError(f"Invalid seed_config JSON: {exc}") from exc
|
||||||
|
if not isinstance(raw, dict):
|
||||||
|
raise ValueError("seed_config must be a JSON object")
|
||||||
|
parsed: dict[str, int] = {}
|
||||||
|
for key, value in raw.items():
|
||||||
|
try:
|
||||||
|
parsed[str(key)] = int(value)
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
continue
|
||||||
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
def configured_axis_seed(seed_config: dict[str, int], axis: str) -> int | None:
|
||||||
|
for key in SEED_AXIS_ALIASES.get(axis, (axis,)):
|
||||||
|
value = seed_config.get(key)
|
||||||
|
if value is not None and value >= 0:
|
||||||
|
return value
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def axis_rng(seed_config: dict[str, int], axis: str, base_seed: int, row_number: int) -> random.Random:
|
||||||
|
configured = configured_axis_seed(seed_config, axis)
|
||||||
|
salt = SEED_AXIS_SALTS.get(axis, 0)
|
||||||
|
if configured is None:
|
||||||
|
return random.Random(row_seed(base_seed, row_number, salt))
|
||||||
|
return random.Random(row_seed(configured, row_number, salt))
|
||||||
@@ -29,6 +29,7 @@ import __init__ as sxcp_nodes # noqa: E402
|
|||||||
import krea_formatter # noqa: E402
|
import krea_formatter # noqa: E402
|
||||||
import prompt_builder as pb # noqa: E402
|
import prompt_builder as pb # noqa: E402
|
||||||
import sdxl_formatter # noqa: E402
|
import sdxl_formatter # noqa: E402
|
||||||
|
import seed_config # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
Trigger = "sxcppnl7"
|
Trigger = "sxcppnl7"
|
||||||
@@ -1761,6 +1762,43 @@ def smoke_node_utility_registration() -> None:
|
|||||||
_expect(krea_config.get("width") == krea_width and krea_config.get("height") == krea_height, "Krea2 config_json dimensions mismatch")
|
_expect(krea_config.get("width") == krea_width and krea_config.get("height") == krea_height, "Krea2 config_json dimensions mismatch")
|
||||||
|
|
||||||
|
|
||||||
|
def smoke_seed_config_policy() -> None:
|
||||||
|
_expect(pb.SEED_AXIS_SALTS is seed_config.SEED_AXIS_SALTS, "prompt_builder seed salts should delegate to seed_config")
|
||||||
|
_expect(pb.seed_mode_choices() == seed_config.seed_mode_choices(), "seed mode choices drifted from seed_config")
|
||||||
|
|
||||||
|
fixed_config = json.loads(
|
||||||
|
pb.build_seed_config_json(
|
||||||
|
category_seed=-1,
|
||||||
|
content_seed=123,
|
||||||
|
pose_seed=456,
|
||||||
|
role_seed=789,
|
||||||
|
category_seed_mode="fixed",
|
||||||
|
content_seed_mode="fixed",
|
||||||
|
pose_seed_mode="follow_main",
|
||||||
|
role_seed_mode="auto",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
_expect(fixed_config["category_seed"] == 0, "fixed seed mode should clamp negative seeds to zero")
|
||||||
|
_expect(fixed_config["content_seed"] == 123, "fixed seed mode should preserve positive seed")
|
||||||
|
_expect(fixed_config["pose_seed"] == -1, "follow_main seed mode should emit unlocked axis")
|
||||||
|
_expect(fixed_config["role_seed"] == 789, "auto seed mode should preserve numeric seed")
|
||||||
|
|
||||||
|
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")
|
||||||
|
|
||||||
|
locked = json.loads(pb.build_seed_lock_config_json(base_seed=100, reroll_axis="content_pose", reroll_seed=999))
|
||||||
|
_expect(locked["content_seed"] == 999, "content_pose reroll should alter content seed")
|
||||||
|
_expect(locked["pose_seed"] == 999 and locked["role_seed"] == 999, "content_pose reroll should alter pose and role seeds")
|
||||||
|
_expect(locked["scene_seed"] == 100, "content_pose reroll should leave scene locked")
|
||||||
|
|
||||||
|
rng_a = pb._axis_rng({"content_seed": 123}, "content", 999, 7)
|
||||||
|
rng_b = seed_config.axis_rng({"content_seed": 123}, "content", 999, 7)
|
||||||
|
_expect(rng_a.random() == rng_b.random(), "prompt_builder axis RNG should delegate to seed_config")
|
||||||
|
_expect(pb._row_seed(123, 7, 41) == seed_config.row_seed(123, 7, 41), "row seed wrapper drifted from seed_config")
|
||||||
|
|
||||||
|
|
||||||
def smoke_node_camera_registration() -> None:
|
def smoke_node_camera_registration() -> None:
|
||||||
required_nodes = [
|
required_nodes = [
|
||||||
"SxCPCameraControl",
|
"SxCPCameraControl",
|
||||||
@@ -2387,6 +2425,7 @@ SMOKE_CASES: list[tuple[str, Callable[[], None]]] = [
|
|||||||
("expression_disabled", smoke_no_expression_fallback),
|
("expression_disabled", smoke_no_expression_fallback),
|
||||||
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
("formatter_metadata_fixtures", smoke_formatter_metadata_fixtures),
|
||||||
("node_utility_registration", smoke_node_utility_registration),
|
("node_utility_registration", smoke_node_utility_registration),
|
||||||
|
("seed_config_policy", smoke_seed_config_policy),
|
||||||
("node_camera_registration", smoke_node_camera_registration),
|
("node_camera_registration", smoke_node_camera_registration),
|
||||||
("node_route_config_registration", smoke_node_route_config_registration),
|
("node_route_config_registration", smoke_node_route_config_registration),
|
||||||
("node_character_registration", smoke_node_character_registration),
|
("node_character_registration", smoke_node_character_registration),
|
||||||
|
|||||||
Reference in New Issue
Block a user