Compare commits

..
6 Commits
5 changed files with 182 additions and 20 deletions
+5 -4
View File
@@ -58,9 +58,10 @@
- `Dumas H3 Prompt Curator` - `Dumas H3 Prompt Curator`
- Inputs: `action_prompt`, `anatomy_guard`, `subject_count_guard`, optional `anchor`, optional `soundscape`, optional `bgm`, 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` - Outputs: `prompt`, `ref_image_1` through `ref_image_9`, `reference_count`, `debug`, `anchor`, `sounds`, `bgm`, `original_ref_1` through `original_ref_9`
- Builds one standalone MiniMax H3 prompt from your final action text plus structured character/location references. - 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. - 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, and each selected original reference image in compacted order.
- 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. - 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`
@@ -100,17 +101,17 @@
- 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` - `Dumas Soundscape Helper`
- Inputs: `soundscape`, `soundscape_description` - Inputs: `soundscape`, `soundscape_description`
- Output: `soundscape` - 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`. - 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` - `Dumas Background Music Helper`
- Inputs: `bgm`, `bgm_description` - Inputs: `bgm`, `bgm_description`
- Output: `bgm` - 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`. - 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`
+41 -8
View File
@@ -1249,12 +1249,19 @@ def curate_h3_prompt(
selected = _selected_prompt_refs(action_prompt, normalized_refs) selected = _selected_prompt_refs(action_prompt, normalized_refs)
picture_map = {slot_number: index for index, (slot_number, _ref) in enumerate(selected, 1)} picture_map = {slot_number: index for index, (slot_number, _ref) in enumerate(selected, 1)}
action = _replace_reference_tags(action_prompt, picture_map) action = _replace_reference_tags(action_prompt, picture_map)
anchor_text = _reference_text(anchor)
soundscape_text = _reference_text(soundscape)
bgm_text = _reference_text(bgm)
reference_description = ""
if selected:
reference_description = " ".join(
_reference_context(ref, picture_number)
for picture_number, (_slot, ref) in enumerate(selected, 1)
)
prompt_parts = [] prompt_parts = []
_append_prompt_section(prompt_parts, "Scene anchor", anchor) _append_prompt_section(prompt_parts, "Scene anchor", anchor_text)
if selected: _append_prompt_section(prompt_parts, "Reference context", reference_description)
contexts = [_reference_context(ref, picture_number) for picture_number, (_slot, ref) in enumerate(selected, 1)]
_append_prompt_section(prompt_parts, "Reference context", " ".join(contexts))
_append_prompt_section(prompt_parts, "Action", action) _append_prompt_section(prompt_parts, "Action", action)
if anatomy_guard == "on" or (anatomy_guard == "auto" and any(ref.get("kind") == "character" for _slot, ref in selected)): if anatomy_guard == "on" or (anatomy_guard == "auto" and any(ref.get("kind") == "character" for _slot, ref in selected)):
_append_prompt_section(prompt_parts, "Anatomy guard", _ANATOMY_GUARD_TEXT) _append_prompt_section(prompt_parts, "Anatomy guard", _ANATOMY_GUARD_TEXT)
@@ -1262,8 +1269,8 @@ def curate_h3_prompt(
subject_count_guard == "auto" and any(ref.get("kind") == "character" for _slot, ref in selected) subject_count_guard == "auto" and any(ref.get("kind") == "character" for _slot, ref in selected)
): ):
_append_prompt_section(prompt_parts, "Subject count guard", _subject_count_guard_text(selected)) _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, "overall_soundscape", soundscape_text)
_append_prompt_section(prompt_parts, "background_music", bgm) _append_prompt_section(prompt_parts, "background_music", bgm_text)
prompt = "\n\n".join(prompt_parts).strip() prompt = "\n\n".join(prompt_parts).strip()
if len(prompt) > _H3_PROMPT_MAX_CHARS: if len(prompt) > _H3_PROMPT_MAX_CHARS:
@@ -1279,7 +1286,16 @@ def curate_h3_prompt(
if selected if selected
else "Selected 0 references." else "Selected 0 references."
) )
return (prompt, *images[:_H3_PROMPT_REF_SLOTS], len(selected), debug) return (
prompt,
*images[:_H3_PROMPT_REF_SLOTS],
len(selected),
debug,
anchor_text,
soundscape_text,
bgm_text,
*images[:_H3_PROMPT_REF_SLOTS],
)
def _parse_positive_int(value): def _parse_positive_int(value):
@@ -2666,7 +2682,12 @@ class DumasH3PromptCuratorNode:
"compacted so only mentioned names, aliases, or explicit <Picture N>/<refN> " "compacted so only mentioned names, aliases, or explicit <Picture N>/<refN> "
"tags are sent onward." "tags are sent onward."
) )
RETURN_TYPES = ("STRING",) + ("IMAGE",) * _H3_PROMPT_REF_SLOTS + ("INT", "STRING") RETURN_TYPES = (
("STRING",)
+ ("IMAGE",) * _H3_PROMPT_REF_SLOTS
+ ("INT", "STRING", "STRING", "STRING", "STRING")
+ ("IMAGE",) * _H3_PROMPT_REF_SLOTS
)
RETURN_NAMES = ( RETURN_NAMES = (
"prompt", "prompt",
"ref_image_1", "ref_image_1",
@@ -2680,6 +2701,18 @@ class DumasH3PromptCuratorNode:
"ref_image_9", "ref_image_9",
"reference_count", "reference_count",
"debug", "debug",
"anchor",
"sounds",
"bgm",
"original_ref_1",
"original_ref_2",
"original_ref_3",
"original_ref_4",
"original_ref_5",
"original_ref_6",
"original_ref_7",
"original_ref_8",
"original_ref_9",
) )
FUNCTION = "curate_prompt" FUNCTION = "curate_prompt"
CATEGORY = "Dumas/MiniMax" CATEGORY = "Dumas/MiniMax"
+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);
+76
View File
@@ -0,0 +1,76 @@
import { app } from "/scripts/app.js";
const NODE_NAME = "DumasH3PromptCurator";
const EXPECTED_OUTPUTS = [
"prompt",
"ref_image_1",
"ref_image_2",
"ref_image_3",
"ref_image_4",
"ref_image_5",
"ref_image_6",
"ref_image_7",
"ref_image_8",
"ref_image_9",
"reference_count",
"debug",
"anchor",
"sounds",
"bgm",
"original_ref_1",
"original_ref_2",
"original_ref_3",
"original_ref_4",
"original_ref_5",
"original_ref_6",
"original_ref_7",
"original_ref_8",
"original_ref_9",
];
const EXPECTED_NAMES = new Set(EXPECTED_OUTPUTS);
function pruneStaleOutputs(node) {
if (!Array.isArray(node.outputs)) return;
const byName = new Map();
for (const output of node.outputs) {
if (!output?.name || !EXPECTED_NAMES.has(output.name) || byName.has(output.name)) continue;
byName.set(output.name, output);
}
const nextOutputs = [];
for (const name of EXPECTED_OUTPUTS) {
const existing = byName.get(name);
if (existing) {
nextOutputs.push(existing);
}
}
if (nextOutputs.length && nextOutputs.length !== node.outputs.length) {
node.outputs = nextOutputs;
node.size = node.computeSize?.() || node.size;
node.setDirtyCanvas?.(true, true);
}
}
app.registerExtension({
name: "Dumas.H3PromptCuratorOutputs",
async beforeRegisterNodeDef(nodeType, nodeData) {
if (nodeData?.name !== NODE_NAME) return;
const originalOnNodeCreated = nodeType.prototype.onNodeCreated;
const originalOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onNodeCreated = function onNodeCreated() {
const created = originalOnNodeCreated?.apply(this, arguments);
pruneStaleOutputs(this);
return created;
};
nodeType.prototype.onConfigure = function onConfigure() {
const configured = originalOnConfigure?.apply(this, arguments);
pruneStaleOutputs(this);
return configured;
};
},
});
+9 -1
View File
@@ -499,6 +499,12 @@ class DumasImageNodeTests(unittest.TestCase):
self.assertIsNone(result[3]) self.assertIsNone(result[3])
self.assertEqual(result[10], 2) self.assertEqual(result[10], 2)
self.assertIn("input 3-><Picture 2> Coffee Shop", result[11]) 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.assertIs(result[15], dave_image)
self.assertIs(result[16], cafe_image)
self.assertIsNone(result[17])
def test_h3_prompt_curator_renumbers_explicit_reference_tags(self): def test_h3_prompt_curator_renumbers_explicit_reference_tags(self):
node = self.image_nodes.DumasH3PromptCuratorNode() node = self.image_nodes.DumasH3PromptCuratorNode()
@@ -636,8 +642,10 @@ class DumasImageNodeTests(unittest.TestCase):
def test_h3_prompt_curator_uses_documented_reference_limits(self): def test_h3_prompt_curator_uses_documented_reference_limits(self):
node = self.image_nodes.DumasH3PromptCuratorNode() node = self.image_nodes.DumasH3PromptCuratorNode()
self.assertEqual(len(node.RETURN_TYPES), 12) self.assertEqual(len(node.RETURN_TYPES), 24)
self.assertEqual(node.RETURN_NAMES[1:10], tuple(f"ref_image_{i}" for i in range(1, 10))) 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)))
def test_normalize_reference_upgrades_generic_summary_with_socket_picture_id(self): def test_normalize_reference_upgrades_generic_summary_with_socket_picture_id(self):
image = FakeTensorBatch() image = FakeTensorBatch()