Expand prompt routing map

This commit is contained in:
2026-06-26 12:22:25 +02:00
parent 9ee105e090
commit 3a3771eed5
2 changed files with 418 additions and 0 deletions
+264
View File
@@ -60,6 +60,41 @@ call the same core generation functions.
| `SxCP SDXL Formatter` | `format_sdxl_prompt` | Converts metadata rows or pair metadata into SDXL/tag style prompts. |
| `SxCP Caption Naturalizer` | `naturalize_caption` | Converts rows into more natural sentence captions. |
## Node IO Map
Use this when wiring or debugging a workflow. If the formatter can receive
`metadata_json`, prefer wiring metadata instead of only prompt text. Metadata is
what keeps cast, role graph, POV labels, camera config, and soft/hard pair state
recoverable.
| Node | Important inputs | Important outputs |
| --- | --- | --- |
| `SxCP Prompt Builder` | category, subcategory, seed, optional config nodes | `prompt`, `negative_prompt`, `caption`, `metadata_json`, `category`, `subcategory` |
| `SxCP Prompt Builder From Configs` | category/cast/profile/filter/config node outputs | Same as `SxCP Prompt Builder` |
| `SxCP Insta/OF Prompt Pair` | options, seed_config, character_cast, location/composition/camera, hardcore_position_config | `softcore_prompt`, `hardcore_prompt`, both negatives, both captions, `shared_descriptor`, `metadata_json` |
| `SxCP Krea2 Formatter` | `source_text`, optional `metadata_json`, target | `krea_prompt`, both pair prompts if pair metadata exists, negative outputs, method |
| `SxCP SDXL Formatter` | `source_text`, optional `metadata_json`, target, style/quality preset | `sdxl_prompt`, both pair prompts if pair metadata exists, negative outputs, method |
| `SxCP Caption Naturalizer` | `source_text`, optional `metadata_json` | `natural_caption`, method |
## Practical Recipes
These recipes identify the intended road before editing prompt text.
| Request | Preferred node route | Critical settings | If wrong, inspect |
| --- | --- | --- | --- |
| Keep character/location but change only sexual pose | `Global Seed` or fixed seed config -> builder/pair | Keep `person_seed` and `scene_seed` fixed; change `pose_seed` and usually `role_seed`; for hardcore categories check `content_seed_axis` | `sexual_poses.json`, `hardcore_position_config`, Krea `_hardcore_action_sentence` |
| Generate a specific hardcore oral/blowjob scene | `Hardcore Position Pool` -> `Hardcore Action Filter` -> `Insta/OF Prompt Pair` or `Prompt Builder` | Use `focus=oral_only` or disable non-oral families; keep `allow_oral=true`; constrain position pool to kneeling/standing/oral variants when needed | `sexual_poses.json` oral subcategory/templates, `_apply_hardcore_position_config_to_subcategory`, `_hardcore_action_sentence` |
| Generate POV oral or POV penetration | `Man Slot` with POV presence -> `character_cast` -> pair/builder -> Krea2 formatter | POV man must be in the cast; use metadata into Krea2; normal camera directive is suppressed by POV | `_pov_hardcore_pose_sentence`, `_pov_action_phrase`, `_cast_prose` omit-label handling |
| Same woman, same room, softcore and hardcore outputs | `Character Slot/Profile` -> `Insta/OF Options` -> `Insta/OF Prompt Pair` | `continuity=same_creator_same_room`; set `softcore_cast` as needed; use pair metadata into formatter | `build_insta_of_pair`, `softcore_row`, `hardcore_row`, pair metadata fields |
| Same cast in softcore and hardcore | Character slot chain -> `Insta/OF Options` | `softcore_cast=same_as_hardcore`; configure partner slots/outfits if needed | `_insta_of_partner_styling`, character slot clothing, pair Krea branch |
| Change only outfit/clothing | Character clothing or category content route | Keep `person_seed`, `scene_seed`, `pose_seed`; change `content_seed`; slot `softcore_outfit` overrides Insta/OF outfit | `SxCP Character Clothing`, `INSTA_OF_SOFTCORE_OUTFITS`, category item templates |
| Force a custom location | `SxCP Location Pool` or `SxCP Location Theme` -> builder/pair | `combine_mode=replace` to force; `add` to mix with category scenes | `_scene_pool`, `_apply_location_config_to_legacy_row`, camera scene adapter |
| Force a custom frame/composition | `SxCP Composition Pool` or `SxCP Location Theme` -> builder/pair | `combine_mode=replace` to force; `add` to mix | `_composition_pool`, `_apply_composition_config_to_legacy_row`, Krea composition phrase |
| Use Qwen/orbit camera geometry | Qwen/orbit node -> camera_config -> builder/pair | For pair, use `softcore_camera_config` and/or `hardcore_camera_config`; set mode from config in options | `_camera_config_with_mode`, `_camera_directive`, `_camera_scene_directive_for_context` |
| Use Krea2 for only hard prompt from a pair | Pair `metadata_json` -> Krea2 Formatter | `target=hardcore`, `input_hint=metadata_json` or auto with metadata connected | `_insta_pair_to_krea`, hard row fields |
| Convert builder output to SDXL tags | Builder/pair metadata -> SDXL Formatter | Use metadata input; set `target`; select style and quality preset | `_row_core_tags`, `_soft_tags`, `_hard_tags` |
| Save/reuse character | Slot/profile nodes -> Profile Save/Load -> slot/builder | Save from the row/profile data you want, not a freshly randomized disconnected route | profile helpers, `web/profile_buttons.js`, profile JSON |
## Seed Axes
Seed routing is centralized around `SEED_AXIS_SALTS`, `SEED_AXIS_ALIASES`, and
@@ -82,6 +117,26 @@ Seed routing is centralized around `SEED_AXIS_SALTS`, `SEED_AXIS_ALIASES`, and
axis. Fixed axis seeds allow changing only one road, for example changing
`pose`/`role` while keeping person, scene, and category stable.
## Seed Playbook
The seed system has two levels: the main row seed and optional per-axis seeds.
If an axis seed is negative or absent, the main row seed plus row number drives
that axis. If an axis seed is fixed, that axis is reproducible even while other
axes change.
| Goal | Seed setup |
| --- | --- |
| Exact full regeneration | Keep main `seed`, `row_number`, `start_index`, and every connected config identical. |
| Same person, new pose | Fix `person_seed`; change `pose_seed` and usually `role_seed`. For hardcore pose categories, changing `content_seed` may also matter if the selected category uses content for pose items. |
| Same scene, new character | Fix `scene_seed`; change `person_seed`. |
| Same action, new framing | Fix `pose_seed`, `role_seed`, and `content_seed`; change `composition_seed` and/or camera config. |
| Same outfit, new pose | Fix `content_seed`; 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. |
Common trap: `row_number` participates in `_axis_rng`. If two workflows have the
same seeds but different `row_number`, they are not expected to match.
## Category Sources
There are two category systems.
@@ -140,6 +195,25 @@ Current category/pool files:
| `categories/location_pools.json` | Named scene pools and location pool extensions. |
| `categories/expression_composition_pools.json` | Named expression pools and composition pools. |
## Pool Ownership Matrix
This table is the first stop when the selected content is wrong.
| File / pool area | Owns | Selection axis | Formatter risk |
| --- | --- | --- | --- |
| `default_categories.json` woman casual subcategories | Casual outfit items, casual scenes, casual expressions, casual compositions | `category`, `subcategory`, `content`, `scene`, `expression`, `composition` | Low unless Krea/SDXL needs shorter clothing tags |
| `default_categories.json` men casual subcategories | Male casual outfit/items and men-specific casual pools | Same as above | Medium if men are part of a mixed cast and clothing detail is too strong |
| `default_categories.json` couple casual subcategories | Couple outfit/action-ish soft poses and couple pools | Same as above | Medium because labels and partner styling can duplicate pair mode |
| `erotic_clothes.json` | Provocative/erotic clothing categories and softcore creator scenes | `content`, `scene`, `expression`, `composition` | Medium because nude/implied-nude wording can conflict with clothes |
| `sexual_poses.json` foreplay/oral/outercourse/penetration/etc. | Hardcore action item templates, role graphs, axis values, hardcore pool references | `pose` for pose-content route, also `role`; sometimes `content` aliases matter | High because Krea2 rewrites action and POV position text |
| `location_pools.json` | Reusable scene pools and legacy scene extensions | `scene` | Medium when a camera-aware adapter changes scene/composition wording |
| `expression_composition_pools.json` | Reusable expressions and framing/composition pools | `expression`, `composition` | Medium because formatter may label or suppress expressions |
| `generate_prompt_batches.py` legacy pools | Built-in generator clothing, pose, expression, scene, composition lists | Main row seed plus axis config through legacy adapter | Medium because legacy prompt format is field-label heavy |
When adding a new pool, choose JSON when the change is pure selectable wording.
Choose Python only when selection logic, compatibility filters, camera adaptation,
profile behavior, or formatter rewriting must change.
## Pool Resolution
### Scene / Location
@@ -297,6 +371,69 @@ Continuity:
- `same_creator_new_scene` lets hardcore use its own scene.
- Shared cast descriptors are stored in pair metadata and consumed by formatters.
## Metadata Field Dictionary
The builder outputs JSON metadata because downstream formatters need more than
plain prompt text. When debugging, inspect these fields before editing pools.
### Normal Row Metadata
| Field | Owner | Consumed by | Meaning |
| --- | --- | --- | --- |
| `source` | `build_prompt` / row builder | All formatters | Usually `json_category` or `built_in_generator`; tells which route created the row. |
| `main_category`, `subcategory` | Category selection | All formatters and debug | Human-readable selected category route. |
| `category_slug`, `subcategory_slug` | JSON category normalization | Debug/filtering | Stable-ish machine labels for selected category route. |
| `content_seed_axis` | `_build_custom_row` | Debug | Shows whether the item/action was driven by `content` or `pose`. Critical for hardcore pose categories. |
| `item` | `_compose_item` or Insta override | Krea/SDXL/Naturalizer | Clothing item, category item, or sexual scene/action text. |
| `item_axis_values` | `_compose_item` | Krea hardcore rewrite, SDXL tags | Filled template axes such as position/action/detail values. |
| `custom_item`, `item_label` | Category/pair route | Formatters and debug | Label/name for item route. |
| `role_graph` | `_role_graph`, POV adapter | Krea/Naturalizer | Choreography/action relationship text after POV adaptation. |
| `source_role_graph` | `_role_graph` before POV rewrite | Krea hardcore rewrite | Raw action graph used to infer position and contact. |
| `scene_text` | `_scene_pool` or location config | All formatters | Final location text. |
| `source_scene_text` | location/body-exposure/camera adapters | Debug/continuity | Previous scene text before an override. |
| `location_config` | Location config parser | Debug | Active location pool config, if connected. |
| `pose` | `_pose_pool` or category item route | Formatters | Generic pose text. Less important for hardcore action categories than `item`/`role_graph`. |
| `expression` | `_expression_pool` and intensity filter | All formatters | Final expression text unless disabled. |
| `shared_expression` | Expression selection | Debug | Expression before character-specific expansion. |
| `character_expression_text` | Character slot expression route | Krea/Naturalizer | Per-character expression clauses. |
| `expression_enabled`, `expression_disabled` | Builder/slot override | All formatters | Hard gate for whether expression text should appear. |
| `expression_intensity_source` | Builder/slot override | Debug | Explains whether intensity came from input, random, slot, or disabled state. |
| `composition` | `_composition_pool`, POV/camera adapter | All formatters | Final framing phrase. |
| `source_composition` | Composition adapter | Krea hardcore rewrite | Previous/raw composition, often better for action inference. |
| `composition_config` | Composition config parser | Debug | Active composition pool config, if connected. |
| `camera_config` | Camera nodes/parser | Krea/SDXL/debug | Structured camera settings. |
| `camera_directive` | `_camera_directive` | Krea/Naturalizer/prompt text | Human camera sentence. Suppressed for POV. |
| `camera_scene_directive` | scene-camera adapter | Krea/Naturalizer/prompt text | Location-aware camera layout sentence. |
| `subject_type`, `subject_phrase` | Subject/context builder | Formatters | Single/couple/group/configured cast route. |
| `women_count`, `men_count`, `person_count` | Cast route | Pair/formatters/debug | Effective cast counts. |
| `cast_descriptors`, `cast_descriptor_text` | Character/cast route | Krea/SDXL/Naturalizer | Visible cast descriptors. |
| `character_cast_slots` | Character slot chain | POV/camera/formatters | Raw configured slots. |
| `character_slot_status`, `character_profile_status` | Character/profile application | Debug | Explains whether slot/profile was applied or skipped. |
| `pov_character_labels` | Character slot presence mode | Krea/prompt/camera | Labels omitted from visible cast and rewritten as first-person POV. |
| `hardcore_position_config` | Hardcore position/filter nodes | Debug | Active hardcore family/position/action constraints. |
| `negative_prompt` | Category/pair/default negative route | Formatter output | Base negative text before formatter extras. |
| `trigger` | Builder input | Formatter/fallback/debug | Active trigger after fallback to default. |
### Insta/OF Pair Metadata
| Field | Owner | Consumed by | Meaning |
| --- | --- | --- | --- |
| `mode` | `build_insta_of_pair` | Formatters | `Insta/OF` selects pair formatter branches. |
| `options` | `SxCP Insta/OF Options` | Formatters/debug | Soft/hard level, cast mode, continuity, camera modes, expression settings. |
| `shared_descriptor` | Soft row descriptor | Pair formatters | Primary creator descriptor. |
| `shared_cast_descriptors` | Cast descriptor builder | Pair formatters | Full cast descriptor list. |
| `softcore_row`, `hardcore_row` | Pair route | Pair formatters | Full normal metadata rows for each side. |
| `softcore_prompt`, `hardcore_prompt` | Pair assembly | Direct output/fallback | Raw pair prompts before formatter rewrite. |
| `softcore_negative_prompt`, `hardcore_negative_prompt` | Pair assembly | Formatter negatives | Separate negatives for each side. |
| `softcore_partner_styling` | `_insta_of_partner_styling` | Krea/SDXL pair branch | Partner softcore clothing and pose when same-cast softcore is enabled. |
| `character_hardcore_clothing` | Character slots | Krea pair branch | Explicit per-character hardcore clothing state. |
| `default_man_hardcore_clothing` | Pair fallback | Krea pair branch | Auto clothing for visible men without configured clothing. |
| `hardcore_clothing_state` | Pair clothing continuity | Krea/SDXL pair branch | Final hard clothing/body exposure sentence before Krea cleanup. |
| `hardcore_detail_density` | Insta/OF options | Krea hardcore action rewrite | Controls compact/balanced/dense action detail. |
| `softcore_camera_config`, `hardcore_camera_config` | Pair camera route | Krea/SDXL pair branch | Separate camera configs after option mode resolution. |
| `softcore_camera_directive`, `hardcore_camera_directive` | Pair camera route | Krea pair branch | Separate plain camera sentences, suppressed for POV. |
| `softcore_camera_scene_directive`, `hardcore_camera_scene_directive` | Scene-camera adapter | Krea/Naturalizer pair branch | Separate location-aware camera layout text. |
## Hardcore Position Route
`SxCP Hardcore Position Pool` and `SxCP Hardcore Action Filter` both emit
@@ -380,6 +517,19 @@ Key Krea2 ownership:
- Clothing state cleanup: `_natural_clothing_state`.
- Camera scene preservation: `_camera_scene_phrase`.
Krea2 field consumption:
| Branch | Reads most from | Key functions |
| --- | --- | --- |
| Normal single row | `subject_type`, `item`, `pose`, `scene_text`, `expression`, `composition`, `camera_*`, style fields | `_normal_row_to_krea` |
| Normal configured cast/hardcore row | `cast_descriptor_text`, `women_count`, `men_count`, `source_role_graph`, `role_graph`, `item`, `item_axis_values`, `source_composition`, `pov_character_labels` | `_normal_row_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase` |
| Insta/OF pair softcore | `shared_descriptor`, `softcore_row`, `softcore_partner_styling`, options, soft camera fields | `_insta_pair_to_krea` |
| Insta/OF pair hardcore | `hardcore_row`, `shared_cast_descriptors`, `hardcore_clothing_state`, `hardcore_detail_density`, hard camera fields, POV labels | `_insta_pair_to_krea`, `_hardcore_action_sentence`, `_pov_action_phrase`, `_natural_clothing_state` |
| Plain text fallback | `source_text` only | `_fallback_text_to_krea` |
If metadata is connected and `method` says `text(fallback)`, the formatter did
not parse metadata. That is a wiring/input-hint issue, not a prompt pool issue.
### SDXL
`format_sdxl_prompt` chooses between:
@@ -391,6 +541,19 @@ Key Krea2 ownership:
Use this route for style triggers, weighted tag style, nude weighting, and Pony /
SDXL quality/style presets.
SDXL field consumption:
| Branch | Reads most from | Key functions |
| --- | --- | --- |
| Normal metadata | cast descriptors, age/body/skin/hair/eyes, item, role graph, scene, camera config/directive | `_row_core_tags`, `_appearance_tags`, `_camera_tags` |
| Pair softcore | `softcore_row`, pair partner styling, root soft camera config | `_soft_tags` |
| Pair hardcore | `hardcore_row`, `hardcore_clothing_state`, hard camera fields, hard prompt text | `_hard_tags` |
| Text fallback | `source_text`, preserve-trigger setting | `_fallback_text_to_sdxl` |
SDXL is the right place for model trigger handling, tag ordering, weight syntax,
quality/style preset changes, and nude-weight defaults. Do not solve those in
JSON category pools unless the raw builder text is also wrong.
### Naturalizer
`naturalize_caption` chooses metadata-specific renderers such as
@@ -399,6 +562,15 @@ SDXL quality/style presets.
Use this route when the row metadata is correct but the sentence-style caption is
too mechanical or unsuitable for training captions.
Naturalizer field consumption:
| Branch | Reads most from | Key functions |
| --- | --- | --- |
| Normal single/couple/group | subject fields, age/body, item, scene, expression, composition, camera scene | `_single_from_row`, `_couple_from_row`, `_group_or_layout_from_row` |
| Configured cast/hardcore | `cast_descriptor_text`, `role_graph`, `item`, `scene_text`, expression, composition | `_configured_cast_from_row` |
| Insta/OF pair | `softcore_row`, `hardcore_row`, pair options and continuity | `_insta_pair_from_row` |
| Text fallback | `caption` or `prompt` text | `_text_to_prose` |
## Utility / Workflow Nodes
These do not own prompt pool wording, but they affect execution and review:
@@ -411,6 +583,24 @@ These do not own prompt pool wording, but they affect execution and review:
| Persistent text preview | `loop_nodes.py`, `web/preview_any_text.js` | Stores any value as text and keeps it after workflow reload. |
| SDXL bucket size | `SxCPSDXLBucketSize` in `__init__.py` | Random/fixed SDXL bucket width and height selection. |
## Drift Audit Helper
The map should be checked when adding nodes, category files, or named pools.
Run:
```bash
python tools/prompt_map_audit.py
```
The script does not import ComfyUI. It parses the repo and prints:
- registered display node names and known return names;
- per-JSON category counts;
- named scene/expression/composition pool inventory.
Use its output to spot doc drift after adding a new node or pool. If a new node
or pool appears there but not in this map, update the relevant route table.
## Editing Cheatsheet
| Symptom | First file/function to inspect |
@@ -433,6 +623,80 @@ These do not own prompt pool wording, but they affect execution and review:
| Saved profile does not match liked character | Profile save/load path and whether the saved input is row metadata or regenerated slot config. |
| Accumulator preview behavior wrong | `loop_nodes.py` accumulator methods and `web/accumulator_preview.js`. |
## Debug Route Traces
Use these traces to narrow a problem in one pass.
### Hardcore action keeps selecting the same family
1. Check metadata `main_category`, `subcategory`, `content_seed_axis`,
`hardcore_position_config`, `item`, `role_graph`, and `item_axis_values`.
2. If `hardcore_position_config` disabled most families, the repeated action may
be the only compatible pool left.
3. Inspect `categories/sexual_poses.json` for the selected subcategory,
`item_templates`, `axes`, and `weight`.
4. If raw `item` differs but Krea output looks identical, inspect
`_hardcore_pose_anchor`, `_hardcore_pose_arrangement`,
`_hardcore_item_detail`, and `_hardcore_action_sentence`.
### POV position is spatially wrong
1. Confirm `pov_character_labels` includes the intended man label.
2. Confirm Krea input uses metadata, not plain prompt fallback.
3. Inspect `source_role_graph`, `item`, `source_composition`, and
`item_axis_values`.
4. Edit `_pov_hardcore_pose_sentence` if the first-person body geometry is
wrong.
5. Edit `sexual_poses.json` if the raw action lacks enough body-position anchor
for any formatter to infer a good POV prompt.
### Camera disappears or becomes too generic
1. Check row `camera_config`, `camera_directive`, and `camera_scene_directive`.
2. If `camera_detail=off` or `camera_mode=disabled`, missing camera text is
expected.
3. If POV labels exist, plain `camera_directive` is intentionally suppressed.
4. If a location-aware sentence is missing, inspect
`_camera_scene_directive_for_context` and the scene detector for that
location family.
5. If raw metadata has camera text but Krea omits it, inspect `_camera_phrase`,
`_pair_camera_phrase`, and `_camera_scene_phrase`.
### Nude/clothing wording conflicts
1. Check pair root `hardcore_clothing_state`.
2. Check hard row `item` and `source_role_graph` for access flags.
3. Character slot `hardcore_clothing` overrides pair fallback clothing.
4. For Krea wording, inspect `_natural_clothing_state`.
5. For generation wording, inspect `_insta_of_hardcore_clothing_state`,
`_hardcore_row_access_flags`, and `character_hardcore_clothing_values`.
### Softcore contains strange no-contact or bed/action leakage
1. Check whether the prompt came from pair softcore or normal category builder.
2. In pair softcore, inspect `softcore_partner_styling`, `softcore_row.item`,
`softcore_row.pose`, and options `softcore_cast`.
3. If the raw soft prompt contains awkward defensive clauses, fix
`build_insta_of_pair` soft prompt assembly.
4. If Krea adds the awkwardness, inspect `_insta_pair_to_krea`.
### Location composition mentions irrelevant props
1. Check `scene_text` and `composition` separately.
2. If scene is good and composition is bad, edit composition pools, not
location pools.
3. If a scene-camera adapter rewrote composition, inspect
`_coworking_composition_prompt` or the future adapter for that scene family.
4. If the issue comes from `Location Theme`, edit `THEMATIC_LOCATION_PRESETS`.
### Trigger missing after formatting
1. For builder raw prompts, check `trigger` and `prepend_trigger_to_prompt`.
2. For Krea fallback, check `preserve_trigger`; metadata route usually rebuilds
prose and does not use prompt text as a raw string.
3. For SDXL, trigger handling belongs to `format_sdxl_prompt` style assembly:
`trigger`, `prepend_trigger_to_prompt`, `preserve_trigger`, and style preset.
## Safe Edit Workflow
Before changing prompt behavior:
+154
View File
@@ -0,0 +1,154 @@
#!/usr/bin/env python3
"""Print a lightweight audit for the prompt routing map.
This intentionally avoids importing the ComfyUI node package. It parses Python
and JSON files directly, so it can run in a plain shell without ComfyUI loaded.
"""
from __future__ import annotations
import ast
import json
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
def _literal_or_none(node: ast.AST) -> Any:
try:
return ast.literal_eval(node)
except Exception:
return None
def _assignment_dict(path: Path, name: str) -> dict[str, Any]:
tree = ast.parse(path.read_text(encoding="utf-8"))
for node in tree.body:
if not isinstance(node, ast.Assign):
continue
if not any(isinstance(target, ast.Name) and target.id == name for target in node.targets):
continue
value = _literal_or_none(node.value)
return value if isinstance(value, dict) else {}
return {}
def _class_return_names(path: Path) -> dict[str, tuple[str, ...]]:
tree = ast.parse(path.read_text(encoding="utf-8"))
result: dict[str, tuple[str, ...]] = {}
for node in tree.body:
if not isinstance(node, ast.ClassDef) or not node.name.startswith("SxCP"):
continue
for item in node.body:
if not isinstance(item, ast.Assign):
continue
if not any(isinstance(target, ast.Name) and target.id == "RETURN_NAMES" for target in item.targets):
continue
value = _literal_or_none(item.value)
if isinstance(value, tuple) and all(isinstance(part, str) for part in value):
result[node.name] = value
return result
def _category_summary(path: Path) -> dict[str, Any]:
data = json.loads(path.read_text(encoding="utf-8"))
categories = data.get("categories") or []
subcategory_count = 0
item_template_count = 0
for category in categories:
subcategories = category.get("subcategories") or []
subcategory_count += len(subcategories)
for subcategory in subcategories:
item_template_count += len(subcategory.get("item_templates") or [])
for item in subcategory.get("items") or []:
if isinstance(item, dict):
item_template_count += len(item.get("item_templates") or [])
return {
"categories": len(categories),
"subcategories": subcategory_count,
"item_templates": item_template_count,
"scene_pools": len(data.get("scene_pools") or {}),
"expression_pools": len(data.get("expression_pools") or {}),
"composition_pools": len(data.get("composition_pools") or {}),
"pool_extensions": len(data.get("pool_extensions") or {}),
}
def _pool_names(path: Path, key: str) -> list[str]:
data = json.loads(path.read_text(encoding="utf-8"))
pools = data.get(key) or {}
return sorted(pools) if isinstance(pools, dict) else []
def print_table(headers: tuple[str, ...], rows: list[tuple[Any, ...]]) -> None:
widths = [len(header) for header in headers]
for row in rows:
for index, value in enumerate(row):
widths[index] = max(widths[index], len(str(value)))
print("| " + " | ".join(header.ljust(widths[index]) for index, header in enumerate(headers)) + " |")
print("| " + " | ".join("-" * width for width in widths) + " |")
for row in rows:
print("| " + " | ".join(str(value).ljust(widths[index]) for index, value in enumerate(row)) + " |")
def main() -> int:
init_path = ROOT / "__init__.py"
loop_path = ROOT / "loop_nodes.py"
display = _assignment_dict(init_path, "NODE_DISPLAY_NAME_MAPPINGS")
loop_display = _assignment_dict(loop_path, "LOOP_NODE_DISPLAY_NAME_MAPPINGS")
display.update(loop_display)
returns = _class_return_names(init_path)
returns.update(_class_return_names(loop_path))
print("# Node Display Map")
node_rows = []
for class_name, display_name in sorted(display.items(), key=lambda item: str(item[1])):
return_names = ", ".join(returns.get(class_name, ()))
node_rows.append((display_name, class_name, return_names or "(dynamic or unnamed)"))
print_table(("Display name", "Class", "Return names"), node_rows)
print("\n# Category JSON Summary")
category_rows = []
for path in sorted((ROOT / "categories").glob("*.json")):
summary = _category_summary(path)
category_rows.append(
(
path.name,
summary["categories"],
summary["subcategories"],
summary["item_templates"],
summary["scene_pools"],
summary["expression_pools"],
summary["composition_pools"],
summary["pool_extensions"],
)
)
print_table(
(
"File",
"Categories",
"Subcategories",
"Item templates",
"Scene pools",
"Expression pools",
"Composition pools",
"Extensions",
),
category_rows,
)
print("\n# Named Pool Inventory")
pool_rows = []
for path in sorted((ROOT / "categories").glob("*.json")):
for key in ("scene_pools", "expression_pools", "composition_pools"):
names = _pool_names(path, key)
if names:
pool_rows.append((path.name, key, len(names), ", ".join(names[:8]) + (" ..." if len(names) > 8 else "")))
print_table(("File", "Pool type", "Count", "First names"), pool_rows)
return 0
if __name__ == "__main__":
raise SystemExit(main())