diff --git a/README.md b/README.md index 4312ef5..482286d 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,11 @@ - 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 `ref_1` through `ref_9` + - 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, ``, or ``. 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, and `overall_soundscape:` text while respecting MiniMax H3's reference-generation shape: one prompt plus up to nine reference images. + - 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` - Inputs: `shot_seconds`, `fps`, optional `cap_to_h3_max` @@ -107,6 +107,11 @@ - 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` - Input: `json_string` - Output: parsed `JSON` @@ -263,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 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, assigns the final `` numbering, and outputs only the compacted images the prompt actually mentions. +`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 `` 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. diff --git a/dumas_image_nodes.py b/dumas_image_nodes.py index 5cb67be..421cdf3 100644 --- a/dumas_image_nodes.py +++ b/dumas_image_nodes.py @@ -398,6 +398,51 @@ _SOUNDSCAPE_PRESETS = OrderedDict( ("custom", ""), ] ) +_BGM_PRESETS = OrderedDict( + [ + ( + "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", ""), + ] +) def _soundscape_options(): @@ -406,6 +451,14 @@ def _soundscape_options(): def _soundscape_description(soundscape_name): return _SOUNDSCAPE_PRESETS.get(soundscape_name, "") + + +def _bgm_options(): + return list(_BGM_PRESETS.keys()) + + +def _bgm_description(bgm_name): + return _BGM_PRESETS.get(bgm_name, "") _LOAD_IMAGES_FOLDER_DEFAULT_STATE = { "version": 1, "folder": "", @@ -1187,6 +1240,7 @@ def curate_h3_prompt( action_prompt, anchor="", soundscape="", + bgm="", refs=(), anatomy_guard="auto", subject_count_guard="auto", @@ -1209,6 +1263,7 @@ def curate_h3_prompt( ): _append_prompt_section(prompt_parts, "Subject count guard", _subject_count_guard_text(selected)) _append_prompt_section(prompt_parts, "overall_soundscape", soundscape) + _append_prompt_section(prompt_parts, "background_music", bgm) prompt = "\n\n".join(prompt_parts).strip() if len(prompt) > _H3_PROMPT_MAX_CHARS: @@ -2561,10 +2616,53 @@ class DumasSoundscapeHelperNode: return (text,) +class DumasBackgroundMusicHelperNode: + DESCRIPTION = ( + "Choose a background music preset, auto-fill its editable description, " + "and pass the final BGM text downstream for MiniMax H3 prompts." + ) + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("bgm",) + FUNCTION = "build_bgm" + CATEGORY = "Dumas/MiniMax" + + @classmethod + def INPUT_TYPES(cls): + default_bgm = "none" + return { + "required": { + "bgm": ( + _bgm_options(), + { + "default": default_bgm, + "tooltip": "Preset title used to seed the editable background music description.", + }, + ), + "bgm_description": ( + "STRING", + { + "default": _bgm_description(default_bgm), + "multiline": True, + "tooltip": ( + "Editable background music description. Whatever text is here " + "is what the node outputs to the bgm socket." + ), + }, + ), + } + } + + def build_bgm(self, bgm, bgm_description): + text = str(bgm_description or "").strip() + if not text: + text = _bgm_description(bgm) + return (text,) + + class DumasH3PromptCuratorNode: DESCRIPTION = ( "Curate one MiniMax H3 prompt from an action textbox, anchor text, " - "soundscape text, and up to nine structured references. References are " + "soundscape/BGM text, and up to nine structured references. References are " "compacted so only mentioned names, aliases, or explicit / " "tags are sent onward." ) @@ -2603,6 +2701,13 @@ class DumasH3PromptCuratorNode: "tooltip": "Optional soundscape text, usually from Dumas Soundscape Helper.", }, ), + "bgm": ( + "STRING", + { + "forceInput": True, + "tooltip": "Optional background music text, usually from Dumas Background Music Helper.", + }, + ), } for slot in range(1, _H3_PROMPT_REF_SLOTS + 1): optional[f"ref_{slot}"] = ( @@ -2658,6 +2763,7 @@ class DumasH3PromptCuratorNode: subject_count_guard, anchor="", soundscape="", + bgm="", ref_1=None, ref_2=None, ref_3=None, @@ -2672,6 +2778,7 @@ class DumasH3PromptCuratorNode: action_prompt, anchor=anchor, soundscape=soundscape, + bgm=bgm, refs=(ref_1, ref_2, ref_3, ref_4, ref_5, ref_6, ref_7, ref_8, ref_9), anatomy_guard=anatomy_guard, subject_count_guard=subject_count_guard, @@ -2730,6 +2837,7 @@ NODE_CLASS_MAPPINGS = { "DumasCharacterReference": DumasCharacterReferenceNode, "DumasLocationReference": DumasLocationReferenceNode, "DumasSoundscapeHelper": DumasSoundscapeHelperNode, + "DumasBackgroundMusicHelper": DumasBackgroundMusicHelperNode, "DumasH3PromptCurator": DumasH3PromptCuratorNode, "DumasAnchorStyle": DumasAnchorStyleNode, "DumasCharacterHelper": DumasCharacterHelperNode, @@ -2746,6 +2854,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "DumasCharacterReference": "Dumas Character Reference", "DumasLocationReference": "Dumas Location Reference", "DumasSoundscapeHelper": "Dumas Soundscape Helper", + "DumasBackgroundMusicHelper": "Dumas Background Music Helper", "DumasH3PromptCurator": "Dumas H3 Prompt Curator", "DumasAnchorStyle": "Dumas Anchor Style", "DumasCharacterHelper": "Dumas Character Helper", diff --git a/tests/test_dumas_image_nodes.py b/tests/test_dumas_image_nodes.py index 56b058a..6503855 100644 --- a/tests/test_dumas_image_nodes.py +++ b/tests/test_dumas_image_nodes.py @@ -440,6 +440,13 @@ class DumasImageNodeTests(unittest.TestCase): 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() @@ -472,6 +479,7 @@ class DumasImageNodeTests(unittest.TestCase): subject_count_guard="auto", anchor="grounded handheld thriller", soundscape="steady rain", + bgm="low suspense music", ref_1=dave, ref_2=van, ref_3=cafe, @@ -483,6 +491,8 @@ class DumasImageNodeTests(unittest.TestCase): 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: Dave", prompt) self.assertIs(result[1], dave_image) self.assertIs(result[2], cafe_image) @@ -616,10 +626,12 @@ class DumasImageNodeTests(unittest.TestCase): 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):