Compare commits

..
2 Commits
Author SHA1 Message Date
chris.dumas 338150648b Add BGM helper for H3 prompt curator 2026-09-07 08:13:45 +00:00
chris.dumas 7de62226f2 Strip legacy anchor style note 2026-09-07 08:01:22 +00:00
3 changed files with 156 additions and 6 deletions
+8 -3
View File
@@ -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, `<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, 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 `<Picture N>` 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 `<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.
+123 -3
View File
@@ -27,6 +27,12 @@ _H3_PLAN_IMAGE_BINDINGS_CAP = 128
_H3_PLAN_IMAGE_SLOTS = 9
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
_ANCHOR_STYLE_H3_NOTE = ""
_ANCHOR_STYLE_LEGACY_NOTE_RE = re.compile(
r"\s*Keep this anchor focused on persistent camera language, lighting, "
r"texture, environment treatment, and tone; do not name characters or "
r"describe one-off actions\.?",
re.I,
)
_H3_PROMPT_REF_SLOTS = 9
_H3_PROMPT_MAX_CHARS = 7000
_PICTURE_TAG_RE = re.compile(r"<\s*picture[\s_\-]*(\d+)\s*>", re.I)
@@ -392,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():
@@ -400,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": "",
@@ -420,6 +479,11 @@ def _anchor_style_description(style_name):
return _ANCHOR_STYLE_PRESETS.get(str(style_name or "").strip().lower(), "")
def _clean_anchor_style_text(text):
cleaned = _ANCHOR_STYLE_LEGACY_NOTE_RE.sub("", str(text or ""))
return re.sub(r"[ \t]{2,}", " ", cleaned).strip()
def _clean_input_token_value(value):
cleaned = ""
if value is not None:
@@ -1176,6 +1240,7 @@ def curate_h3_prompt(
action_prompt,
anchor="",
soundscape="",
bgm="",
refs=(),
anatomy_guard="auto",
subject_count_guard="auto",
@@ -1198,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:
@@ -2550,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 <Picture N>/<refN> "
"tags are sent onward."
)
@@ -2592,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}"] = (
@@ -2647,6 +2763,7 @@ class DumasH3PromptCuratorNode:
subject_count_guard,
anchor="",
soundscape="",
bgm="",
ref_1=None,
ref_2=None,
ref_3=None,
@@ -2661,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,
@@ -2704,9 +2822,9 @@ class DumasAnchorStyleNode:
}
def build_anchor(self, anchor_style, style_description):
text = str(style_description or "").strip()
text = _clean_anchor_style_text(style_description)
if not text:
text = _anchor_style_description(anchor_style)
text = _clean_anchor_style_text(_anchor_style_description(anchor_style))
return (text,)
@@ -2719,6 +2837,7 @@ NODE_CLASS_MAPPINGS = {
"DumasCharacterReference": DumasCharacterReferenceNode,
"DumasLocationReference": DumasLocationReferenceNode,
"DumasSoundscapeHelper": DumasSoundscapeHelperNode,
"DumasBackgroundMusicHelper": DumasBackgroundMusicHelperNode,
"DumasH3PromptCurator": DumasH3PromptCuratorNode,
"DumasAnchorStyle": DumasAnchorStyleNode,
"DumasCharacterHelper": DumasCharacterHelperNode,
@@ -2735,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",
+25
View File
@@ -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: <Picture 1> 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):
@@ -688,6 +700,19 @@ class DumasImageNodeTests(unittest.TestCase):
self.assertIn("real time", 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):
node = self.image_nodes.DumasAnchorStyleNode()
custom = "Lo-fi pirate broadcast with smeared highlights and anxious zoom corrections."