From d7caf1c2708c0e3f607bd564bb5ff85f0ae57e4c Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Sat, 27 Jun 2026 11:37:02 +0200 Subject: [PATCH] Extract node tooltip policy --- __init__.py | 370 +------------------ docs/prompt-architecture-improvement-plan.md | 3 + docs/prompt-pool-routing-map.md | 1 + node_tooltips.py | 367 ++++++++++++++++++ tools/prompt_smoke.py | 10 + 5 files changed, 385 insertions(+), 366 deletions(-) create mode 100644 node_tooltips.py diff --git a/__init__.py b/__init__.py index fe42bda..729e429 100644 --- a/__init__.py +++ b/__init__.py @@ -1,9 +1,5 @@ from __future__ import annotations -import json -import random -import re - try: from aiohttp import web from server import PromptServer @@ -29,369 +25,11 @@ SXCP_CHARACTER_CAST = "SXCP_CHARACTER_CAST" SXCP_CHARACTER_SLOT = "SXCP_CHARACTER_SLOT" SXCP_CHARACTER_PROFILE = "SXCP_CHARACTER_PROFILE" -COMMON_INPUT_TOOLTIPS = { - "row_number": "Generation row to use. Changing it advances the deterministic selection without changing the main seed.", - "start_index": "Metadata/output index offset only. It does not limit category pools or random choices.", - "seed": "Main seed used when no more specific seed config overrides an axis.", - "global_seed": "One seed that locks all prompt axes so the same inputs can recreate the same result.", - "base_seed": "Base seed used by Seed Locker before applying a selected reroll axis.", - "reroll_seed": "Seed for the selected reroll axis. Use -1 to derive it from the base seed.", - "category": "Main category source. auto_weighted is legacy random; auto_full mixes legacy random with JSON categories including hardcore.", - "subcategory": "Specific subcategory, or random to choose within the selected category.", - "category_config": "Category/subcategory config from SxCP Category Preset.", - "cast_config": "Cast size config from SxCP Cast Control.", - "generation_profile": "General style/intensity profile from SxCP Generation Profile.", - "filter_config": "Ethnicity/body filter config. Ethnicity List can feed this too.", - "ethnicity_list": "Optional ethnicity pool. When connected, it overrides the slot or generator ethnicity picker.", - "seed_config": "Per-axis seed config. Connect Global Seed, Seed Locker, or Seed Control here.", - "camera_config": "Camera config used by the prompt formatter when camera mode is from_camera_config.", - "location_config": "Location config from SxCP Location Pool. It can replace or add to the category scene pool.", - "composition_config": "Composition config from SxCP Composition Pool or Location Theme. It can replace or add framing options.", - "softcore_camera_config": "Camera config used only for the softcore Insta/OF prompt. Falls back to camera_config if empty.", - "hardcore_camera_config": "Camera config used only for the hardcore Insta/OF prompt. Falls back to camera_config if empty.", - "character_profile": "Saved or loaded single-character profile. Character slots override this for configured casts.", - "character_cast": "Chain character slots here. The node closest to the final generator becomes the next auto_chain label.", - "character_slot": "Single slot payload for saving/loading profiles or debugging one character.", - "hardcore_position_config": "Hardcore action/position config. Chain Position Pool into Action Filter, then into the generator.", - "custom_locations": "One custom location per line. Use plain text, or slug: location text.", - "custom_compositions": "One custom composition/framing phrase per line.", - "theme": "Matched location and composition theme, useful when the place needs compatible framing.", - "metadata_json": "Structured metadata from an SxCP generator. Prefer this over raw prompt text for formatters and profile save.", - "source_text": "Raw prompt, caption, or metadata JSON depending on input_hint.", - "source_text_input": "Optional linked raw prompt/caption input. When connected, it overrides the source_text widget.", - "input_hint": "Tells the node how to interpret source_text. auto tries metadata first.", - "target": "For dual prompts, choose which side to output as the main Krea prompt.", - "detail_level": "Controls how much detail the rewriter keeps. concise is shorter, dense keeps more clauses.", - "style_mode": "How strongly the formatter rewrites visual style terms.", - "preserve_trigger": "Keep the trigger token in the formatted prompt instead of stripping it.", - "negative_prompt": "Negative prompt text to pass through or merge with generated negatives.", - "extra_positive": "Extra positive text appended after the generated prompt.", - "extra_negative": "Extra negative text appended after the generated negative prompt.", - "trigger": "Training or style trigger token.", - "prepend_trigger_to_prompt": "If enabled, put the trigger token at the start of generated prompts.", - "bucket_index": "0 picks a random bucket. 1+ picks that position inside the selected orientation pool.", - "megapixels": "Approximate megapixel count for the selected bucket.", - "enabled": "Enable this node's effect while keeping it wired in the graph.", - "combine_mode": "replace starts a new pool/config; add merges selected values into the incoming config.", - "manual": "Manual character details config. Non-empty manual fields override generated slot details.", - "characteristics": "Chainable character characteristic pool such as age/body/eyes/clothing.", - "hair_config": "Chainable hair pool. Combine length, color, and style nodes before the character slot.", - "summary": "Human-readable description of the config produced by this node.", - "status": "Operation result or warning text.", - "profile_name": "Name of the profile to save, load, rename, or delete.", - "manual_profile_name": "Free-text profile name used when profile_name is set to manual.", - "fallback_profile_json": "Profile JSON to use when a named profile cannot be loaded.", - "rename_to": "New profile name used only when rename_now is enabled.", - "save_now": "Writes the profile to disk only when enabled. Keep off while adjusting fields.", - "delete_now": "Deletes the selected profile when enabled.", - "rename_now": "Renames the selected profile when enabled.", - "source": "Where the save node reads character data from.", - "subject_type": "Character type for this slot or saved profile.", - "label": "Character label. auto_chain assigns the next Woman/Man label based on incoming cast order.", - "slot_seed": "Per-character seed. Use -1 to follow the generator person seed.", - "age": "Age choice for this slot. Use Age Range node for a custom random age pool.", - "manual_age": "Exact age phrase override, for example '32-year-old adult'.", - "ethnicity": "Ethnicity choice for this slot. A connected Ethnicity List overrides this picker.", - "figure": "General figure bias for generated body descriptors.", - "figure_bias": "Woman-slot figure bias. Body pool can give more precise body choices.", - "women_count": "Number of women in the generated cast when no Insta/OF preset overrides it.", - "men_count": "Number of men in the generated cast when no Insta/OF preset overrides it.", - "hardcore_women_count": "Number of women in the hardcore cast when hardcore_cast is use_counts.", - "hardcore_men_count": "Number of men in the hardcore cast when hardcore_cast is use_counts.", - "body": "Body choice for this slot. A Body Pool node can replace the random list.", - "manual_body": "Exact body phrase override.", - "body_phrase": "Full custom body wording. Use only when the body picker is not specific enough.", - "skin": "Manual skin/complexion phrase.", - "hair": "Manual hair phrase. Hair config nodes are better for controlled random choices.", - "eyes": "Manual eye description.", - "descriptor_detail": "How detailed this character's descriptor should be. Men usually work better compact.", - "expression_enabled": "Master expression toggle for this generator or character.", - "expression_intensity": "Expression intensity from 0 to 1. On the direct builder, -1 randomizes per row; on slots, -1 inherits the generator setting.", - "expression_intensity_mode": "For Generation Profile, choose profile_default, random, or fixed value from expression_intensity.", - "softcore_expression_intensity": "Optional expression intensity override for this character in softcore prompts. -1 inherits.", - "hardcore_expression_intensity": "Optional expression intensity override for this character in hardcore prompts. -1 inherits.", - "presence_mode": "Controls whether the character is visible, implied POV, or otherwise present.", - "softcore_outfit": "Manual softcore outfit text for this character.", - "hardcore_clothing": "Manual hardcore clothing/body exposure text for this character.", - "custom_softcore_outfits": "One custom softcore outfit per line. Used when softcore_source is custom.", - "custom_hardcore_clothing": "One custom hardcore clothing/body exposure state per line.", - "condition": "Loop condition. When false, the loop stops and passes current values through.", - "total": "Total number of loop iterations.", - "skip": "Number of leading loop indexes to skip. skip=1 starts generation at index 2.", - "collection": "Existing accumulated value or batch.", - "value": "Value to append, store, or pass through.", - "store_key": "Accumulator memory key. Same key shares stored entries across executions.", - "store_key_input": "Connect SxCP Accumulator store_key here so preview/delete/save uses the same accumulator and graph dependency.", - "action": "Accumulator operation: append, replace, clear, read, or append a variant.", - "max_items": "Maximum stored entries kept in this accumulator.", - "image_batch_mode": "How image entries are batched when dimensions differ.", - "skip_empty": "Ignore empty inputs instead of adding blank entries.", - "image": "Image to store in the accumulator.", - "entry_id": "Stable ID used for replace_by_entry_id or grouping variants.", - "entry_tag": "Optional suffix added to entry_id.", - "preview_limit": "Maximum number of accumulator images to show in the preview panel.", - "view_mode": "Accumulator Preview layout: grid shows many images, carousel shows one large image at a time.", - "zoom_level": "Accumulator Preview image scale. Higher values make grid thumbnails or carousel image area larger.", - "carousel_index": "1-based image position shown in carousel mode. The previous/next buttons update this value.", - "delete_action": "Optional execution-time delete operation. JS buttons can delete interactively without setting this.", - "delete_entry_id": "Entry id to delete when delete_action is delete_entry_id.", - "delete_index": "1-based entry index to delete when delete_action is delete_index. 0 disables it.", - "save_batch": "When enabled, save all current accumulator images once finished is true.", - "finished": "Gate for saving. Outside a loop, leave true; inside a loop, wire a final-iteration signal.", - "save_path": "Folder to save the accumulator batch. Relative paths are inside ComfyUI output; absolute paths are used directly.", - "filename_prefix": "Filename prefix for saved accumulator images.", - "clear_after_save": "Clear the accumulator store after a successful batch save.", - "preview_text": "Serialized persistent text preview. It is updated after execution and saved with the workflow.", - "preview_format": "How to convert an arbitrary input to preview text.", - "max_chars": "Maximum stored preview characters. 0 disables truncation.", - "mode": "Switch direction: pick_input selects one input to value, route_output sends route_value to one output.", - "index": "Index used by SxCP Index Switch. For Loop Start outputs one_based indexes by default.", - "index_base": "one_based means index 1 selects input_1. zero_based means index 0 selects input_1.", - "missing_behavior": "What to do when the requested switch input is not connected: use fallback, output none, clamp, or wrap.", - "fallback": "Optional value used by SxCP Index Switch when the requested input is missing and missing_behavior is fallback.", - "route_value": "Value routed to output_N when mode is route_output.", - "clothing": "Built-in clothing density for legacy direct generation. random picks full/minimal from the seeded row.", - "poses": "Built-in pose pool for legacy direct generation. random picks standard/evocative from the seeded row.", - "backside_bias": "Legacy bias toward rear/backside poses where that category supports it.", - "minimal_clothing_ratio": "Legacy weighted ratio override. -1 keeps the category/profile default.", - "standard_pose_ratio": "Legacy weighted ratio override. -1 keeps the category/profile default.", - "profile": "Generation profile preset for broad style, clothing, pose, and expression defaults.", - "clothing_override": "Override the profile clothing setting, or leave profile_default.", - "poses_override": "Override the profile pose setting, or leave profile_default.", - "trigger_policy": "Controls whether the profile prepends the trigger token.", - "cast_mode": "Preset cast shape. Custom counts are used when the preset allows them.", - "women_weights": "Comma-separated count weights. First value maps to women_start_count, second to +1, and so on.", - "men_weights": "Comma-separated count weights. First value maps to men_start_count, second to +1, and so on.", - "women_start_count": "Woman count represented by the first women_weights value.", - "men_start_count": "Man count represented by the first men_weights value.", - "empty_behavior": "What to do if the weighted pick selects zero women and zero men.", - "preset": "Category preset for common workflow lanes.", - "camera_mode": "Camera style preset.", - "shot_size": "How much of the body/frame should be visible.", - "angle": "Camera angle relative to the subject.", - "lens": "Lens wording to include in the prompt.", - "distance": "Camera distance wording.", - "orientation": "Horizontal/vertical framing wording.", - "phone_visibility": "Whether the prompt mentions a visible/hidden phone.", - "priority": "How strictly the prompt should enforce the camera wording.", - "camera_detail": "off omits camera text, compact keeps one line, full emits detailed camera wording.", - "subject_focus": "Optional camera focus phrase, such as face/body/contact emphasis.", - "strict_excludes": "When enabled, only selected ethnicity groups are used. When off, selections act more like soft includes.", - "min_age": "Minimum adult age in this custom age pool.", - "max_age": "Maximum adult age in this custom age pool.", - "softcore_source": "Softcore outfit source for this character. custom reads custom_softcore_outfits.", - "hardcore_state": "Hardcore clothing/body exposure state for this character.", - "softcore_expression_enabled": "Enable expression text in the softcore prompt.", - "hardcore_expression_enabled": "Enable expression text in the hardcore prompt.", - "flow": "Loop flow-control socket. Wire from the matching loop start node.", - "collection_mode": "How the loop end collects per-iteration values.", - "skip_none": "Do not add empty values to the collection.", - "collected": "Current accumulated value carried through the loop.", - "collect_value": "Value captured from the current loop iteration.", - "a": "First integer/boolean helper input.", - "b": "Second integer/boolean helper input.", -} +try: + from .node_tooltips import install_input_tooltips as _install_input_tooltips +except ImportError: + from node_tooltips import install_input_tooltips as _install_input_tooltips -NODE_INPUT_TOOLTIPS = { - "SxCPSeedControl": { - "category_seed_mode": "auto/follow_main follows the main seed; fixed uses category_seed; random rerolls this axis each queue.", - "subcategory_seed_mode": "Controls which subcategory is selected. Change this to switch oral vs penetration when both are allowed.", - "content_seed_mode": "Controls item/outfit content for non-pose categories.", - "person_seed_mode": "Controls generated character appearance unless a slot seed overrides it.", - "scene_seed_mode": "Controls location/scene selection.", - "pose_seed_mode": "Controls pose/item selection for pose categories, including hardcore positions.", - "role_seed_mode": "Controls role assignment and secondary action details.", - "expression_seed_mode": "Controls selected expression text.", - "composition_seed_mode": "Controls framing/composition text.", - }, - "SxCPSeedLocker": { - "reroll_axis": "Choose the one axis to change while the rest stays locked. Use pose for sexual pose, scene for location, person for appearance.", - }, - "SxCPCastBias": { - "seed": "Fixed cast-bias seed. Use -1 for a fresh cast each queue, or connect Global Seed/Seed Locker through seed_config.", - "seed_config": "Optional seed config. The category seed controls weighted cast selection.", - "women_weights": "Example with women_start_count=1: 0.6,0.25,0.1 means 60% one woman, 25% two women, 10% three women.", - "men_weights": "Example with men_start_count=0: 0.5,0.35,0.1 means 50% no man, 35% one man, 10% two men.", - "empty_behavior": "Prevents accidental empty casts when both weighted pools pick zero.", - }, - "SxCPSDXLBucketSize": { - "orientation": "Bucket orientation filter. any uses the full table; portrait/square/landscape restrict random selection.", - "seed": "Fixed bucket seed. Use -1 for a fresh random bucket each queue, or connect Global Seed for reproducible sizes.", - "row_number": "Deterministic row offset for the bucket. With a fixed seed, changing this advances the bucket choice.", - "bucket_index": "0=random. 1+ selects that bucket position inside the selected orientation pool and ignores seed.", - "seed_config": "Optional seed config. The composition seed controls bucket choice, so Seed Locker can keep sizes fixed while rerolling pose/person.", - }, - "SxCPKrea2ResolutionSelector": { - "megapixels": "Target megapixel preset. If it cannot fit the aspect ratio under the 2K Krea2 Turbo limit, the node clamps to the maximum valid size.", - "aspect_ratio": "Krea API ratios are listed first; local-only helper ratios like 8:9 are included after them.", - }, - "SxCPCameraControl": { - "camera_mode": "Camera style preset. Use from_camera_config in Insta/OF options to consume this.", - "priority": "locked makes the camera wording strict; soft_hint allows the model more freedom.", - "camera_detail": "off omits camera text, compact keeps one short line, full emits detailed camera constraints.", - "phone_visibility": "Use phone_hidden or suppress_phone_visibility when you do not want 'phone hidden' text in prompts.", - }, - "SxCPCameraOrbitControl": { - "horizontal_angle": "Orbit angle in degrees. 0=front, 90=right side, 180=back, 270=left side.", - "vertical_angle": "Camera elevation. Negative looks up, positive looks down.", - "zoom": "Maps to distance/framing when framing is from_zoom.", - "framing": "How zoom should be translated into shot size/distance wording.", - "include_degrees": "Include numeric degree wording in addition to human camera direction.", - }, - "SxCPQwenCameraTranslator": { - "qwen_prompt": "Camera prompt from Qwen MultiAngle, for example ' front-right quarter view eye-level shot medium shot'.", - "camera_info": "Optional structured camera_info from Qwen MultiAngle. Used before qwen_prompt when prefer_camera_info is true.", - "prefer_camera_info": "Use structured camera_info values when available instead of parsing the text prompt.", - "suppress_phone_visibility": "Avoid adding phone visibility text unless you explicitly set a phone option.", - }, - "SxCPHardcorePositionPool": { - "family": "Restrict the broad hardcore family. Use any when you want oral and penetration to both be possible.", - "combine_mode": "replace discards incoming position choices; add merges these choices with the incoming config.", - "hardcore_position_config": "Optional incoming config. Usually connect previous Position Pool here only when chaining pools.", - }, - "SxCPHardcoreActionFilter": { - "focus": "keep_pool preserves/broadens the incoming pool; *_only modes force one action family.", - "allow_toys": "Allow toy/strap-on wording in hardcore actions.", - "allow_double": "Allow double-penetration or second-contact wording.", - "allow_penetration": "Allow vaginal/penetrative sex subcategories.", - "allow_foreplay": "Allow hardcore teasing/foreplay setup actions such as kissing, caressing, breast/face touching, and undressing.", - "allow_interaction": "Allow non-act interaction pools such as body worship, clothing transitions, guidance, camera presentation, watching, and aftercare.", - "allow_manual": "Allow manual stimulation pools such as fingering, clit rubbing, and mutual masturbation.", - "allow_oral": "Allow oral sex subcategories.", - "allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.", - "allow_anal": "Allow anal subcategories.", - "allow_climax": "Allow cumshot/climax aftermath subcategories.", - }, - "SxCPInstaOFOptions": { - "softcore_cast": "solo keeps softcore focused on Woman A; same_as_hardcore includes the same cast as the hardcore prompt.", - "hardcore_cast": "use_counts reads hardcore_women_count/hardcore_men_count; presets set the counts automatically.", - "softcore_level": "Controls the soft prompt exposure/outfit level.", - "hardcore_level": "Controls how explicit the hardcore prompt style is.", - "platform_style": "Instagram/OnlyFans styling bias for the dual prompt pair.", - "continuity": "Whether the softcore and hardcore prompts share the room/creator setup.", - "hardcore_clothing_continuity": "How clothing carries from softcore to hardcore. explicit_nude omits clothing references.", - "softcore_camera_mode": "Camera mode for the softcore prompt, or from_camera_config.", - "hardcore_camera_mode": "Camera mode for the hardcore prompt. same_as_softcore reuses the softcore setting.", - "camera_detail": "Global camera verbosity for the pair unless a camera config overrides it.", - "hardcore_detail_density": "How dense the hardcore action sentence should be in the Krea formatter.", - }, - "SxCPInstaOFPromptPair": { - "options_json": "Options from SxCP Insta/OF Options. If empty, defaults are used.", - "ethnicity": "Fallback ethnicity when no filter/ethnicity list or character slots are connected.", - "figure": "Fallback figure bias when no character slot overrides it.", - }, - "SxCPPromptBuilderFromConfigs": { - "seed": "Main seed. Connect Seed Config for per-axis control.", - }, - "SxCPCharacterProfileSave": { - "profile_name": "Profile filename stem. Saving requires save_now=true.", - "metadata_json": "Use generator metadata to save the currently generated character without regenerating it.", - "character_slot": "Use this when saving a configured slot directly.", - }, - "SxCPCharacterProfileLoad": { - "enabled": "When false, outputs an empty profile and leaves downstream generation unchanged.", - "override_age": "Optional loaded-profile override. Empty keeps the profile value.", - "override_body": "Optional body override. Empty keeps the profile value.", - "override_descriptor_detail": "Override descriptor verbosity while keeping the rest of the loaded profile.", - }, - "SxCPKrea2Formatter": { - "metadata_json": "Best input for Krea2 formatting because it preserves cast, camera, and hardcore action metadata.", - "preserve_trigger": "Reminder: Krea2 formatting is intended to remove training/style triggers. Leave false unless you intentionally want a raw text trigger preserved.", - "source_text": "Raw prompt fallback. Known trigger tokens are stripped by default for Krea2.", - }, - "SxCPSDXLFormatter": { - "metadata_json": "Best input for SDXL tag formatting because it preserves cast, camera, outfit, and explicit action metadata.", - "formatter_profile": "High-level formatter defaults. manual_controls keeps style_preset and quality_preset authoritative.", - "style_preset": "Positive style anchor preset. flat_vector_pony matches the old SDXL tag style.", - "quality_preset": "Quality/score tag tail for SDXL or Pony-style checkpoints.", - "custom_style": "Optional replacement for the style preset. Leave empty to use style_preset.", - "custom_quality": "Optional replacement for the quality preset. Leave empty to use quality_preset.", - "nude_weight": "Weight used when explicit nude/body exposure tags are inferred.", - }, - "SxCPCaptionNaturalizer": { - "metadata_json": "Best input for training captions because it preserves structured generator details.", - "caption_profile": "Preset behavior for the caption rewrite. manual_controls keeps detail/style/include-trigger widgets authoritative.", - "style_policy": "drop_style_tail removes generation/style boilerplate; keep_style_terms preserves more of it.", - "include_trigger": "Keep this true for LoRA/training captions so the trigger token is learned.", - }, - "SxCPForLoopStart": { - "index": "Output loop index. First generated index is skip + 1.", - "collected": "Current accumulated value carried through the loop.", - }, - "SxCPLoopAppend": { - "mode": "auto_batch tries tensor/latent batching first, then falls back to a list.", - }, - "SxCPAccumulator": { - "image_batch_mode": "same_size_only keeps incompatible sizes separate; resize_to_first forces one image batch.", - }, -} - - -def _tooltip_for_input(node_name: str, input_name: str) -> str: - node_tooltips = NODE_INPUT_TOOLTIPS.get(node_name, {}) - if input_name in node_tooltips: - return node_tooltips[input_name] - if input_name in COMMON_INPUT_TOOLTIPS: - return COMMON_INPUT_TOOLTIPS[input_name] - if input_name.endswith("_seed_mode"): - axis = input_name[: -len("_seed_mode")] - return f"How the {axis} seed is resolved: follow the main seed, use the fixed field, or reroll randomly." - if input_name.endswith("_seed"): - axis = input_name[: -len("_seed")] - return f"Fixed {axis} seed value. Used only when the matching seed mode is fixed, or as a fallback for auto modes." - if input_name.startswith("include_"): - value = input_name[len("include_") :].replace("_", " ") - return f"Include {value} in this random pool." - if input_name.startswith("initial_value"): - return "Carry value passed into the loop body and returned on the matching output." - if re.match(r"^input_\d+$", input_name): - return "Autoscaling switch input. Connect the last visible input to reveal the next one." - if re.match(r"^output_\d+$", input_name): - return "Autoscaling routed output. Connect the last visible output to reveal the next one." - if input_name.startswith("override_"): - return "Optional loaded-profile override. Leave empty or keep_profile to preserve the profile value." - return "" - - -def _copy_input_spec_with_tooltip(input_spec, tooltip: str): - if not tooltip or not isinstance(input_spec, tuple): - return input_spec - if len(input_spec) >= 2 and isinstance(input_spec[1], dict): - options = dict(input_spec[1]) - options.setdefault("tooltip", tooltip) - return (input_spec[0], options, *input_spec[2:]) - if len(input_spec) == 1: - return (input_spec[0], {"tooltip": tooltip}) - return input_spec - - -def _inject_input_tooltips(input_types: dict, node_name: str) -> dict: - patched = dict(input_types) - for group_name in ("required", "optional"): - group = patched.get(group_name) - if not isinstance(group, dict): - continue - patched_group = {} - for input_name, input_spec in group.items(): - patched_group[input_name] = _copy_input_spec_with_tooltip( - input_spec, - _tooltip_for_input(node_name, input_name), - ) - patched[group_name] = patched_group - return patched - - -def _install_input_tooltips(node_classes: dict[str, type]) -> None: - for node_name, node_class in node_classes.items(): - original = getattr(node_class, "INPUT_TYPES", None) - if original is None or getattr(node_class, "_sxcp_tooltips_installed", False): - continue - - def input_types(cls, _original=original, _node_name=node_name): - return _inject_input_tooltips(_original(), _node_name) - - node_class.INPUT_TYPES = classmethod(input_types) - node_class._sxcp_tooltips_installed = True try: from .loop_nodes import ( diff --git a/docs/prompt-architecture-improvement-plan.md b/docs/prompt-architecture-improvement-plan.md index 3032b7a..ad8bb23 100644 --- a/docs/prompt-architecture-improvement-plan.md +++ b/docs/prompt-architecture-improvement-plan.md @@ -533,6 +533,9 @@ Already isolated: route-output selection, status text, and lazy-input selection live in `index_switch_policy.py`; `loop_nodes.py` keeps the ComfyUI node wrapper and accumulator/loop runtime logic. +- node input tooltip inventory, node-specific tooltip overrides, dynamic input + fallback tooltip rules, and tooltip injection live in `node_tooltips.py`; + `__init__.py` only applies the installer to the assembled node registry. - profile-save and accumulator server payload handling lives in `server_routes.py`; `__init__.py` only wires those pure handlers to ComfyUI JSON responses, and `tools/prompt_smoke.py` covers the handlers without diff --git a/docs/prompt-pool-routing-map.md b/docs/prompt-pool-routing-map.md index 8db088e..47c2a0f 100644 --- a/docs/prompt-pool-routing-map.md +++ b/docs/prompt-pool-routing-map.md @@ -119,6 +119,7 @@ Core helper ownership: | `prompt_hygiene.py` | Generic prompt, caption, and negative-prompt cleanup. | | `row_normalization.py` | Final prompt-row and pair metadata normalization: trigger prepending, extra-positive append, negative merge/dedupe, caption-part joining, embedded soft/hard row output and side-metadata synchronization, and embedded row sanitation. | | `formatter_input.py` | Shared formatter input parsing: text cleanup, metadata/source JSON detection, trigger-prefix stripping, shared prompt field-label inventory, fallback field-label stripping, `Avoid:` splitting, prompt-field extraction, and metadata row-value fallback. | +| `node_tooltips.py` | Node input tooltip inventory, node-specific overrides, dynamic-input fallback rules, and tooltip injection installer used by `__init__.py`. | | `server_routes.py` | Pure payload handlers for profile-save and accumulator server endpoints, used by ComfyUI routes and smoke tests without importing ComfyUI. | | `sdxl_presets.py` | SDXL formatter profiles, style presets, quality presets, default negative prompt, and metadata-family tag hints used by the SDXL formatter and node choice lists. | | `caption_policy.py` | Caption naturalizer policy data and helpers: caption profiles, style tails, item labels, metadata-family caption labels, detail/style-policy normalization, clothing cleanup, and composition cleanup. | diff --git a/node_tooltips.py b/node_tooltips.py new file mode 100644 index 0000000..65579b3 --- /dev/null +++ b/node_tooltips.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +import re + +COMMON_INPUT_TOOLTIPS = { + "row_number": "Generation row to use. Changing it advances the deterministic selection without changing the main seed.", + "start_index": "Metadata/output index offset only. It does not limit category pools or random choices.", + "seed": "Main seed used when no more specific seed config overrides an axis.", + "global_seed": "One seed that locks all prompt axes so the same inputs can recreate the same result.", + "base_seed": "Base seed used by Seed Locker before applying a selected reroll axis.", + "reroll_seed": "Seed for the selected reroll axis. Use -1 to derive it from the base seed.", + "category": "Main category source. auto_weighted is legacy random; auto_full mixes legacy random with JSON categories including hardcore.", + "subcategory": "Specific subcategory, or random to choose within the selected category.", + "category_config": "Category/subcategory config from SxCP Category Preset.", + "cast_config": "Cast size config from SxCP Cast Control.", + "generation_profile": "General style/intensity profile from SxCP Generation Profile.", + "filter_config": "Ethnicity/body filter config. Ethnicity List can feed this too.", + "ethnicity_list": "Optional ethnicity pool. When connected, it overrides the slot or generator ethnicity picker.", + "seed_config": "Per-axis seed config. Connect Global Seed, Seed Locker, or Seed Control here.", + "camera_config": "Camera config used by the prompt formatter when camera mode is from_camera_config.", + "location_config": "Location config from SxCP Location Pool. It can replace or add to the category scene pool.", + "composition_config": "Composition config from SxCP Composition Pool or Location Theme. It can replace or add framing options.", + "softcore_camera_config": "Camera config used only for the softcore Insta/OF prompt. Falls back to camera_config if empty.", + "hardcore_camera_config": "Camera config used only for the hardcore Insta/OF prompt. Falls back to camera_config if empty.", + "character_profile": "Saved or loaded single-character profile. Character slots override this for configured casts.", + "character_cast": "Chain character slots here. The node closest to the final generator becomes the next auto_chain label.", + "character_slot": "Single slot payload for saving/loading profiles or debugging one character.", + "hardcore_position_config": "Hardcore action/position config. Chain Position Pool into Action Filter, then into the generator.", + "custom_locations": "One custom location per line. Use plain text, or slug: location text.", + "custom_compositions": "One custom composition/framing phrase per line.", + "theme": "Matched location and composition theme, useful when the place needs compatible framing.", + "metadata_json": "Structured metadata from an SxCP generator. Prefer this over raw prompt text for formatters and profile save.", + "source_text": "Raw prompt, caption, or metadata JSON depending on input_hint.", + "source_text_input": "Optional linked raw prompt/caption input. When connected, it overrides the source_text widget.", + "input_hint": "Tells the node how to interpret source_text. auto tries metadata first.", + "target": "For dual prompts, choose which side to output as the main Krea prompt.", + "detail_level": "Controls how much detail the rewriter keeps. concise is shorter, dense keeps more clauses.", + "style_mode": "How strongly the formatter rewrites visual style terms.", + "preserve_trigger": "Keep the trigger token in the formatted prompt instead of stripping it.", + "negative_prompt": "Negative prompt text to pass through or merge with generated negatives.", + "extra_positive": "Extra positive text appended after the generated prompt.", + "extra_negative": "Extra negative text appended after the generated negative prompt.", + "trigger": "Training or style trigger token.", + "prepend_trigger_to_prompt": "If enabled, put the trigger token at the start of generated prompts.", + "bucket_index": "0 picks a random bucket. 1+ picks that position inside the selected orientation pool.", + "megapixels": "Approximate megapixel count for the selected bucket.", + "enabled": "Enable this node's effect while keeping it wired in the graph.", + "combine_mode": "replace starts a new pool/config; add merges selected values into the incoming config.", + "manual": "Manual character details config. Non-empty manual fields override generated slot details.", + "characteristics": "Chainable character characteristic pool such as age/body/eyes/clothing.", + "hair_config": "Chainable hair pool. Combine length, color, and style nodes before the character slot.", + "summary": "Human-readable description of the config produced by this node.", + "status": "Operation result or warning text.", + "profile_name": "Name of the profile to save, load, rename, or delete.", + "manual_profile_name": "Free-text profile name used when profile_name is set to manual.", + "fallback_profile_json": "Profile JSON to use when a named profile cannot be loaded.", + "rename_to": "New profile name used only when rename_now is enabled.", + "save_now": "Writes the profile to disk only when enabled. Keep off while adjusting fields.", + "delete_now": "Deletes the selected profile when enabled.", + "rename_now": "Renames the selected profile when enabled.", + "source": "Where the save node reads character data from.", + "subject_type": "Character type for this slot or saved profile.", + "label": "Character label. auto_chain assigns the next Woman/Man label based on incoming cast order.", + "slot_seed": "Per-character seed. Use -1 to follow the generator person seed.", + "age": "Age choice for this slot. Use Age Range node for a custom random age pool.", + "manual_age": "Exact age phrase override, for example '32-year-old adult'.", + "ethnicity": "Ethnicity choice for this slot. A connected Ethnicity List overrides this picker.", + "figure": "General figure bias for generated body descriptors.", + "figure_bias": "Woman-slot figure bias. Body pool can give more precise body choices.", + "women_count": "Number of women in the generated cast when no Insta/OF preset overrides it.", + "men_count": "Number of men in the generated cast when no Insta/OF preset overrides it.", + "hardcore_women_count": "Number of women in the hardcore cast when hardcore_cast is use_counts.", + "hardcore_men_count": "Number of men in the hardcore cast when hardcore_cast is use_counts.", + "body": "Body choice for this slot. A Body Pool node can replace the random list.", + "manual_body": "Exact body phrase override.", + "body_phrase": "Full custom body wording. Use only when the body picker is not specific enough.", + "skin": "Manual skin/complexion phrase.", + "hair": "Manual hair phrase. Hair config nodes are better for controlled random choices.", + "eyes": "Manual eye description.", + "descriptor_detail": "How detailed this character's descriptor should be. Men usually work better compact.", + "expression_enabled": "Master expression toggle for this generator or character.", + "expression_intensity": "Expression intensity from 0 to 1. On the direct builder, -1 randomizes per row; on slots, -1 inherits the generator setting.", + "expression_intensity_mode": "For Generation Profile, choose profile_default, random, or fixed value from expression_intensity.", + "softcore_expression_intensity": "Optional expression intensity override for this character in softcore prompts. -1 inherits.", + "hardcore_expression_intensity": "Optional expression intensity override for this character in hardcore prompts. -1 inherits.", + "presence_mode": "Controls whether the character is visible, implied POV, or otherwise present.", + "softcore_outfit": "Manual softcore outfit text for this character.", + "hardcore_clothing": "Manual hardcore clothing/body exposure text for this character.", + "custom_softcore_outfits": "One custom softcore outfit per line. Used when softcore_source is custom.", + "custom_hardcore_clothing": "One custom hardcore clothing/body exposure state per line.", + "condition": "Loop condition. When false, the loop stops and passes current values through.", + "total": "Total number of loop iterations.", + "skip": "Number of leading loop indexes to skip. skip=1 starts generation at index 2.", + "collection": "Existing accumulated value or batch.", + "value": "Value to append, store, or pass through.", + "store_key": "Accumulator memory key. Same key shares stored entries across executions.", + "store_key_input": "Connect SxCP Accumulator store_key here so preview/delete/save uses the same accumulator and graph dependency.", + "action": "Accumulator operation: append, replace, clear, read, or append a variant.", + "max_items": "Maximum stored entries kept in this accumulator.", + "image_batch_mode": "How image entries are batched when dimensions differ.", + "skip_empty": "Ignore empty inputs instead of adding blank entries.", + "image": "Image to store in the accumulator.", + "entry_id": "Stable ID used for replace_by_entry_id or grouping variants.", + "entry_tag": "Optional suffix added to entry_id.", + "preview_limit": "Maximum number of accumulator images to show in the preview panel.", + "view_mode": "Accumulator Preview layout: grid shows many images, carousel shows one large image at a time.", + "zoom_level": "Accumulator Preview image scale. Higher values make grid thumbnails or carousel image area larger.", + "carousel_index": "1-based image position shown in carousel mode. The previous/next buttons update this value.", + "delete_action": "Optional execution-time delete operation. JS buttons can delete interactively without setting this.", + "delete_entry_id": "Entry id to delete when delete_action is delete_entry_id.", + "delete_index": "1-based entry index to delete when delete_action is delete_index. 0 disables it.", + "save_batch": "When enabled, save all current accumulator images once finished is true.", + "finished": "Gate for saving. Outside a loop, leave true; inside a loop, wire a final-iteration signal.", + "save_path": "Folder to save the accumulator batch. Relative paths are inside ComfyUI output; absolute paths are used directly.", + "filename_prefix": "Filename prefix for saved accumulator images.", + "clear_after_save": "Clear the accumulator store after a successful batch save.", + "preview_text": "Serialized persistent text preview. It is updated after execution and saved with the workflow.", + "preview_format": "How to convert an arbitrary input to preview text.", + "max_chars": "Maximum stored preview characters. 0 disables truncation.", + "mode": "Switch direction: pick_input selects one input to value, route_output sends route_value to one output.", + "index": "Index used by SxCP Index Switch. For Loop Start outputs one_based indexes by default.", + "index_base": "one_based means index 1 selects input_1. zero_based means index 0 selects input_1.", + "missing_behavior": "What to do when the requested switch input is not connected: use fallback, output none, clamp, or wrap.", + "fallback": "Optional value used by SxCP Index Switch when the requested input is missing and missing_behavior is fallback.", + "route_value": "Value routed to output_N when mode is route_output.", + "clothing": "Built-in clothing density for legacy direct generation. random picks full/minimal from the seeded row.", + "poses": "Built-in pose pool for legacy direct generation. random picks standard/evocative from the seeded row.", + "backside_bias": "Legacy bias toward rear/backside poses where that category supports it.", + "minimal_clothing_ratio": "Legacy weighted ratio override. -1 keeps the category/profile default.", + "standard_pose_ratio": "Legacy weighted ratio override. -1 keeps the category/profile default.", + "profile": "Generation profile preset for broad style, clothing, pose, and expression defaults.", + "clothing_override": "Override the profile clothing setting, or leave profile_default.", + "poses_override": "Override the profile pose setting, or leave profile_default.", + "trigger_policy": "Controls whether the profile prepends the trigger token.", + "cast_mode": "Preset cast shape. Custom counts are used when the preset allows them.", + "women_weights": "Comma-separated count weights. First value maps to women_start_count, second to +1, and so on.", + "men_weights": "Comma-separated count weights. First value maps to men_start_count, second to +1, and so on.", + "women_start_count": "Woman count represented by the first women_weights value.", + "men_start_count": "Man count represented by the first men_weights value.", + "empty_behavior": "What to do if the weighted pick selects zero women and zero men.", + "preset": "Category preset for common workflow lanes.", + "camera_mode": "Camera style preset.", + "shot_size": "How much of the body/frame should be visible.", + "angle": "Camera angle relative to the subject.", + "lens": "Lens wording to include in the prompt.", + "distance": "Camera distance wording.", + "orientation": "Horizontal/vertical framing wording.", + "phone_visibility": "Whether the prompt mentions a visible/hidden phone.", + "priority": "How strictly the prompt should enforce the camera wording.", + "camera_detail": "off omits camera text, compact keeps one line, full emits detailed camera wording.", + "subject_focus": "Optional camera focus phrase, such as face/body/contact emphasis.", + "strict_excludes": "When enabled, only selected ethnicity groups are used. When off, selections act more like soft includes.", + "min_age": "Minimum adult age in this custom age pool.", + "max_age": "Maximum adult age in this custom age pool.", + "softcore_source": "Softcore outfit source for this character. custom reads custom_softcore_outfits.", + "hardcore_state": "Hardcore clothing/body exposure state for this character.", + "softcore_expression_enabled": "Enable expression text in the softcore prompt.", + "hardcore_expression_enabled": "Enable expression text in the hardcore prompt.", + "flow": "Loop flow-control socket. Wire from the matching loop start node.", + "collection_mode": "How the loop end collects per-iteration values.", + "skip_none": "Do not add empty values to the collection.", + "collected": "Current accumulated value carried through the loop.", + "collect_value": "Value captured from the current loop iteration.", + "a": "First integer/boolean helper input.", + "b": "Second integer/boolean helper input.", +} + +NODE_INPUT_TOOLTIPS = { + "SxCPSeedControl": { + "category_seed_mode": "auto/follow_main follows the main seed; fixed uses category_seed; random rerolls this axis each queue.", + "subcategory_seed_mode": "Controls which subcategory is selected. Change this to switch oral vs penetration when both are allowed.", + "content_seed_mode": "Controls item/outfit content for non-pose categories.", + "person_seed_mode": "Controls generated character appearance unless a slot seed overrides it.", + "scene_seed_mode": "Controls location/scene selection.", + "pose_seed_mode": "Controls pose/item selection for pose categories, including hardcore positions.", + "role_seed_mode": "Controls role assignment and secondary action details.", + "expression_seed_mode": "Controls selected expression text.", + "composition_seed_mode": "Controls framing/composition text.", + }, + "SxCPSeedLocker": { + "reroll_axis": "Choose the one axis to change while the rest stays locked. Use pose for sexual pose, scene for location, person for appearance.", + }, + "SxCPCastBias": { + "seed": "Fixed cast-bias seed. Use -1 for a fresh cast each queue, or connect Global Seed/Seed Locker through seed_config.", + "seed_config": "Optional seed config. The category seed controls weighted cast selection.", + "women_weights": "Example with women_start_count=1: 0.6,0.25,0.1 means 60% one woman, 25% two women, 10% three women.", + "men_weights": "Example with men_start_count=0: 0.5,0.35,0.1 means 50% no man, 35% one man, 10% two men.", + "empty_behavior": "Prevents accidental empty casts when both weighted pools pick zero.", + }, + "SxCPSDXLBucketSize": { + "orientation": "Bucket orientation filter. any uses the full table; portrait/square/landscape restrict random selection.", + "seed": "Fixed bucket seed. Use -1 for a fresh random bucket each queue, or connect Global Seed for reproducible sizes.", + "row_number": "Deterministic row offset for the bucket. With a fixed seed, changing this advances the bucket choice.", + "bucket_index": "0=random. 1+ selects that bucket position inside the selected orientation pool and ignores seed.", + "seed_config": "Optional seed config. The composition seed controls bucket choice, so Seed Locker can keep sizes fixed while rerolling pose/person.", + }, + "SxCPKrea2ResolutionSelector": { + "megapixels": "Target megapixel preset. If it cannot fit the aspect ratio under the 2K Krea2 Turbo limit, the node clamps to the maximum valid size.", + "aspect_ratio": "Krea API ratios are listed first; local-only helper ratios like 8:9 are included after them.", + }, + "SxCPCameraControl": { + "camera_mode": "Camera style preset. Use from_camera_config in Insta/OF options to consume this.", + "priority": "locked makes the camera wording strict; soft_hint allows the model more freedom.", + "camera_detail": "off omits camera text, compact keeps one short line, full emits detailed camera constraints.", + "phone_visibility": "Use phone_hidden or suppress_phone_visibility when you do not want 'phone hidden' text in prompts.", + }, + "SxCPCameraOrbitControl": { + "horizontal_angle": "Orbit angle in degrees. 0=front, 90=right side, 180=back, 270=left side.", + "vertical_angle": "Camera elevation. Negative looks up, positive looks down.", + "zoom": "Maps to distance/framing when framing is from_zoom.", + "framing": "How zoom should be translated into shot size/distance wording.", + "include_degrees": "Include numeric degree wording in addition to human camera direction.", + }, + "SxCPQwenCameraTranslator": { + "qwen_prompt": "Camera prompt from Qwen MultiAngle, for example ' front-right quarter view eye-level shot medium shot'.", + "camera_info": "Optional structured camera_info from Qwen MultiAngle. Used before qwen_prompt when prefer_camera_info is true.", + "prefer_camera_info": "Use structured camera_info values when available instead of parsing the text prompt.", + "suppress_phone_visibility": "Avoid adding phone visibility text unless you explicitly set a phone option.", + }, + "SxCPHardcorePositionPool": { + "family": "Restrict the broad hardcore family. Use any when you want oral and penetration to both be possible.", + "combine_mode": "replace discards incoming position choices; add merges these choices with the incoming config.", + "hardcore_position_config": "Optional incoming config. Usually connect previous Position Pool here only when chaining pools.", + }, + "SxCPHardcoreActionFilter": { + "focus": "keep_pool preserves/broadens the incoming pool; *_only modes force one action family.", + "allow_toys": "Allow toy/strap-on wording in hardcore actions.", + "allow_double": "Allow double-penetration or second-contact wording.", + "allow_penetration": "Allow vaginal/penetrative sex subcategories.", + "allow_foreplay": "Allow hardcore teasing/foreplay setup actions such as kissing, caressing, breast/face touching, and undressing.", + "allow_interaction": "Allow non-act interaction pools such as body worship, clothing transitions, guidance, camera presentation, watching, and aftercare.", + "allow_manual": "Allow manual stimulation pools such as fingering, clit rubbing, and mutual masturbation.", + "allow_oral": "Allow oral sex subcategories.", + "allow_outercourse": "Allow non-penetrative penis-contact acts such as boobjob/titjob, footjob, penis licking, and testicle sucking.", + "allow_anal": "Allow anal subcategories.", + "allow_climax": "Allow cumshot/climax aftermath subcategories.", + }, + "SxCPInstaOFOptions": { + "softcore_cast": "solo keeps softcore focused on Woman A; same_as_hardcore includes the same cast as the hardcore prompt.", + "hardcore_cast": "use_counts reads hardcore_women_count/hardcore_men_count; presets set the counts automatically.", + "softcore_level": "Controls the soft prompt exposure/outfit level.", + "hardcore_level": "Controls how explicit the hardcore prompt style is.", + "platform_style": "Instagram/OnlyFans styling bias for the dual prompt pair.", + "continuity": "Whether the softcore and hardcore prompts share the room/creator setup.", + "hardcore_clothing_continuity": "How clothing carries from softcore to hardcore. explicit_nude omits clothing references.", + "softcore_camera_mode": "Camera mode for the softcore prompt, or from_camera_config.", + "hardcore_camera_mode": "Camera mode for the hardcore prompt. same_as_softcore reuses the softcore setting.", + "camera_detail": "Global camera verbosity for the pair unless a camera config overrides it.", + "hardcore_detail_density": "How dense the hardcore action sentence should be in the Krea formatter.", + }, + "SxCPInstaOFPromptPair": { + "options_json": "Options from SxCP Insta/OF Options. If empty, defaults are used.", + "ethnicity": "Fallback ethnicity when no filter/ethnicity list or character slots are connected.", + "figure": "Fallback figure bias when no character slot overrides it.", + }, + "SxCPPromptBuilderFromConfigs": { + "seed": "Main seed. Connect Seed Config for per-axis control.", + }, + "SxCPCharacterProfileSave": { + "profile_name": "Profile filename stem. Saving requires save_now=true.", + "metadata_json": "Use generator metadata to save the currently generated character without regenerating it.", + "character_slot": "Use this when saving a configured slot directly.", + }, + "SxCPCharacterProfileLoad": { + "enabled": "When false, outputs an empty profile and leaves downstream generation unchanged.", + "override_age": "Optional loaded-profile override. Empty keeps the profile value.", + "override_body": "Optional body override. Empty keeps the profile value.", + "override_descriptor_detail": "Override descriptor verbosity while keeping the rest of the loaded profile.", + }, + "SxCPKrea2Formatter": { + "metadata_json": "Best input for Krea2 formatting because it preserves cast, camera, and hardcore action metadata.", + "preserve_trigger": "Reminder: Krea2 formatting is intended to remove training/style triggers. Leave false unless you intentionally want a raw text trigger preserved.", + "source_text": "Raw prompt fallback. Known trigger tokens are stripped by default for Krea2.", + }, + "SxCPSDXLFormatter": { + "metadata_json": "Best input for SDXL tag formatting because it preserves cast, camera, outfit, and explicit action metadata.", + "formatter_profile": "High-level formatter defaults. manual_controls keeps style_preset and quality_preset authoritative.", + "style_preset": "Positive style anchor preset. flat_vector_pony matches the old SDXL tag style.", + "quality_preset": "Quality/score tag tail for SDXL or Pony-style checkpoints.", + "custom_style": "Optional replacement for the style preset. Leave empty to use style_preset.", + "custom_quality": "Optional replacement for the quality preset. Leave empty to use quality_preset.", + "nude_weight": "Weight used when explicit nude/body exposure tags are inferred.", + }, + "SxCPCaptionNaturalizer": { + "metadata_json": "Best input for training captions because it preserves structured generator details.", + "caption_profile": "Preset behavior for the caption rewrite. manual_controls keeps detail/style/include-trigger widgets authoritative.", + "style_policy": "drop_style_tail removes generation/style boilerplate; keep_style_terms preserves more of it.", + "include_trigger": "Keep this true for LoRA/training captions so the trigger token is learned.", + }, + "SxCPForLoopStart": { + "index": "Output loop index. First generated index is skip + 1.", + "collected": "Current accumulated value carried through the loop.", + }, + "SxCPLoopAppend": { + "mode": "auto_batch tries tensor/latent batching first, then falls back to a list.", + }, + "SxCPAccumulator": { + "image_batch_mode": "same_size_only keeps incompatible sizes separate; resize_to_first forces one image batch.", + }, +} + + +def _tooltip_for_input(node_name: str, input_name: str) -> str: + node_tooltips = NODE_INPUT_TOOLTIPS.get(node_name, {}) + if input_name in node_tooltips: + return node_tooltips[input_name] + if input_name in COMMON_INPUT_TOOLTIPS: + return COMMON_INPUT_TOOLTIPS[input_name] + if input_name.endswith("_seed_mode"): + axis = input_name[: -len("_seed_mode")] + return f"How the {axis} seed is resolved: follow the main seed, use the fixed field, or reroll randomly." + if input_name.endswith("_seed"): + axis = input_name[: -len("_seed")] + return f"Fixed {axis} seed value. Used only when the matching seed mode is fixed, or as a fallback for auto modes." + if input_name.startswith("include_"): + value = input_name[len("include_") :].replace("_", " ") + return f"Include {value} in this random pool." + if input_name.startswith("initial_value"): + return "Carry value passed into the loop body and returned on the matching output." + if re.match(r"^input_\d+$", input_name): + return "Autoscaling switch input. Connect the last visible input to reveal the next one." + if re.match(r"^output_\d+$", input_name): + return "Autoscaling routed output. Connect the last visible output to reveal the next one." + if input_name.startswith("override_"): + return "Optional loaded-profile override. Leave empty or keep_profile to preserve the profile value." + return "" + + +def _copy_input_spec_with_tooltip(input_spec, tooltip: str): + if not tooltip or not isinstance(input_spec, tuple): + return input_spec + if len(input_spec) >= 2 and isinstance(input_spec[1], dict): + options = dict(input_spec[1]) + options.setdefault("tooltip", tooltip) + return (input_spec[0], options, *input_spec[2:]) + if len(input_spec) == 1: + return (input_spec[0], {"tooltip": tooltip}) + return input_spec + + +def _inject_input_tooltips(input_types: dict, node_name: str) -> dict: + patched = dict(input_types) + for group_name in ("required", "optional"): + group = patched.get(group_name) + if not isinstance(group, dict): + continue + patched_group = {} + for input_name, input_spec in group.items(): + patched_group[input_name] = _copy_input_spec_with_tooltip( + input_spec, + _tooltip_for_input(node_name, input_name), + ) + patched[group_name] = patched_group + return patched + + +def install_input_tooltips(node_classes: dict[str, type]) -> None: + for node_name, node_class in node_classes.items(): + original = getattr(node_class, "INPUT_TYPES", None) + if original is None or getattr(node_class, "_sxcp_tooltips_installed", False): + continue + + def input_types(cls, _original=original, _node_name=node_name): + return _inject_input_tooltips(_original(), _node_name) + + node_class.INPUT_TYPES = classmethod(input_types) + node_class._sxcp_tooltips_installed = True diff --git a/tools/prompt_smoke.py b/tools/prompt_smoke.py index 6a3b860..7087236 100644 --- a/tools/prompt_smoke.py +++ b/tools/prompt_smoke.py @@ -42,6 +42,7 @@ import hardcore_position_config # noqa: E402 import __init__ as sxcp_nodes # noqa: E402 import generation_profile_config # noqa: E402 import index_switch_policy # noqa: E402 +import node_tooltips # noqa: E402 import krea_cast # noqa: E402 import krea_configured_cast_formatter # noqa: E402 import krea_formatter # noqa: E402 @@ -4482,6 +4483,15 @@ def smoke_node_utility_registration() -> None: seed_inputs = seed_control.INPUT_TYPES().get("required") or {} _expect("category_seed_mode" in seed_inputs, "Seed Control lost category seed mode input") _expect("tooltip" in seed_inputs["category_seed_mode"][1], "Seed Control tooltip injection missing") + _expect( + node_tooltips._tooltip_for_input("SxCPSeedControl", "category_seed_mode") + == "auto/follow_main follows the main seed; fixed uses category_seed; random rerolls this axis each queue.", + "Node tooltip policy lost Seed Control override", + ) + _expect( + "Autoscaling switch input" in node_tooltips._tooltip_for_input("SxCPIndexSwitch", "input_12"), + "Node tooltip policy lost autoscaling input fallback", + ) seed, seed_config, summary = sxcp_nodes.NODE_CLASS_MAPPINGS["SxCPGlobalSeed"]().build(12345) parsed_seed = json.loads(seed_config)