Compare commits

...
82 Commits
Author SHA1 Message Date
chris.dumas 338150648b Add BGM helper for H3 prompt curator 2026-09-07 08:13:45 +00:00
chris.dumas 7de62226f2 Strip legacy anchor style note 2026-09-07 08:01:22 +00:00
chris.dumas a51141cb26 Add location helper reference outputs 2026-09-07 07:46:24 +00:00
chris.dumas dd8ef84379 Add character helper reference outputs 2026-09-05 18:00:17 +00:00
chris.dumas d20b257134 Add H3 prompt subject count guard 2026-09-05 17:54:43 +00:00
chris.dumas 173205ca51 Add H3 prompt curator 2026-09-05 16:43:05 +00:00
chris.dumas 98833ba99d Restore general character and location helpers 2026-09-04 15:52:40 +00:00
chris.dumas a7f7225b3a Bypass learned latent upscaler on low VRAM 2026-09-04 15:24:30 +00:00
chris.dumas 5dc5c2ce8b Prioritize temporal fallback for latent upscale OOM 2026-09-04 15:10:38 +00:00
chris.dumas 83645811f4 Reuse conditioning for refinement tiles 2026-09-04 14:52:43 +00:00
chris.dumas 216bc05762 Retry temporal split after latent upscale OOM 2026-09-04 14:43:25 +00:00
chris.dumas 49e099ee7f Log H3 latent upscale tiling plan 2026-09-04 14:14:24 +00:00
chris.dumas 32a16645b5 Use adaptive CUDA latent upscale chunking 2026-09-04 12:09:59 +00:00
chris.dumas 7bb0b0abea Reuse conditioning for latent upscale refine 2026-09-04 11:49:21 +00:00
chris.dumas 0b189dcf7a Keep latent upscaler loaded during pass 2026-09-04 11:04:26 +00:00
chris.dumas e8599055a1 Force temporal chunks for CUDA latent upscale 2026-09-04 10:22:41 +00:00
chris.dumas 618a48e4d9 Harden latent upscale GPU cleanup 2026-09-04 10:05:24 +00:00
chris.dumas cbabcf8208 Offload H3 before latent upscale 2026-09-04 09:49:34 +00:00
chris.dumas fbfbe4ef5a Extend H3 spatial fallback ladder 2026-09-04 09:22:35 +00:00
chris.dumas 5b52793fad Clamp H3 spatial overlaps to tile size 2026-09-04 09:05:29 +00:00
chris.dumas 7e45c81589 Reset overlap in H3 spatial retries 2026-09-04 08:48:20 +00:00
chris.dumas 027865b9ca Tighten H3 spatial fallback retries 2026-09-04 08:27:54 +00:00
chris.dumas 9f0c546681 Remove latent upscale CPU fallback 2026-09-04 08:13:27 +00:00
chris.dumas 30ae81b86b Jump latent upscale fallback to spatial split 2026-09-04 08:00:06 +00:00
chris.dumas 179fa0778e Add CPU fallback for latent upscale OOM 2026-09-04 07:33:10 +00:00
chris.dumas a34c9eb4ff Reduce latent upscale VRAM pressure 2026-09-04 07:21:38 +00:00
chris.dumas 782a7d658b Fix temporal chunking to use H3 token grid 2026-09-04 07:06:08 +00:00
chris.dumas 04a61af874 Add temporal OOM backoff for latent upscale 2026-09-04 06:54:35 +00:00
chris.dumas a0d80bcc47 Add temporal chunking to latent upscale 2026-09-04 06:41:03 +00:00
chris.dumas fbb5f799cd Unload latent upscale model on OOM 2026-09-03 20:30:58 +00:00
chris.dumas 9dc3c405a6 Clamp latent upscale fallback tile minimum 2026-09-03 20:15:58 +00:00
chris.dumas 21ae4d62e0 Keep latent upscale shrink steps 32-aligned 2026-09-03 19:59:50 +00:00
chris.dumas 836a6eba33 Back off latent upscale tile size on OOM 2026-09-03 19:43:15 +00:00
chris.dumas 3b63d33ec7 Tile latent upscale model inference 2026-09-03 19:01:13 +00:00
chris.dumas c62921c3e8 Stop retrying latent upscale OOMs as sampling 2026-09-03 18:46:56 +00:00
chris.dumas 3ab6342ce5 Handle legacy latent upscale payloads 2026-09-03 18:33:01 +00:00
chris.dumas 51e03a39b7 Reduce latent upscale branch memory 2026-09-03 18:08:44 +00:00
chris.dumas f7b94ccaef Free first-pass latent before refinement 2026-09-03 16:34:12 +00:00
chris.dumas f6120a8500 Adopt full MMH3 spatial split controls 2026-09-03 16:15:43 +00:00
chris.dumas 0e119646ab Add remaining spatial split settings 2026-09-03 15:47:59 +00:00
chris.dumas c89570eae9 Expand latent upscale spatial stitch controls 2026-09-03 15:35:39 +00:00
chris.dumas 36d9f4369c Add spatial batching to latent upscale 2026-09-03 15:12:42 +00:00
chris.dumas 2cb3c694f0 Fix latent upscale summary scope leak 2026-09-03 14:37:24 +00:00
chris.dumas 5c12cd18a3 Expose latent upscale sampler controls 2026-09-03 13:05:32 +00:00
chris.dumas c1d937e0e2 Add H3 latent upscale refinement stage 2026-09-03 12:53:57 +00:00
chris.dumas 306816534b Bump package version to 0.1.1 2026-09-03 12:18:26 +00:00
chris.dumas e098e44bf1 Simplify H3 beat timing and cleanup 2026-09-03 08:56:43 +00:00
chris.dumas 769257513b Harden H3 detail-pass cleanup 2026-09-03 07:18:13 +00:00
chris.dumas ab6ba86d76 Fix H3 shot mode runtime error 2026-09-02 15:20:07 +00:00
chris.dumas ecd8ca6336 Align H3 prompt refs with beat numbering 2026-09-02 15:06:41 +00:00
chris.dumas 76b88b0c59 Clean up per-beat H3 script output 2026-09-02 14:52:58 +00:00
chris.dumas 62fbbdd98e Use socket picture labels in ref summaries 2026-09-02 14:44:54 +00:00
chris.dumas 85e15c910a Fix H3 section control interactions 2026-09-02 14:39:10 +00:00
chris.dumas 30400038c2 Add collapsible H3 long video sections 2026-09-02 13:25:54 +00:00
chris.dumas c8ddedbd57 Remove picture id from reference nodes 2026-09-02 13:20:38 +00:00
chris.dumas 6684c5f2e0 Remove legacy H3 ref image aliases 2026-09-02 13:06:25 +00:00
chris.dumas 08630964d5 Keep multiline beat prompts intact 2026-09-02 13:00:25 +00:00
chris.dumas 271e7de300 Fix tag-driven H3 ref routing 2026-09-02 12:55:51 +00:00
chris.dumas 130b268d1e Fix named reference lookup for long videos 2026-08-31 09:40:51 +00:00
chris.dumas a2dabaab7e Revert "Revert long videos ref routing to release"
This reverts commit 00985b73b3.
2026-08-30 21:37:15 +00:00
chris.dumas b7c3bba199 Revert "Restore missing named refs helper"
This reverts commit dfead66b9e.
2026-08-30 21:37:06 +00:00
chris.dumas 35ec12159e Revert "Retry long videos decode with tiling fallback"
This reverts commit 6a7bec5d03.
2026-08-30 21:36:58 +00:00
chris.dumas 58b1c77c19 Revert "Restore long videos release snapshot"
This reverts commit 4a0f5cfcd4.
2026-08-30 21:36:50 +00:00
chris.dumas 4a0f5cfcd4 Restore long videos release snapshot 2026-08-30 21:06:22 +00:00
chris.dumas 6a7bec5d03 Retry long videos decode with tiling fallback 2026-08-30 20:28:47 +00:00
chris.dumas dfead66b9e Restore missing named refs helper 2026-08-30 13:23:13 +00:00
chris.dumas 00985b73b3 Revert long videos ref routing to release 2026-08-30 10:07:45 +00:00
chris.dumas ffae1d87a5 Restore character ref regression tests 2026-08-29 12:20:38 +00:00
chris.dumas 74aca203ab Restore release-era character ref routing 2026-08-29 12:18:38 +00:00
chris.dumas e08281c12b Prioritize character refs over location refs 2026-08-29 11:36:53 +00:00
chris.dumas faaeb374e0 Document named ref routing guardrail 2026-08-29 11:02:21 +00:00
chris.dumas 0bac83a689 Fix named character ref routing 2026-08-29 10:51:01 +00:00
chris.dumas 08365c7df5 Add long videos timing breakdown 2026-08-29 00:22:48 +00:00
chris.dumas 9cbab72e86 Optimize long videos ref bookkeeping 2026-08-28 22:09:12 +00:00
chris.dumas e29d01ad24 Tighten tagged ref routing and anchor presets 2026-08-28 15:45:22 +00:00
chris.dumas ddd7dbb5af Harden and streamline H3 reference matching 2026-08-28 13:49:01 +00:00
chris.dumas 1ad68ee9e4 Stop ref wardrobe from fighting live character memory 2026-08-28 13:16:05 +00:00
chris.dumas e64ad0fae4 Expand H3 reference-node documentation 2026-08-28 12:48:33 +00:00
chris.dumas deb6f1d033 Clarify H3 long videos state behavior 2026-08-28 12:36:46 +00:00
chris.dumas 72d4d74dd3 Clarify H3 long videos guide behavior 2026-08-28 12:27:12 +00:00
chris.dumas c42cf436c5 Document H3 long videos node 2026-08-28 11:43:11 +00:00
chris.dumas eeca789c27 Tidy H3 long videos input layout 2026-08-28 11:36:39 +00:00
14 changed files with 7262 additions and 1319 deletions
File diff suppressed because it is too large Load Diff
+39 -3
View File
@@ -38,17 +38,31 @@
- Outputs: `images`, `audio`, `info`, `script`, `frames_per_shot`, `total_frames`, `shots`, `video_seconds`, `fps`, `fps_int`, `latent`, `soundscape` - Outputs: `images`, `audio`, `info`, `script`, `frames_per_shot`, `total_frames`, `shots`, `video_seconds`, `fps`, `fps_int`, `latent`, `soundscape`
- First-pass Dumas port of the `MiniMax-H3-Longvideos` sampler, brought in as a local starting point for long-form H3 chaining work. - First-pass Dumas port of the `MiniMax-H3-Longvideos` sampler, brought in as a local starting point for long-form H3 chaining work.
- Keeps the upstream split-beats / handoff / ref-routing behavior close to source so future Dumas-specific improvements can be compared against a known baseline. - Keeps the upstream split-beats / handoff / ref-routing behavior close to source so future Dumas-specific improvements can be compared against a known baseline.
- Full user guide: [`H3_LONG_VIDEOS_GUIDE.md`](./H3_LONG_VIDEOS_GUIDE.md)
- Only the canonical `DumasH3LongVideos` node key is exposed now; the older FL2VA/REF2VA alias entries are no longer duplicated in the Add Node menu. - Only the canonical `DumasH3LongVideos` node key is exposed now; the older FL2VA/REF2VA alias entries are no longer duplicated in the Add Node menu.
- Prompt `<Picture N>` tags now map to the actual ref socket numbers you wire, even with gaps such as only `ref_2` and `ref_7` connected. - Prompt `<Picture N>` tags now map to the actual ref socket numbers you wire, even with gaps such as only `ref_2` and `ref_7` connected.
- Character refs now contribute appearance and wardrobe context from the same structured object, while location refs contribute environment context from theirs. - Character refs now contribute appearance and wardrobe context from the same structured object, while location refs contribute environment context from theirs.
- The default ref2v bias is now stronger: `ref_mode` defaults to `auto ref2v` so untagged prompts condition every shot instead of only shot 1, and `ref_noise_aug` defaults to `0.95` rather than the upstream-literal `0.999`. - The default ref2v bias is now stronger: `ref_mode` defaults to `auto ref2v` so untagged prompts condition every shot instead of only shot 1, and `ref_noise_aug` defaults to `0.95` rather than the upstream-literal `0.999`.
- `Dumas H3 Latent Upscale Params` provides the optional pre-decode latent refinement stage for the long-video node.
- Per-shot directives now support `continuity:`, `ref_mode:`, `ref_noise_aug:`, `anchor_add:`, `soundscape:`, and `music:` in addition to the existing timing and wardrobe directives. - Per-shot directives now support `continuity:`, `ref_mode:`, `ref_noise_aug:`, `anchor_add:`, `soundscape:`, and `music:` in addition to the existing timing and wardrobe directives.
- `Dumas H3 Latent Upscale Params`
- Inputs: `mode`, `model_name`, `method`, `width`, `height`, `device`, `precision`, `sampler_name`, `scheduler`, `steps`, `denoise`, `megapixels`, `tile_width`, `tile_height`, `overlap`, `fade_width`, `fade_height`, `overlap_mode`, `overlap_blend`, `tile_size_mode`, `grid_rows`, `grid_cols`, `spatial_w_overlap`, `spatial_h_overlap`, `min_tile_size`, `masked_area_noise`, `brightness_match`, `dynamic_fade`, `dynamic_fade_min`, `chunk_length`, `temporal_overlap`, `resize_conditioning`, `anchor_strength`
- Output: `latent_upscale_param`
- Bundles the optional latent-space upscaler settings used by `Dumas H3 Long Videos` before decode, so the main node can rebuild conditioning at the target size and run a short refinement pass with your chosen sampler, scheduler, step count, denoise, and the full upstream spatial split controls.
- `Dumas H3 Beat Prompt` - `Dumas H3 Beat Prompt`
- Inputs: authored through the custom front-end beat editor - Inputs: authored through the custom front-end beat editor
- Output: `prompt` - Output: `prompt`
- Builds one H3 prompt block per beat, with quick controls for per-shot timing, continuity, ref behavior, anchor additions, soundscape, and music while staying compatible with direct text editing. - Builds one H3 prompt block per beat, with quick controls for per-shot timing, continuity, ref behavior, anchor additions, soundscape, and music while staying compatible with direct text editing.
- `Dumas H3 Prompt Curator`
- Inputs: `action_prompt`, `anatomy_guard`, `subject_count_guard`, optional `anchor`, optional `soundscape`, optional `bgm`, optional `ref_1` through `ref_9`
- Outputs: `prompt`, `ref_image_1` through `ref_image_9`, `reference_count`, `debug`
- Builds one standalone MiniMax H3 prompt from your final action text plus structured character/location references.
- The action text can mention references by character/location name, alias, `<Picture N>`, or `<refN>`. Only mentioned references are emitted, and the output images are compacted/renumbered so skipped inputs do not leave gaps.
- Adds curated reference context, anatomy guard text, optional subject-count guard text, anchor/style text, `overall_soundscape:` text, and `background_music:` text while respecting MiniMax H3's reference-generation shape: one prompt plus up to nine reference images.
- `Dumas H3 Shot Length` - `Dumas H3 Shot Length`
- Inputs: `shot_seconds`, `fps`, optional `cap_to_h3_max` - Inputs: `shot_seconds`, `fps`, optional `cap_to_h3_max`
- Outputs: `seconds`, `frames`, `info` - Outputs: `seconds`, `frames`, `info`
@@ -60,15 +74,27 @@
- Reports the detected H3 base precision / quant format and the relevant compute-capability hints for the current card. - Reports the detected H3 base precision / quant format and the relevant compute-capability hints for the current card.
- `Dumas Character Reference` - `Dumas Character Reference`
- Inputs: `image`, `picture_id`, `character_id`, `name`, `alias`, `gender`, `age`, `nationality`, `occupation`, `height_feet`, `height_inches`, `accent`, `description`, `general`, `wardrobe` - Inputs: `image`, `character_id`, `name`, `alias`, `gender`, `age`, `nationality`, `occupation`, `height_feet`, `height_inches`, `accent`, `description`, `general`, `wardrobe`
- Output: `reference` - Output: `reference`
- Builds one structured `REFERENCE` object carrying the conditioning image, identity description, wardrobe, general notes, and simple facts together. - Builds one structured `REFERENCE` object carrying the conditioning image, identity description, wardrobe, general notes, and simple facts together.
- `Dumas Location Reference` - `Dumas Location Reference`
- Inputs: `image`, `picture_id`, `location_id`, `name`, `alias`, `description`, `general` - Inputs: `image`, `location_id`, `name`, `alias`, `description`, `general`
- Output: `reference` - Output: `reference`
- Builds one structured `REFERENCE` object for a location/environment so H3 can use the same socket type for both character and scenic refs. - Builds one structured `REFERENCE` object for a location/environment so H3 can use the same socket type for both character and scenic refs.
- `Dumas Character Helper`
- Inputs: `image1`, `image2`, picture IDs, character identity fields, `general`, `wardrobe`
- Outputs: `image1`, `image2`, `reference_prompt`, `wardrobe`, `reference1`, `reference2`
- Restores the original general-purpose helper shape while also emitting two structured `REFERENCE` objects for the prompt curator.
- The structured references carry the same character name, alias, age, height, gender, nationality, occupation, accent, wardrobe, and notes, so mentioning the character name in `Dumas H3 Prompt Curator` can include both helper images and the character facts automatically.
- `Dumas Location Helper`
- Inputs: `image1`, `image2`, picture IDs, `location_id`, `name`, `alias`, `description`, `general`
- Outputs: `image1`, `image2`, `reference_prompt`, `reference1`, `reference2`
- Matching general-purpose helper for environments/locations: pass two images through unchanged, emit location reference prompt text, and provide two structured `REFERENCE` objects for the prompt curator.
- The structured references carry the same location name, alias, description, and notes, so mentioning the location name in `Dumas H3 Prompt Curator` can include both helper images and the location context automatically.
- `Dumas Anchor Style` - `Dumas Anchor Style`
- Inputs: `anchor_style`, `style_description` - Inputs: `anchor_style`, `style_description`
- Output: `anchor` - Output: `anchor`
@@ -76,6 +102,16 @@
- The preset wording is tuned for H3-safe persistent anchors: camera language, lighting, texture, production treatment, and tone, without naming characters or describing one-off actions. - The preset wording is tuned for H3-safe persistent anchors: camera language, lighting, texture, production treatment, and tone, without naming characters or describing one-off actions.
- Selecting a preset fills the editable description field, and the edited multiline description is the `STRING` value passed downstream into H3 anchor sockets such as `anchor_override`. - Selecting a preset fills the editable description field, and the edited multiline description is the `STRING` value passed downstream into H3 anchor sockets such as `anchor_override`.
- `Dumas Soundscape Helper`
- Inputs: `soundscape`, `soundscape_description`
- Output: `soundscape`
- Matching soundscape helper for standalone H3 prompts. Pick a preset such as quiet interior, rainy street, cafe, city night, forest, industrial, or silent, then edit the text that flows into `Dumas H3 Prompt Curator`.
- `Dumas Background Music Helper`
- Inputs: `bgm`, `bgm_description`
- Output: `bgm`
- Matching BGM helper for standalone H3 prompts. Pick a preset such as subtle tension, cinematic suspense, emotional piano, dark ambient, hopeful orchestral, retro synth, action pulse, lo-fi, or no vocals, then edit the text that flows into `Dumas H3 Prompt Curator`.
- `Dumas JSON String to Object` - `Dumas JSON String to Object`
- Input: `json_string` - Input: `json_string`
- Output: parsed `JSON` - Output: parsed `JSON`
@@ -232,7 +268,7 @@ decr -> use index - 1
`Dumas H3 Plan Attach Scene Images` and `Dumas H3 Plan Extract Scene Images` are a companion pair for `ComfyUI-MiniMaxH3-Contex-Loop` and the local `ref2v` lane. The upstream H3 plan node cannot dynamically grow nine new image sockets for every JSON-defined scene, so Dumas stores scene image bindings beside the plan using a lightweight token and an in-memory registry. That keeps `plan.json` archiving intact while still letting you wire up nine IMAGE sockets per scene through chained helper nodes. `Dumas H3 Plan Attach Scene Images` and `Dumas H3 Plan Extract Scene Images` are a companion pair for `ComfyUI-MiniMaxH3-Contex-Loop` and the local `ref2v` lane. The upstream H3 plan node cannot dynamically grow nine new image sockets for every JSON-defined scene, so Dumas stores scene image bindings beside the plan using a lightweight token and an in-memory registry. That keeps `plan.json` archiving intact while still letting you wire up nine IMAGE sockets per scene through chained helper nodes.
`Dumas Character Reference` and `Dumas Location Reference` live in `Dumas/MiniMax`. Both output a structured `REFERENCE` object that carries the image plus its semantic payload. `Dumas H3 Long Videos` accepts those `REFERENCE` sockets directly on `ref_1`..`ref_9`, resolves `<Picture N>` against the wired slot positions, and can also pull character wardrobe context from the structured ref data when `character_memory` is left blank. `Dumas Character Helper` is the restored two-image/text helper for general H3 workflows, and `Dumas Location Helper` mirrors it for scene/environment references. Both helpers also emit structured `REFERENCE` sockets for the curator. The structured `Dumas Character Reference` and `Dumas Location Reference` nodes remain available separately for workflows that want a single `REFERENCE` socket. `Dumas H3 Prompt Curator` consumes those structured references plus optional anchor, soundscape, and BGM strings, assigns the final `<Picture N>` numbering, and outputs only the compacted images the prompt actually mentions.
`Dumas Strip Iteration Suffix` keeps the part before the first underscore and drops the rest. Names like `char123_pose_final.png` become `char123.png`, while names with no underscore such as `char123.png` are left untouched. `Dumas Strip Iteration Suffix` keeps the part before the first underscore and drops the rest. Names like `char123_pose_final.png` become `char123.png`, while names with no underscore such as `char123.png` are left untouched.
+4 -5
View File
@@ -119,8 +119,10 @@ For locations:
- Optional list of alternate match names. - Optional list of alternate match names.
- `picture_id` - `picture_id`
- Optional integer representing the intended `<Picture N>` identity. - Optional integer representing the effective `<Picture N>` identity.
- This is authoring metadata, not the final socket position. - It is inferred by consumer nodes such as `Dumas H3 Long Videos` from the
connected socket position when that position is known.
- Producer nodes do not need a manual `picture_id` input.
- `picture_label` - `picture_label`
- Derived convenience text like `<Picture 1>`. - Derived convenience text like `<Picture 1>`.
@@ -167,7 +169,6 @@ Recommended name:
Inputs: Inputs:
- `image` - `image`
- `picture_id`
- `character_id` - `character_id`
- `name` - `name`
- `alias` - `alias`
@@ -202,7 +203,6 @@ Recommended name:
Inputs: Inputs:
- `image` - `image`
- `picture_id`
- `location_id` - `location_id`
- `name` - `name`
- `alias` - `alias`
@@ -383,4 +383,3 @@ That is the change that removes the current ambiguity between:
- character identity - character identity
- wardrobe data - wardrobe data
- location / environment description - location / environment description
+6
View File
@@ -14,6 +14,10 @@ from .dumas_h3_longvideos import (
NODE_CLASS_MAPPINGS as H3_LONGVIDEO_NODE_CLASS_MAPPINGS, NODE_CLASS_MAPPINGS as H3_LONGVIDEO_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as H3_LONGVIDEO_NODE_DISPLAY_NAME_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as H3_LONGVIDEO_NODE_DISPLAY_NAME_MAPPINGS,
) )
from .dumas_h3_latent_upscale import (
NODE_CLASS_MAPPINGS as H3_LATENT_UPSCALE_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as H3_LATENT_UPSCALE_NODE_DISPLAY_NAME_MAPPINGS,
)
from .dumas_h3_shot_length import ( from .dumas_h3_shot_length import (
NODE_CLASS_MAPPINGS as H3_SHOT_LENGTH_NODE_CLASS_MAPPINGS, NODE_CLASS_MAPPINGS as H3_SHOT_LENGTH_NODE_CLASS_MAPPINGS,
NODE_DISPLAY_NAME_MAPPINGS as H3_SHOT_LENGTH_NODE_DISPLAY_NAME_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS as H3_SHOT_LENGTH_NODE_DISPLAY_NAME_MAPPINGS,
@@ -31,6 +35,7 @@ NODE_CLASS_MAPPINGS = {}
NODE_CLASS_MAPPINGS.update(JSON_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(JSON_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(IMAGE_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(IMAGE_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(H3_LONGVIDEO_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(H3_LONGVIDEO_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(H3_LATENT_UPSCALE_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(H3_SHOT_LENGTH_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(H3_SHOT_LENGTH_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(H3_INSPECTOR_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(H3_INSPECTOR_NODE_CLASS_MAPPINGS)
NODE_CLASS_MAPPINGS.update(H3_BEAT_PROMPT_NODE_CLASS_MAPPINGS) NODE_CLASS_MAPPINGS.update(H3_BEAT_PROMPT_NODE_CLASS_MAPPINGS)
@@ -39,6 +44,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS.update(JSON_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(JSON_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(IMAGE_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(IMAGE_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(H3_LONGVIDEO_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(H3_LONGVIDEO_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(H3_LATENT_UPSCALE_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(H3_SHOT_LENGTH_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(H3_SHOT_LENGTH_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(H3_INSPECTOR_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(H3_INSPECTOR_NODE_DISPLAY_NAME_MAPPINGS)
NODE_DISPLAY_NAME_MAPPINGS.update(H3_BEAT_PROMPT_NODE_DISPLAY_NAME_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(H3_BEAT_PROMPT_NODE_DISPLAY_NAME_MAPPINGS)
+10 -1
View File
@@ -150,7 +150,16 @@ class H3ModelInspector:
@classmethod @classmethod
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return {"required": {"model": ("MODEL",)}} return {
"required": {
"model": (
"MODEL",
{
"tooltip": "MiniMax / H3 model to inspect for quantization and tensor format."
},
)
}
}
def inspect(self, model): def inspect(self, model):
label, _counts, report = _detect(model) label, _counts, report = _detect(model)
File diff suppressed because it is too large Load Diff
+1996 -1213
View File
File diff suppressed because it is too large Load Diff
+4 -3
View File
@@ -39,10 +39,11 @@ class H3ShotLength:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"shot_seconds": ("FLOAT", {"default": 5.0, "min": 0.2, "max": 15.1, "step": 0.5, "shot_seconds": ("FLOAT", {"default": 3.0, "min": 0.2, "max": 15.1, "step": 0.5,
"tooltip": "Length of each shot. Feeds the sampler's shot_seconds AND (as frames) " "tooltip": "Length of each shot. Feeds the sampler's shot_seconds AND (as frames) "
"the preview override. Max ~15s (362 frames)."}), "the preview override. Default 3s matches the common one-beat H3 test shot. Max ~15s (362 frames)."}),
"fps": ("INT", {"default": 24, "min": 1, "max": 60}), "fps": ("INT", {"default": 24, "min": 1, "max": 60,
"tooltip": "Frame rate used for the seconds->frames conversion. H3 itself renders at 24fps, so 24 is the realistic default."}),
}, },
"optional": { "optional": {
"cap_to_h3_max": ("BOOLEAN", {"default": True, "cap_to_h3_max": ("BOOLEAN", {"default": True,
+975 -30
View File
File diff suppressed because it is too large Load Diff
+50 -29
View File
@@ -277,7 +277,14 @@ class DumasJSONStringToObjectNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_string": ("STRING", {"multiline": True}), "json_string": (
"STRING",
{
"multiline": True,
"default": '{\n "shots": [\n {\n "prompt": "Francine stands by the window."\n }\n ]\n}',
"tooltip": "Raw JSON text to parse into a structured JSON object."
},
),
} }
} }
@@ -299,7 +306,14 @@ class DumasStripIterationSuffixNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"filename": ("STRING", {"default": "", "multiline": False}), "filename": (
"STRING",
{
"default": "francine_pose_final.png",
"multiline": False,
"tooltip": "Filename to normalize by removing everything after the first underscore in the stem."
},
),
} }
} }
@@ -316,7 +330,14 @@ class DumasSlugifyStringNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"text": ("STRING", {"default": "", "multiline": False}), "text": (
"STRING",
{
"default": "Francine Coffee Shop",
"multiline": False,
"tooltip": "Text to slugify into lowercase ASCII words joined with hyphens."
},
),
} }
} }
@@ -334,8 +355,8 @@ class DumasJSONObjectToStringNode:
return { return {
"required": { "required": {
"json_object": ("JSON",), "json_object": ("JSON",),
"pretty": ("BOOLEAN", {"default": True}), "pretty": ("BOOLEAN", {"default": True, "tooltip": "Pretty-print the JSON with indentation."}),
"sort_keys": ("BOOLEAN", {"default": False}), "sort_keys": ("BOOLEAN", {"default": False, "tooltip": "Sort object keys alphabetically before serializing."}),
} }
} }
@@ -355,7 +376,7 @@ class DumasJSONGetValueNode:
return { return {
"required": { "required": {
"json_object": ("JSON",), "json_object": ("JSON",),
"path": ("STRING", {"default": "", "multiline": False}), "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to read, such as 'shots.0.prompt'."}),
} }
} }
@@ -373,8 +394,8 @@ class DumasJSONSetValueNode:
return { return {
"required": { "required": {
"json_object": ("JSON",), "json_object": ("JSON",),
"path": ("STRING", {"default": "", "multiline": False}), "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to write, such as 'shots.0.prompt' or 'shots.1.duration'."}),
"value_json": ("STRING", {"multiline": True, "default": "null"}), "value_json": ("STRING", {"multiline": True, "default": '"Francine stands by the window."', "tooltip": "JSON value to store at the path. Must be valid JSON, so strings need quotes."}),
} }
} }
@@ -398,7 +419,7 @@ class DumasJSONHasKeyNode:
return { return {
"required": { "required": {
"json_object": ("JSON",), "json_object": ("JSON",),
"path": ("STRING", {"default": "", "multiline": False}), "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to test for existence."}),
} }
} }
@@ -416,7 +437,7 @@ class DumasJSONRemoveKeyNode:
return { return {
"required": { "required": {
"json_object": ("JSON",), "json_object": ("JSON",),
"path": ("STRING", {"default": "", "multiline": False}), "path": ("STRING", {"default": "shots.0.prompt", "multiline": False, "tooltip": "Dot-path to remove from the object."}),
} }
} }
@@ -434,7 +455,7 @@ class DumasJSONPickFieldsNode:
return { return {
"required": { "required": {
"json_object": ("JSON",), "json_object": ("JSON",),
"paths": ("STRING", {"multiline": True, "default": ""}), "paths": ("STRING", {"multiline": True, "default": "shots.0.prompt\nshots.0.duration", "tooltip": "One dot-path per line. Only those fields are copied into the output object."}),
} }
} }
@@ -463,8 +484,8 @@ class DumasJSONMergeObjectsNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"base_object": ("JSON",), "base_object": ("JSON", {"tooltip": "Base JSON object to start from."}),
"overlay_object": ("JSON",), "overlay_object": ("JSON", {"tooltip": "Overlay JSON object whose keys replace or merge into the base object."}),
} }
} }
@@ -482,7 +503,7 @@ class DumasJSONKeysNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_object": ("JSON",), "json_object": ("JSON", {"tooltip": "JSON object whose top-level keys should be listed."}),
} }
} }
@@ -502,7 +523,7 @@ class DumasJSONArrayLengthNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_array": ("JSON",), "json_array": ("JSON", {"tooltip": "JSON array whose length should be measured."}),
} }
} }
@@ -521,8 +542,8 @@ class DumasJSONArrayAppendNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_array": ("JSON",), "json_array": ("JSON", {"tooltip": "JSON array to append to."}),
"value_json": ("STRING", {"multiline": True, "default": "null"}), "value_json": ("STRING", {"multiline": True, "default": '{"prompt":"Francine looks toward the door."}', "tooltip": "JSON value to append. Must be valid JSON."}),
} }
} }
@@ -548,10 +569,10 @@ class DumasJSONArraySliceNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_array": ("JSON",), "json_array": ("JSON", {"tooltip": "JSON array to slice."}),
"start": ("INT", {"default": 0, "step": 1}), "start": ("INT", {"default": 0, "step": 1, "tooltip": "Zero-based start index."}),
"end": ("INT", {"default": 0, "step": 1}), "end": ("INT", {"default": 0, "step": 1, "tooltip": "Zero-based end index. Use 0 to mean 'to the end'."}),
"step": ("INT", {"default": 1, "step": 1, "min": 1}), "step": ("INT", {"default": 1, "step": 1, "min": 1, "tooltip": "Slice step size."}),
} }
} }
@@ -572,9 +593,9 @@ class DumasJSONArrayIteratorNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_input": ("JSON",), "json_input": ("JSON", {"tooltip": "JSON array to iterate over."}),
"index": ("INT", {"default": 0, "min": 0, "step": 1}), "index": ("INT", {"default": 0, "min": 0, "step": 1, "tooltip": "Current zero-based index."}),
"mode": (["fixed", "incr", "decr"], {"default": "fixed"}), "mode": (["fixed", "incr", "decr"], {"default": "fixed", "tooltip": "Keep the index fixed, increment it, or decrement it before reading."}),
} }
} }
@@ -600,9 +621,9 @@ class DumasJSONObjectIteratorNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_input": ("JSON",), "json_input": ("JSON", {"tooltip": "JSON object whose key/value pairs should be iterated in insertion order."}),
"index": ("INT", {"default": 0, "min": 0, "step": 1}), "index": ("INT", {"default": 0, "min": 0, "step": 1, "tooltip": "Current zero-based index into the object's items."}),
"mode": (["fixed", "incr", "decr"], {"default": "fixed"}), "mode": (["fixed", "incr", "decr"], {"default": "fixed", "tooltip": "Keep the index fixed, increment it, or decrement it before reading."}),
} }
} }
@@ -628,7 +649,7 @@ class DumasJSONFlattenNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"json_input": ("JSON",), "json_input": ("JSON", {"tooltip": "Nested JSON value to flatten into dot-path keys."}),
} }
} }
@@ -645,7 +666,7 @@ class DumasJSONUnflattenNode:
def INPUT_TYPES(cls): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"flat_json_object": ("JSON",), "flat_json_object": ("JSON", {"tooltip": "Flat JSON object whose keys are dot-paths to rebuild into nested JSON."}),
} }
} }
+375
View File
@@ -0,0 +1,375 @@
import { app } from "/scripts/app.js";
import { applyAdaptiveCanvasOnly } from "../shared/nodes2.mjs";
const COMFY_CLASS = "DumasH3LongVideos";
const STATE_PROPERTY = "dumas_h3_longvideos_section_state";
const DOM_WIDGET_NAME = "dumas_h3_longvideos_sections";
const MIN_WIDTH = 520;
const MIN_HEIGHT = 280;
const GROUPS = [
{
id: "prompt",
label: "Prompt",
defaultCollapsed: false,
widgets: ["prompt", "resolution", "megapixels", "beat_split", "anchor_override", "shot_seconds", "plan_only", "fps"],
},
{
id: "refs",
label: "Refs",
defaultCollapsed: true,
widgets: ["ref_mode", "ref_image_size", "ref_noise_aug", "character_memory", "trim_seam", "vary_seed_per_shot", "handoff_offset"],
},
{
id: "sampling",
label: "Sampling",
defaultCollapsed: true,
widgets: [
"steps", "cfg", "sampler_name", "scheduler", "seed",
"apply_model_sampling", "shift_video", "shift_audio",
"vram_headroom_gb", "allow_res_backoff",
"decode_tile_frames", "decode_tile_size",
],
},
{
id: "audio",
label: "Audio",
defaultCollapsed: true,
widgets: [
"global_soundscape", "non_diegetic_music", "auto_soundscape",
"auto_silence_nonspeech", "allow_nonspeech_vocals",
"mute_nonspeech_audio", "mute_fade_ms",
],
},
{
id: "scene",
label: "Scene Logic",
defaultCollapsed: true,
widgets: [
"auto_wardrobe", "auto_props", "prevent_nudity", "exposed_terms",
"anatomy_guard", "subject_count_guard", "lock_restraints",
"contact_guard", "motion_guard", "solidity_guard",
],
},
{
id: "finish",
label: "Upscale",
defaultCollapsed: true,
widgets: [
"upscale", "upscale_model", "upscale_target_short_edge", "upscale_batch",
],
},
{
id: "overlay",
label: "Overlays",
defaultCollapsed: true,
widgets: [
"watermark_text", "watermark_position", "watermark_size", "watermark_opacity", "watermark_margin",
"intro_text", "intro_position", "intro_seconds", "intro_fade", "intro_size",
"overlay_font", "overlay_stroke",
],
},
];
function injectCSS() {
if (document.getElementById("dumas-h3lv-sections-css")) return;
const style = document.createElement("style");
style.id = "dumas-h3lv-sections-css";
style.textContent = `
.dh3lv-sections {
box-sizing: border-box;
width: 100%;
padding: 8px 10px 6px;
color: #e6e7eb;
font: 12px/1.35 "Segoe UI", sans-serif;
pointer-events: auto;
background: linear-gradient(180deg, rgba(33, 36, 42, 0.96), rgba(22, 24, 29, 0.96));
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}
.dh3lv-sections-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
margin-bottom: 8px;
}
.dh3lv-sections-title {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #9da5b1;
}
.dh3lv-sections-actions {
display: flex;
gap: 6px;
}
.dh3lv-sections-list {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.dh3lv-chip,
.dh3lv-action {
appearance: none;
border: 1px solid #464d59;
background: #262c35;
color: #d7dce3;
border-radius: 999px;
padding: 5px 9px;
cursor: pointer;
font: inherit;
line-height: 1.1;
}
.dh3lv-chip[data-open="true"] {
background: #d96f2b;
border-color: #f09358;
color: #fff7f0;
}
.dh3lv-chip:hover,
.dh3lv-action:hover {
filter: brightness(1.06);
}
.dh3lv-count {
opacity: 0.78;
margin-left: 4px;
font-size: 11px;
}
`;
document.head.appendChild(style);
}
function defaultState() {
const state = {};
for (const group of GROUPS) state[group.id] = !group.defaultCollapsed;
return state;
}
function parseState(value) {
let parsed = value;
if (typeof parsed === "string") {
try {
parsed = JSON.parse(parsed);
} catch (_error) {
parsed = null;
}
}
const base = defaultState();
if (!parsed || typeof parsed !== "object") return base;
for (const group of GROUPS) {
if (typeof parsed[group.id] === "boolean") base[group.id] = parsed[group.id];
}
return base;
}
function readState(node) {
return parseState(node.properties?.[STATE_PROPERTY] || node._dh3lvSectionState || "");
}
function writeState(node, state) {
const normalized = parseState(state);
const serialized = JSON.stringify(normalized);
node._dh3lvSectionState = serialized;
node.properties = node.properties || {};
node.properties[STATE_PROPERTY] = serialized;
}
function findWidget(node, name) {
return (node.widgets || []).find((widget) => widget?.name === name) || null;
}
function isInteractiveTarget(target) {
return !!target?.closest?.("button, input, textarea, select, label");
}
function stopCanvasEvent(event) {
if (isInteractiveTarget(event.target)) event.stopPropagation();
}
function stopCanvasKeyboard(event) {
if (isInteractiveTarget(event.target)) event.stopImmediatePropagation();
}
function setWidgetHidden(widget, hidden) {
if (!widget) return;
if (!widget._dh3lvOriginal) {
widget._dh3lvOriginal = {
type: widget.type,
computeSize: widget.computeSize,
hidden: widget.hidden,
};
}
if (hidden) {
widget.type = "hidden";
widget.hidden = true;
widget.computeSize = () => [0, -4];
return;
}
widget.type = widget._dh3lvOriginal.type;
widget.hidden = !!widget._dh3lvOriginal.hidden;
widget.computeSize = widget._dh3lvOriginal.computeSize;
}
function applyVisibility(node) {
const state = readState(node);
for (const group of GROUPS) {
for (const name of group.widgets) {
const widget = findWidget(node, name);
if (!widget || widget.name === DOM_WIDGET_NAME) continue;
setWidgetHidden(widget, !state[group.id]);
}
}
}
function resizeNode(node) {
requestAnimationFrame(() => {
const size = node.computeSize?.();
if (Array.isArray(size)) {
node.size[0] = Math.max(MIN_WIDTH, size[0] || 0, node.size?.[0] || 0);
node.size[1] = Math.max(MIN_HEIGHT, size[1] || 0);
}
node.setDirtyCanvas?.(true, true);
});
}
function renderToolbar(node) {
const ui = node._dh3lvUI;
if (!ui) return;
const state = readState(node);
ui.list.innerHTML = "";
for (const group of GROUPS) {
const button = document.createElement("button");
button.type = "button";
button.className = "dh3lv-chip";
button.dataset.open = state[group.id] ? "true" : "false";
button.textContent = state[group.id] ? `Hide ${group.label}` : `Show ${group.label}`;
const count = document.createElement("span");
count.className = "dh3lv-count";
count.textContent = String(group.widgets.filter((name) => findWidget(node, name)).length);
button.appendChild(count);
button.addEventListener("click", () => {
const next = readState(node);
next[group.id] = !next[group.id];
writeState(node, next);
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
});
ui.list.appendChild(button);
}
}
function setAll(node, open) {
const next = {};
for (const group of GROUPS) next[group.id] = !!open;
writeState(node, next);
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
}
function setupNode(node) {
if (node._dh3lvUI) return;
injectCSS();
writeState(node, readState(node));
const root = document.createElement("div");
root.className = "dh3lv-sections";
const head = document.createElement("div");
head.className = "dh3lv-sections-head";
const title = document.createElement("div");
title.className = "dh3lv-sections-title";
title.textContent = "Sections";
const actions = document.createElement("div");
actions.className = "dh3lv-sections-actions";
const expandAll = document.createElement("button");
expandAll.type = "button";
expandAll.className = "dh3lv-action";
expandAll.textContent = "Expand All";
expandAll.addEventListener("click", () => setAll(node, true));
const collapseAll = document.createElement("button");
collapseAll.type = "button";
collapseAll.className = "dh3lv-action";
collapseAll.textContent = "Collapse Extras";
collapseAll.addEventListener("click", () => {
const next = defaultState();
writeState(node, next);
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
});
actions.append(expandAll, collapseAll);
head.append(title, actions);
const list = document.createElement("div");
list.className = "dh3lv-sections-list";
root.append(head, list);
root.addEventListener("pointerdown", stopCanvasEvent);
root.addEventListener("mousedown", stopCanvasEvent);
root.addEventListener("click", stopCanvasEvent);
root.addEventListener("dblclick", stopCanvasEvent);
root.addEventListener("keydown", stopCanvasKeyboard, true);
node._dh3lvUI = { root, list };
const widget = node.addDOMWidget(DOM_WIDGET_NAME, "custom", root, {
getValue: () => null,
setValue: () => {},
serialize: false,
getMinHeight: () => 52,
hideOnZoom: false,
});
applyAdaptiveCanvasOnly(widget);
const widgets = node.widgets || [];
const index = widgets.indexOf(widget);
if (index > 0) {
widgets.splice(index, 1);
widgets.unshift(widget);
}
applyVisibility(node);
renderToolbar(node);
resizeNode(node);
}
app.registerExtension({
name: "Dumas.H3LongVideosSections",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== COMFY_CLASS) return;
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function onNodeCreated() {
const result = originalOnNodeCreated?.apply(this, arguments);
setupNode(this);
return result;
};
const originalConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function onConfigure() {
const result = originalConfigure?.apply(this, arguments);
setupNode(this);
writeState(this, readState(this));
applyVisibility(this);
renderToolbar(this);
resizeNode(this);
return result;
};
const originalSerialize = nodeType.prototype.onSerialize;
nodeType.prototype.onSerialize = function onSerialize(o) {
writeState(this, readState(this));
const result = originalSerialize?.apply(this, arguments);
if (o && this.properties?.[STATE_PROPERTY]) {
o.properties = o.properties || {};
o.properties[STATE_PROPERTY] = this.properties[STATE_PROPERTY];
}
return result;
};
},
});
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "dumasnodes" name = "dumasnodes"
description = "Dumas-branded generic utility nodes for ComfyUI, starting with JSON helpers." description = "Dumas-branded generic utility nodes for ComfyUI, starting with JSON helpers."
version = "0.1.0" version = "0.1.1"
license = { file = "LICENSE" } license = { file = "LICENSE" }
[project.urls] [project.urls]
File diff suppressed because it is too large Load Diff
+348 -12
View File
@@ -245,7 +245,6 @@ class DumasImageNodeTests(unittest.TestCase):
result = node.build_reference( result = node.build_reference(
image=image, image=image,
picture_id="2",
character_id="char_dave", character_id="char_dave",
name="Dave", name="Dave",
alias="The Locksmith", alias="The Locksmith",
@@ -270,10 +269,10 @@ class DumasImageNodeTests(unittest.TestCase):
"id": "char-dave", "id": "char-dave",
"name": "Dave", "name": "Dave",
"aliases": ["The Locksmith"], "aliases": ["The Locksmith"],
"picture_id": 2, "picture_id": None,
"picture_label": "<Picture 2>", "picture_label": "",
"image": image, "image": image,
"summary": "Dave shown in <Picture 2>.", "summary": "Dave reference.",
"description": "Square jaw, tired eyes, cropped brown hair.", "description": "Square jaw, tired eyes, cropped brown hair.",
"wardrobe": "weathered red flight jacket, grey cargo shorts, black boots", "wardrobe": "weathered red flight jacket, grey cargo shorts, black boots",
"general": "wears a long grey coat", "general": "wears a long grey coat",
@@ -289,13 +288,16 @@ class DumasImageNodeTests(unittest.TestCase):
}, },
) )
def test_character_reference_input_types_do_not_expose_picture_id(self):
required = self.image_nodes.DumasCharacterReferenceNode.INPUT_TYPES()["required"]
self.assertNotIn("picture_id", required)
def test_character_reference_handles_missing_optional_fields(self): def test_character_reference_handles_missing_optional_fields(self):
node = self.image_nodes.DumasCharacterReferenceNode() node = self.image_nodes.DumasCharacterReferenceNode()
image = FakeTensorBatch() image = FakeTensorBatch()
result = node.build_reference( result = node.build_reference(
image=image, image=image,
picture_id="4",
character_id="", character_id="",
name="", name="",
alias="", alias="",
@@ -313,8 +315,8 @@ class DumasImageNodeTests(unittest.TestCase):
reference = result[0] reference = result[0]
self.assertEqual(reference["kind"], "character") self.assertEqual(reference["kind"], "character")
self.assertEqual(reference["picture_id"], 4) self.assertIsNone(reference["picture_id"])
self.assertEqual(reference["picture_label"], "<Picture 4>") self.assertEqual(reference["picture_label"], "")
self.assertEqual(reference["wardrobe"], "") self.assertEqual(reference["wardrobe"], "")
self.assertEqual(reference["general"], "") self.assertEqual(reference["general"], "")
self.assertEqual(reference["facts"]["age"], "") self.assertEqual(reference["facts"]["age"], "")
@@ -325,7 +327,6 @@ class DumasImageNodeTests(unittest.TestCase):
result = node.build_reference( result = node.build_reference(
image=image, image=image,
picture_id="9",
location_id="coffee-shop-01", location_id="coffee-shop-01",
name="Coffee Shop", name="Coffee Shop",
alias="Cafe Interior", alias="Cafe Interior",
@@ -340,10 +341,10 @@ class DumasImageNodeTests(unittest.TestCase):
"id": "coffee-shop-01", "id": "coffee-shop-01",
"name": "Coffee Shop", "name": "Coffee Shop",
"aliases": ["Cafe Interior"], "aliases": ["Cafe Interior"],
"picture_id": 9, "picture_id": None,
"picture_label": "<Picture 9>", "picture_label": "",
"image": image, "image": image,
"summary": "Coffee Shop shown in <Picture 9>.", "summary": "Coffee Shop reference.",
"description": "Warm tungsten lighting, narrow counter, rainy front window.", "description": "Warm tungsten lighting, narrow counter, rainy front window.",
"wardrobe": "", "wardrobe": "",
"general": "Evening ambience, cramped but cozy.", "general": "Evening ambience, cramped but cozy.",
@@ -351,6 +352,328 @@ class DumasImageNodeTests(unittest.TestCase):
}, },
) )
def test_location_reference_input_types_do_not_expose_picture_id(self):
required = self.image_nodes.DumasLocationReferenceNode.INPUT_TYPES()["required"]
self.assertNotIn("picture_id", required)
def test_character_helper_restores_image_and_text_outputs(self):
node = self.image_nodes.DumasCharacterHelperNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
result = node.build_character_text(
image1=image1,
image2=image2,
image1_picture_id="1",
image2_picture_id="2",
character_id="char_dave",
name="Dave",
alias="The Locksmith",
gender="male",
age="41",
nationality="English",
occupation="a detective",
height_feet="6",
height_inches="2",
accent="English",
general="Moves carefully and notices every exit",
wardrobe="weathered red flight jacket, grey cargo shorts, black boots",
)
self.assertIs(result[0], image1)
self.assertIs(result[1], image2)
self.assertIn("<Picture 1> and <Picture 2> reference the same character", result[2])
self.assertIn("Dave is also known as The Locksmith", result[2])
self.assertIn("is 41 years old", result[2])
self.assertEqual(result[3], "Dave = weathered red flight jacket, grey cargo shorts, black boots")
self.assertIs(result[4]["image"], image1)
self.assertIs(result[5]["image"], image2)
self.assertEqual(result[4]["id"], "char-dave")
self.assertEqual(result[5]["id"], "char-dave")
self.assertEqual(result[4]["name"], "Dave")
self.assertEqual(result[4]["aliases"], ["The Locksmith"])
self.assertEqual(result[4]["facts"]["age"], "41")
self.assertEqual(result[4]["facts"]["height_feet"], "6")
self.assertEqual(result[4]["facts"]["height_inches"], "2")
self.assertEqual(result[4]["wardrobe"], "weathered red flight jacket, grey cargo shorts, black boots")
self.assertEqual(len(result), 6)
def test_location_helper_matches_character_helper_shape_without_wardrobe(self):
node = self.image_nodes.DumasLocationHelperNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
result = node.build_location_text(
image1=image1,
image2=image2,
image1_picture_id="3",
image2_picture_id="4",
location_id="coffee-shop-01",
name="Coffee Shop",
alias="Cafe Interior",
description="Warm tungsten lighting, narrow counter, rainy front window",
general="Evening ambience, cramped but cozy",
)
self.assertIs(result[0], image1)
self.assertIs(result[1], image2)
self.assertIn("<Picture 3> and <Picture 4> reference the same location", result[2])
self.assertIn("Coffee Shop is also known as Cafe Interior", result[2])
self.assertIn("Warm tungsten lighting, narrow counter, rainy front window.", result[2])
self.assertIn("Evening ambience, cramped but cozy.", result[2])
self.assertIs(result[3]["image"], image1)
self.assertIs(result[4]["image"], image2)
self.assertEqual(result[3]["kind"], "location")
self.assertEqual(result[4]["kind"], "location")
self.assertEqual(result[3]["id"], "coffee-shop-01")
self.assertEqual(result[4]["id"], "coffee-shop-01")
self.assertEqual(result[3]["name"], "Coffee Shop")
self.assertEqual(result[3]["aliases"], ["Cafe Interior"])
self.assertEqual(result[3]["description"], "Warm tungsten lighting, narrow counter, rainy front window")
self.assertEqual(result[3]["general"], "Evening ambience, cramped but cozy")
self.assertEqual(len(result), 5)
def test_soundscape_helper_defaults_to_selected_preset_description(self):
node = self.image_nodes.DumasSoundscapeHelperNode()
result = node.build_soundscape("rainy street", "")
self.assertEqual(result[0], "steady rain, wet pavement, distant traffic hum")
def test_background_music_helper_defaults_to_selected_preset_description(self):
node = self.image_nodes.DumasBackgroundMusicHelperNode()
result = node.build_bgm("subtle tension", "")
self.assertEqual(result[0], "low, restrained tension bed with sparse pulses and no vocals")
def test_h3_prompt_curator_compacts_named_references(self):
node = self.image_nodes.DumasH3PromptCuratorNode()
dave_image = FakeTensorBatch()
cafe_image = FakeTensorBatch()
van_image = FakeTensorBatch()
dave = self.image_nodes.make_reference(
kind="character",
image=dave_image,
name="Dave",
aliases="The Locksmith",
description="tired eyes, cropped brown hair",
wardrobe="red flight jacket",
)
cafe = self.image_nodes.make_reference(
kind="location",
image=cafe_image,
name="Coffee Shop",
description="warm tungsten lighting and rainy windows",
)
van = self.image_nodes.make_reference(
kind="location",
image=van_image,
name="Blue Van",
description="scuffed blue delivery van",
)
result = node.curate_prompt(
action_prompt="Dave runs from the Coffee Shop into the rain.",
anatomy_guard="auto",
subject_count_guard="auto",
anchor="grounded handheld thriller",
soundscape="steady rain",
bgm="low suspense music",
ref_1=dave,
ref_2=van,
ref_3=cafe,
)
prompt = result[0]
self.assertIn("<Picture 1> Dave", prompt)
self.assertIn("<Picture 2> Coffee Shop", prompt)
self.assertIn("Action: Dave runs from the Coffee Shop into the rain.", prompt)
self.assertIn("Anatomy guard:", prompt)
self.assertIn("Subject count guard:", prompt)
self.assertIn("overall_soundscape: steady rain", prompt)
self.assertIn("background_music: low suspense music", prompt)
self.assertIn("exactly one named character: <Picture 1> Dave", prompt)
self.assertIs(result[1], dave_image)
self.assertIs(result[2], cafe_image)
self.assertIsNone(result[3])
self.assertEqual(result[10], 2)
self.assertIn("input 3-><Picture 2> Coffee Shop", result[11])
def test_h3_prompt_curator_renumbers_explicit_reference_tags(self):
node = self.image_nodes.DumasH3PromptCuratorNode()
image1 = FakeTensorBatch()
image3 = FakeTensorBatch()
unused = FakeTensorBatch()
first = self.image_nodes.make_reference(kind="character", image=image1, name="Maya")
second = self.image_nodes.make_reference(kind="location", image=unused, name="Lobby")
third = self.image_nodes.make_reference(kind="location", image=image3, name="Rooftop")
result = node.curate_prompt(
action_prompt="<Picture 1> Maya crosses to <ref3> as the wind rises.",
anatomy_guard="off",
subject_count_guard="off",
ref_1=first,
ref_2=second,
ref_3=third,
)
prompt = result[0]
self.assertIn("<Picture 1> Maya crosses to <Picture 2>", prompt)
self.assertNotIn("<Picture 3>", prompt)
self.assertIs(result[1], image1)
self.assertIs(result[2], image3)
self.assertIsNone(result[3])
self.assertEqual(result[10], 2)
def test_h3_prompt_curator_can_force_subject_count_without_character_refs(self):
node = self.image_nodes.DumasH3PromptCuratorNode()
result = node.curate_prompt(
action_prompt="A locked-off shot of the empty corridor.",
anatomy_guard="off",
subject_count_guard="on",
)
self.assertIn("Subject count guard:", result[0])
self.assertIn("Only include the people explicitly described", result[0])
self.assertEqual(result[10], 0)
def test_h3_prompt_curator_treats_helper_image_pair_as_one_character(self):
helper = self.image_nodes.DumasCharacterHelperNode()
curator = self.image_nodes.DumasH3PromptCuratorNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
helper_result = helper.build_character_text(
image1=image1,
image2=image2,
image1_picture_id="1",
image2_picture_id="2",
character_id="char_dave",
name="Dave",
alias="The Locksmith",
gender="male",
age="41",
nationality="English",
occupation="detective",
height_feet="6",
height_inches="2",
accent="English",
general="Tired eyes, cropped brown hair",
wardrobe="weathered red flight jacket",
)
result = curator.curate_prompt(
action_prompt="Dave checks the locked door.",
anatomy_guard="on",
subject_count_guard="auto",
ref_1=helper_result[4],
ref_2=helper_result[5],
)
self.assertIs(result[1], image1)
self.assertIs(result[2], image2)
self.assertEqual(result[10], 2)
self.assertIn("Character facts for <Picture 1> Dave", result[0])
self.assertIn("41 years old", result[0])
self.assertIn("6 foot 2 tall", result[0])
self.assertIn("exactly one named character: <Picture 1> Dave", result[0])
self.assertNotIn("exactly 2 named characters", result[0])
def test_h3_prompt_curator_uses_location_helper_references_by_name(self):
helper = self.image_nodes.DumasLocationHelperNode()
curator = self.image_nodes.DumasH3PromptCuratorNode()
image1 = FakeTensorBatch()
image2 = FakeTensorBatch()
helper_result = helper.build_location_text(
image1=image1,
image2=image2,
image1_picture_id="1",
image2_picture_id="2",
location_id="coffee_shop",
name="Coffee Shop",
alias="Cafe Interior",
description="Warm tungsten lighting, narrow counter, rainy front window",
general="Evening ambience, cramped but cozy",
)
result = curator.curate_prompt(
action_prompt="A slow push through the Coffee Shop as rain streaks the windows.",
anatomy_guard="on",
subject_count_guard="auto",
ref_1=helper_result[3],
ref_2=helper_result[4],
)
self.assertIs(result[1], image1)
self.assertIs(result[2], image2)
self.assertEqual(result[10], 2)
self.assertIn("<Picture 1> Coffee Shop", result[0])
self.assertIn("<Picture 2> Coffee Shop", result[0])
self.assertIn("Location context for <Picture 1> Coffee Shop", result[0])
self.assertIn("Warm tungsten lighting", result[0])
self.assertNotIn("Subject count guard:", result[0])
def test_h3_prompt_curator_defaults_anatomy_guard_to_on(self):
required = self.image_nodes.DumasH3PromptCuratorNode.INPUT_TYPES()["required"]
self.assertEqual(required["anatomy_guard"][1]["default"], "on")
def test_helper_node_mappings_use_general_purpose_helpers(self):
mappings = self.image_nodes.NODE_CLASS_MAPPINGS
display = self.image_nodes.NODE_DISPLAY_NAME_MAPPINGS
self.assertIs(mappings["DumasCharacterHelper"], self.image_nodes.DumasCharacterHelperNode)
self.assertIs(mappings["DumasLocationHelper"], self.image_nodes.DumasLocationHelperNode)
self.assertIs(mappings["DumasSoundscapeHelper"], self.image_nodes.DumasSoundscapeHelperNode)
self.assertIs(mappings["DumasBackgroundMusicHelper"], self.image_nodes.DumasBackgroundMusicHelperNode)
self.assertIs(mappings["DumasH3PromptCurator"], self.image_nodes.DumasH3PromptCuratorNode)
self.assertEqual(display["DumasCharacterHelper"], "Dumas Character Helper")
self.assertEqual(display["DumasLocationHelper"], "Dumas Location Helper")
self.assertEqual(display["DumasSoundscapeHelper"], "Dumas Soundscape Helper")
self.assertEqual(display["DumasBackgroundMusicHelper"], "Dumas Background Music Helper")
self.assertEqual(display["DumasH3PromptCurator"], "Dumas H3 Prompt Curator")
def test_h3_prompt_curator_uses_documented_reference_limits(self):
node = self.image_nodes.DumasH3PromptCuratorNode()
self.assertEqual(len(node.RETURN_TYPES), 12)
self.assertEqual(node.RETURN_NAMES[1:10], tuple(f"ref_image_{i}" for i in range(1, 10)))
def test_normalize_reference_upgrades_generic_summary_with_socket_picture_id(self):
image = FakeTensorBatch()
reference = self.image_nodes.normalize_reference(
{
"kind": "character",
"name": "Dave",
"image": image,
"summary": "Dave reference.",
},
picture_id=3,
allow_image_fallback=False,
)
self.assertEqual(reference["picture_id"], 3)
self.assertEqual(reference["picture_label"], "<Picture 3>")
self.assertEqual(reference["summary"], "Dave shown in <Picture 3>.")
def test_normalize_reference_keeps_custom_summary_when_socket_picture_id_is_added(self):
image = FakeTensorBatch()
reference = self.image_nodes.normalize_reference(
{
"kind": "character",
"name": "Dave",
"image": image,
"summary": "Primary hero look for the opening close-up.",
},
picture_id=3,
allow_image_fallback=False,
)
self.assertEqual(reference["picture_id"], 3)
self.assertEqual(reference["summary"], "Primary hero look for the opening close-up.")
def test_anchor_style_node_exposes_requested_presets(self): def test_anchor_style_node_exposes_requested_presets(self):
input_types = self.image_nodes.DumasAnchorStyleNode.INPUT_TYPES() input_types = self.image_nodes.DumasAnchorStyleNode.INPUT_TYPES()
options = input_types["required"]["anchor_style"][0] options = input_types["required"]["anchor_style"][0]
@@ -375,7 +698,20 @@ class DumasImageNodeTests(unittest.TestCase):
self.assertIn("found-footage", result[0]) self.assertIn("found-footage", result[0])
self.assertIn("real time", result[0]) self.assertIn("real time", result[0])
self.assertIn("persistent camera language", result[0]) self.assertNotIn("persistent camera language", result[0])
def test_anchor_style_node_strips_legacy_persistent_anchor_note(self):
node = self.image_nodes.DumasAnchorStyleNode()
legacy = (
"Gritty handheld realism. Keep this anchor focused on persistent camera "
"language, lighting, texture, environment treatment, and tone; do not "
"name characters or describe one-off actions."
)
result = node.build_anchor("cinematic action movie", legacy)
self.assertEqual(result[0], "Gritty handheld realism.")
self.assertNotIn("persistent camera language", result[0])
def test_anchor_style_node_prefers_manual_description_edits(self): def test_anchor_style_node_prefers_manual_description_edits(self):
node = self.image_nodes.DumasAnchorStyleNode() node = self.image_nodes.DumasAnchorStyleNode()