Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
338150648b | ||
|
|
7de62226f2 | ||
|
|
a51141cb26 | ||
|
|
dd8ef84379 | ||
|
|
d20b257134 | ||
|
|
173205ca51 | ||
|
|
98833ba99d | ||
|
|
a7f7225b3a | ||
|
|
5dc5c2ce8b | ||
|
|
83645811f4 | ||
|
|
216bc05762 | ||
|
|
49e099ee7f | ||
|
|
32a16645b5 | ||
|
|
7bb0b0abea | ||
|
|
0b189dcf7a | ||
|
|
e8599055a1 | ||
|
|
618a48e4d9 | ||
|
|
cbabcf8208 | ||
|
|
fbfbe4ef5a | ||
|
|
5b52793fad | ||
|
|
7e45c81589 | ||
|
|
027865b9ca | ||
|
|
9f0c546681 | ||
|
|
30ae81b86b | ||
|
|
179fa0778e | ||
|
|
a34c9eb4ff | ||
|
|
782a7d658b | ||
|
|
04a61af874 | ||
|
|
a0d80bcc47 | ||
|
|
fbb5f799cd | ||
|
|
9dc3c405a6 | ||
|
|
21ae4d62e0 | ||
|
|
836a6eba33 | ||
|
|
3b63d33ec7 | ||
|
|
c62921c3e8 | ||
|
|
3ab6342ce5 | ||
|
|
51e03a39b7 | ||
|
|
f7b94ccaef | ||
|
|
f6120a8500 | ||
|
|
0e119646ab | ||
|
|
c89570eae9 | ||
|
|
36d9f4369c | ||
|
|
2cb3c694f0 | ||
|
|
5c12cd18a3 | ||
|
|
c1d937e0e2 | ||
|
|
306816534b | ||
|
|
e098e44bf1 | ||
|
|
769257513b | ||
|
|
ab6ba86d76 | ||
|
|
ecd8ca6336 | ||
|
|
76b88b0c59 | ||
|
|
62fbbdd98e | ||
|
|
85e15c910a | ||
|
|
30400038c2 | ||
|
|
c8ddedbd57 | ||
|
|
6684c5f2e0 | ||
|
|
08630964d5 | ||
|
|
271e7de300 | ||
|
|
130b268d1e | ||
|
|
a2dabaab7e | ||
|
|
b7c3bba199 | ||
|
|
35ec12159e | ||
|
|
58b1c77c19 | ||
|
|
4a0f5cfcd4 | ||
|
|
6a7bec5d03 | ||
|
|
dfead66b9e | ||
|
|
00985b73b3 | ||
|
|
ffae1d87a5 | ||
|
|
74aca203ab | ||
|
|
e08281c12b | ||
|
|
faaeb374e0 | ||
|
|
0bac83a689 | ||
|
|
08365c7df5 | ||
|
|
9cbab72e86 | ||
|
|
e29d01ad24 | ||
|
|
ddd7dbb5af | ||
|
|
1ad68ee9e4 | ||
|
|
e64ad0fae4 | ||
|
|
deb6f1d033 | ||
|
|
72d4d74dd3 | ||
|
|
c42cf436c5 | ||
|
|
eeca789c27 |
File diff suppressed because it is too large
Load Diff
@@ -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.
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
File diff suppressed because it is too large
Load Diff
+50
-29
@@ -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."}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user