Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d20b257134 |
@@ -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.
|
- 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`
|
- `Dumas H3 Prompt Curator`
|
||||||
- Inputs: `action_prompt`, `anatomy_guard`, optional `anchor`, optional `soundscape`, optional `ref_1` through `ref_9`
|
- Inputs: `action_prompt`, `anatomy_guard`, `subject_count_guard`, optional `anchor`, optional `soundscape`, 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`
|
||||||
- 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.
|
||||||
- Adds curated reference context, optional anatomy 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, and `overall_soundscape:` 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`
|
||||||
|
|||||||
+55
-2
@@ -36,6 +36,10 @@ _ANATOMY_GUARD_TEXT = (
|
|||||||
"and two legs with two feet. Limbs stay attached to the correct body and move "
|
"and two legs with two feet. Limbs stay attached to the correct body and move "
|
||||||
"only with the person they belong to."
|
"only with the person they belong to."
|
||||||
)
|
)
|
||||||
|
_SUBJECT_COUNT_FALLBACK_TEXT = (
|
||||||
|
"Only include the people explicitly described in the action. Do not invent "
|
||||||
|
"extra people, doubles, duplicate bodies, background performers, or extra faces."
|
||||||
|
)
|
||||||
_ANCHOR_STYLE_PRESETS = OrderedDict(
|
_ANCHOR_STYLE_PRESETS = OrderedDict(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
@@ -1131,13 +1135,46 @@ def _reference_context(ref, compact_picture_number):
|
|||||||
return " ".join(part for part in parts if part).strip()
|
return " ".join(part for part in parts if part).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _subject_count_guard_text(selected_refs):
|
||||||
|
character_labels = []
|
||||||
|
for picture_number, (_slot, ref) in enumerate(selected_refs or (), 1):
|
||||||
|
if ref.get("kind") != "character":
|
||||||
|
continue
|
||||||
|
name = _reference_text(ref.get("name")) or _reference_text(ref.get("id"))
|
||||||
|
label = f"<Picture {picture_number}>"
|
||||||
|
if name:
|
||||||
|
label = f"{label} {name}"
|
||||||
|
character_labels.append(label)
|
||||||
|
if not character_labels:
|
||||||
|
return _SUBJECT_COUNT_FALLBACK_TEXT
|
||||||
|
if len(character_labels) == 1:
|
||||||
|
return (
|
||||||
|
f"The shot contains exactly one named character: {character_labels[0]}. "
|
||||||
|
"Do not create any extra people, doubles, duplicate bodies, background "
|
||||||
|
"performers, or extra faces."
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f"The shot contains exactly {len(character_labels)} named characters: "
|
||||||
|
+ ", ".join(character_labels)
|
||||||
|
+ ". Do not create any extra people, doubles, duplicate bodies, background "
|
||||||
|
"performers, or extra faces."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _append_prompt_section(parts, label, text):
|
def _append_prompt_section(parts, label, text):
|
||||||
clean = _reference_text(text)
|
clean = _reference_text(text)
|
||||||
if clean:
|
if clean:
|
||||||
parts.append(f"{label}: {clean}")
|
parts.append(f"{label}: {clean}")
|
||||||
|
|
||||||
|
|
||||||
def curate_h3_prompt(action_prompt, anchor="", soundscape="", refs=(), anatomy_guard="auto"):
|
def curate_h3_prompt(
|
||||||
|
action_prompt,
|
||||||
|
anchor="",
|
||||||
|
soundscape="",
|
||||||
|
refs=(),
|
||||||
|
anatomy_guard="auto",
|
||||||
|
subject_count_guard="auto",
|
||||||
|
):
|
||||||
normalized_refs = _normalize_prompt_refs(refs)
|
normalized_refs = _normalize_prompt_refs(refs)
|
||||||
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)}
|
||||||
@@ -1151,6 +1188,10 @@ def curate_h3_prompt(action_prompt, anchor="", soundscape="", refs=(), anatomy_g
|
|||||||
_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)
|
||||||
|
if subject_count_guard == "on" or (
|
||||||
|
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, "overall_soundscape", soundscape)
|
_append_prompt_section(prompt_parts, "overall_soundscape", soundscape)
|
||||||
|
|
||||||
prompt = "\n\n".join(prompt_parts).strip()
|
prompt = "\n\n".join(prompt_parts).strip()
|
||||||
@@ -2526,12 +2567,22 @@ class DumasH3PromptCuratorNode:
|
|||||||
"anatomy_guard": (
|
"anatomy_guard": (
|
||||||
["auto", "on", "off"],
|
["auto", "on", "off"],
|
||||||
{
|
{
|
||||||
"default": "auto",
|
"default": "on",
|
||||||
"tooltip": (
|
"tooltip": (
|
||||||
"Add the anatomy guard. Auto adds it when a character reference is used."
|
"Add the anatomy guard. Auto adds it when a character reference is used."
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
"subject_count_guard": (
|
||||||
|
["auto", "on", "off"],
|
||||||
|
{
|
||||||
|
"default": "auto",
|
||||||
|
"tooltip": (
|
||||||
|
"Add a guard against extra people, duplicate bodies, or extra faces. "
|
||||||
|
"Auto adds it when a character reference is used."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"optional": optional,
|
"optional": optional,
|
||||||
}
|
}
|
||||||
@@ -2540,6 +2591,7 @@ class DumasH3PromptCuratorNode:
|
|||||||
self,
|
self,
|
||||||
action_prompt,
|
action_prompt,
|
||||||
anatomy_guard,
|
anatomy_guard,
|
||||||
|
subject_count_guard,
|
||||||
anchor="",
|
anchor="",
|
||||||
soundscape="",
|
soundscape="",
|
||||||
ref_1=None,
|
ref_1=None,
|
||||||
@@ -2558,6 +2610,7 @@ class DumasH3PromptCuratorNode:
|
|||||||
soundscape=soundscape,
|
soundscape=soundscape,
|
||||||
refs=(ref_1, ref_2, ref_3, ref_4, ref_5, ref_6, ref_7, ref_8, ref_9),
|
refs=(ref_1, ref_2, ref_3, ref_4, ref_5, ref_6, ref_7, ref_8, ref_9),
|
||||||
anatomy_guard=anatomy_guard,
|
anatomy_guard=anatomy_guard,
|
||||||
|
subject_count_guard=subject_count_guard,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -448,6 +448,7 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
result = node.curate_prompt(
|
result = node.curate_prompt(
|
||||||
action_prompt="Dave runs from the Coffee Shop into the rain.",
|
action_prompt="Dave runs from the Coffee Shop into the rain.",
|
||||||
anatomy_guard="auto",
|
anatomy_guard="auto",
|
||||||
|
subject_count_guard="auto",
|
||||||
anchor="grounded handheld thriller",
|
anchor="grounded handheld thriller",
|
||||||
soundscape="steady rain",
|
soundscape="steady rain",
|
||||||
ref_1=dave,
|
ref_1=dave,
|
||||||
@@ -460,6 +461,8 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
self.assertIn("<Picture 2> Coffee Shop", prompt)
|
self.assertIn("<Picture 2> Coffee Shop", prompt)
|
||||||
self.assertIn("Action: Dave runs from the Coffee Shop into the rain.", prompt)
|
self.assertIn("Action: Dave runs from the Coffee Shop into the rain.", prompt)
|
||||||
self.assertIn("Anatomy guard:", prompt)
|
self.assertIn("Anatomy guard:", prompt)
|
||||||
|
self.assertIn("Subject count guard:", prompt)
|
||||||
|
self.assertIn("exactly one named character: <Picture 1> Dave", prompt)
|
||||||
self.assertIs(result[1], dave_image)
|
self.assertIs(result[1], dave_image)
|
||||||
self.assertIs(result[2], cafe_image)
|
self.assertIs(result[2], cafe_image)
|
||||||
self.assertIsNone(result[3])
|
self.assertIsNone(result[3])
|
||||||
@@ -478,6 +481,7 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
result = node.curate_prompt(
|
result = node.curate_prompt(
|
||||||
action_prompt="<Picture 1> Maya crosses to <ref3> as the wind rises.",
|
action_prompt="<Picture 1> Maya crosses to <ref3> as the wind rises.",
|
||||||
anatomy_guard="off",
|
anatomy_guard="off",
|
||||||
|
subject_count_guard="off",
|
||||||
ref_1=first,
|
ref_1=first,
|
||||||
ref_2=second,
|
ref_2=second,
|
||||||
ref_3=third,
|
ref_3=third,
|
||||||
@@ -491,6 +495,24 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
self.assertIsNone(result[3])
|
self.assertIsNone(result[3])
|
||||||
self.assertEqual(result[10], 2)
|
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_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):
|
def test_helper_node_mappings_use_general_purpose_helpers(self):
|
||||||
mappings = self.image_nodes.NODE_CLASS_MAPPINGS
|
mappings = self.image_nodes.NODE_CLASS_MAPPINGS
|
||||||
display = self.image_nodes.NODE_DISPLAY_NAME_MAPPINGS
|
display = self.image_nodes.NODE_DISPLAY_NAME_MAPPINGS
|
||||||
|
|||||||
Reference in New Issue
Block a user