From 3a3771eed5f9f8fd3da77896f79a52a4dda939a8 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 12:22:25 +0200 Subject: [PATCH] Expand prompt routing map --- docs/prompt-pool-routing-map.md | 264 ++++++++++++++++++++++++++++++++ tools/prompt_map_audit.py | 154 +++++++++++++++++++ 2 files changed, 418 insertions(+) create mode 100644 tools/prompt_map_audit.py diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index e250ac4..081da66 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -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: diff --git a/tools/prompt_map_audit.py b/tools/prompt_map_audit.py new file mode 100644 index 0000000..3bc0f52 --- /dev/null +++ b/tools/prompt_map_audit.py @@ -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())