Compare commits

...
86 Commits
Author SHA1 Message Date
chris.dumas 1a4cfbfd7e Add compiled prompt reference output pairs 2026-09-10 08:15:33 +00:00
chris.dumas eac4e73b47 Expose individual prompt reference descriptions 2026-09-10 07:59:11 +00:00
chris.dumas e973a0d785 Expose H3 prompt curator components 2026-09-09 15:34:06 +00:00
chris.dumas 388de1d837 Sync helper preset text boxes 2026-09-07 13:26:29 +00:00
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
15 changed files with 7427 additions and 1327 deletions
File diff suppressed because it is too large Load Diff
+41 -4
View File
@@ -38,17 +38,32 @@
- 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`, `anchor`, `sounds`, `bgm`, `original_ref_1` through `original_ref_9`, `reference_description`, `original_ref_description_1` through `original_ref_description_9`, `compiled_ref_image_1` through `compiled_ref_image_9`, `compiled_ref_description_1` through `compiled_ref_description_9`
- 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.
- Extra component outputs expose the cleaned anchor, sounds, BGM, selected structured references in compacted order, the compiled reference-description block used inside the prompt, and plain image/compiled-description socket pairs for each selected reference.
- 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,21 +75,43 @@
- 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`
- Offers a large preset dropdown of anchor-style titles such as cinematic action movie, comedy, found footage, 90s sitcom, mobile/cell phone captured, news broadcast, mockumentary, heist thriller, cyberpunk neon, nature documentary, courtroom drama, and more. - Offers a large preset dropdown of anchor-style titles such as cinematic action movie, comedy, found footage, 90s sitcom, mobile/cell phone captured, news broadcast, mockumentary, heist thriller, cyberpunk neon, nature documentary, courtroom drama, and more.
- 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.
- `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; the preset fills the editable textbox, and the edited text 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; the preset fills the editable textbox, and the edited text flows into `Dumas H3 Prompt Curator`.
- `Dumas JSON String to Object` - `Dumas JSON String to Object`
- Input: `json_string` - Input: `json_string`
@@ -232,7 +269,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
+1063 -280
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,
+1051 -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."}),
} }
} }
+51 -7
View File
@@ -3,7 +3,7 @@ import { app } from "/scripts/app.js";
const NODE_NAME = "DumasAnchorStyle"; const NODE_NAME = "DumasAnchorStyle";
const STYLE_INPUT = "anchor_style"; const STYLE_INPUT = "anchor_style";
const DESCRIPTION_INPUT = "style_description"; const DESCRIPTION_INPUT = "style_description";
const H3_NOTE = " Keep this anchor focused on persistent camera language, lighting, texture, environment treatment, and tone; do not name characters or describe one-off actions."; const H3_NOTE = "";
const PRESETS = { const PRESETS = {
"cinematic action movie": "Big-screen action cinema with assertive visual storytelling: dynamic camera placement, strong forward momentum, crisp geography, muscular lighting contrast, practical atmosphere, and a sense of physical consequence. Favor heroic framing, controlled handheld energy or motivated tracking moves, dramatic silhouettes, tasteful lens flares, impact-driven pacing, and polished studio spectacle without drifting into comic-book unreality unless the shot explicitly asks for it." + H3_NOTE, "cinematic action movie": "Big-screen action cinema with assertive visual storytelling: dynamic camera placement, strong forward momentum, crisp geography, muscular lighting contrast, practical atmosphere, and a sense of physical consequence. Favor heroic framing, controlled handheld energy or motivated tracking moves, dramatic silhouettes, tasteful lens flares, impact-driven pacing, and polished studio spectacle without drifting into comic-book unreality unless the shot explicitly asks for it." + H3_NOTE,
"comedy": "Play the scene for comedic readability and timing: clear staging, expressive performances, slightly heightened reactions, clean eyelines, and visual beats that leave room for the joke to land. Use bright approachable lighting, grounded but playful production design, readable framing, and a tone that feels observant, awkward, or absurd without becoming broad parody unless the action supports it." + H3_NOTE, "comedy": "Play the scene for comedic readability and timing: clear staging, expressive performances, slightly heightened reactions, clean eyelines, and visual beats that leave room for the joke to land. Use bright approachable lighting, grounded but playful production design, readable framing, and a tone that feels observant, awkward, or absurd without becoming broad parody unless the action supports it." + H3_NOTE,
@@ -51,28 +51,72 @@ const PRESETS = {
"fantasy adventure": "Rousing fantasy-adventure language: scenic scale, adventurous clarity, tactile costume-and-prop detail, and camera movement that feels exploratory rather than oppressive. Favor storybook geography, weathered materials, golden or stormy atmosphere, and a tone of peril, wonder, and forward motion." + H3_NOTE, "fantasy adventure": "Rousing fantasy-adventure language: scenic scale, adventurous clarity, tactile costume-and-prop detail, and camera movement that feels exploratory rather than oppressive. Favor storybook geography, weathered materials, golden or stormy atmosphere, and a tone of peril, wonder, and forward motion." + H3_NOTE,
}; };
const SOUNDSCAPE_PRESETS = {
"quiet interior": "quiet indoor room tone, faint ventilation and distant household ambience",
"rainy street": "steady rain, wet pavement, distant traffic hum",
"cafe": "low room tone, faint glassware, cutlery, and muted conversation",
"city night": "distant traffic hum, occasional horn, night air",
"forest": "wind in leaves, distant birds, soft natural ambience",
"industrial": "large interior reverb, distant metal ticks, low machine hum",
"silent": "no dialogue, no vocals, only the natural ambient bed of the scene",
"custom": "",
};
const BGM_PRESETS = {
"none": "",
"subtle tension": "low, restrained tension bed with sparse pulses and no vocals",
"cinematic suspense": "cinematic suspense score with muted strings, low drones, and controlled rising pressure",
"emotional piano": "soft emotional piano underscoring with gentle space and no vocals",
"dark ambient": "dark ambient music bed with deep drones, distant texture, and slow unease",
"hopeful orchestral": "hopeful orchestral underscore with warm strings, gentle brass, and restrained lift",
"retro synth": "retro synth score with analog pulses, warm pads, and steady momentum",
"action pulse": "driving action pulse with percussion, rhythmic bass, and urgent forward motion",
"lo-fi": "soft lo-fi instrumental bed with mellow rhythm and warm tape texture",
"no vocals": "instrumental background music only, no singing, no lyrics, no vocal hooks",
"custom": "",
};
const NODE_CONFIGS = {
[NODE_NAME]: {
presetInput: STYLE_INPUT,
descriptionInput: DESCRIPTION_INPUT,
presets: PRESETS,
},
DumasSoundscapeHelper: {
presetInput: "soundscape",
descriptionInput: "soundscape_description",
presets: SOUNDSCAPE_PRESETS,
},
DumasBackgroundMusicHelper: {
presetInput: "bgm",
descriptionInput: "bgm_description",
presets: BGM_PRESETS,
},
};
function findWidget(node, name) { function findWidget(node, name) {
return (node.widgets || []).find((widget) => widget?.name === name) || null; return (node.widgets || []).find((widget) => widget?.name === name) || null;
} }
app.registerExtension({ app.registerExtension({
name: "Dumas.AnchorStyle", name: "Dumas.PresetTextHelpers",
async beforeRegisterNodeDef(nodeType, nodeData) { async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== NODE_NAME) return; const config = NODE_CONFIGS[nodeData?.name];
if (!config) return;
const originalOnNodeCreated = nodeType.prototype.onNodeCreated; const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function onNodeCreated() { nodeType.prototype.onNodeCreated = function onNodeCreated() {
const created = originalOnNodeCreated?.apply(this, arguments); const created = originalOnNodeCreated?.apply(this, arguments);
const styleWidget = findWidget(this, STYLE_INPUT); const styleWidget = findWidget(this, config.presetInput);
const descriptionWidget = findWidget(this, DESCRIPTION_INPUT); const descriptionWidget = findWidget(this, config.descriptionInput);
if (!styleWidget || !descriptionWidget) return created; if (!styleWidget || !descriptionWidget) return created;
const originalCallback = styleWidget.callback; const originalCallback = styleWidget.callback;
styleWidget.callback = (...args) => { styleWidget.callback = (...args) => {
const selected = String(styleWidget.value || ""); const selected = String(styleWidget.value || "");
if (Object.hasOwn(PRESETS, selected)) { if (Object.hasOwn(config.presets, selected)) {
descriptionWidget.value = PRESETS[selected]; descriptionWidget.value = config.presets[selected];
descriptionWidget.inputEl?.dispatchEvent(new Event("input", { bubbles: true })); descriptionWidget.inputEl?.dispatchEvent(new Event("input", { bubbles: true }));
} }
this.setDirtyCanvas?.(true, true); this.setDirtyCanvas?.(true, true);
+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
+384 -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,364 @@ 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])
self.assertEqual(result[12], "grounded handheld thriller")
self.assertEqual(result[13], "steady rain")
self.assertEqual(result[14], "low suspense music")
self.assertEqual(result[15]["name"], "Dave")
self.assertIs(result[15]["image"], dave_image)
self.assertEqual(result[16]["name"], "Coffee Shop")
self.assertIs(result[16]["image"], cafe_image)
self.assertIsNone(result[17])
self.assertIn("<Picture 1> Dave", result[24])
self.assertIn("<Picture 2> Coffee Shop", result[24])
self.assertIn("Location context for <Picture 2> Coffee Shop", result[24])
self.assertIn("<Picture 1> Dave", result[25])
self.assertNotIn("<Picture 2> Coffee Shop", result[25])
self.assertIn("<Picture 2> Coffee Shop", result[26])
self.assertIn("Location context for <Picture 2> Coffee Shop", result[26])
self.assertEqual(result[27], "")
self.assertIs(result[34], dave_image)
self.assertIs(result[35], cafe_image)
self.assertIsNone(result[36])
self.assertIn("<Picture 1> Dave", result[43])
self.assertNotIn("<Picture 2> Coffee Shop", result[43])
self.assertIn("<Picture 2> Coffee Shop", result[44])
self.assertIn("Location context for <Picture 2> Coffee Shop", result[44])
self.assertEqual(result[45], "")
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), 52)
self.assertEqual(node.RETURN_NAMES[1:10], tuple(f"ref_image_{i}" for i in range(1, 10)))
self.assertEqual(node.RETURN_NAMES[12:15], ("anchor", "sounds", "bgm"))
self.assertEqual(node.RETURN_NAMES[15:24], tuple(f"original_ref_{i}" for i in range(1, 10)))
self.assertEqual(node.RETURN_NAMES[24], "reference_description")
self.assertEqual(
node.RETURN_NAMES[25:34],
tuple(f"original_ref_description_{i}" for i in range(1, 10)),
)
self.assertEqual(node.RETURN_NAMES[34:43], tuple(f"compiled_ref_image_{i}" for i in range(1, 10)))
self.assertEqual(
node.RETURN_NAMES[43:52],
tuple(f"compiled_ref_description_{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 +734,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()