Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
338150648b | ||
|
|
7de62226f2 | ||
|
|
a51141cb26 | ||
|
|
dd8ef84379 |
@@ -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`, `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`
|
- 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, 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`
|
- `Dumas H3 Shot Length`
|
||||||
- Inputs: `shot_seconds`, `fps`, optional `cap_to_h3_max`
|
- Inputs: `shot_seconds`, `fps`, optional `cap_to_h3_max`
|
||||||
@@ -85,13 +85,15 @@
|
|||||||
|
|
||||||
- `Dumas Character Helper`
|
- `Dumas Character Helper`
|
||||||
- Inputs: `image1`, `image2`, picture IDs, character identity fields, `general`, `wardrobe`
|
- Inputs: `image1`, `image2`, picture IDs, character identity fields, `general`, `wardrobe`
|
||||||
- Outputs: `image1`, `image2`, `reference_prompt`, `wardrobe`
|
- Outputs: `image1`, `image2`, `reference_prompt`, `wardrobe`, `reference1`, `reference2`
|
||||||
- Restores the original general-purpose helper shape: pass two images through unchanged and emit prompt text/wardrobe text for manual wiring.
|
- 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`
|
- `Dumas Location Helper`
|
||||||
- Inputs: `image1`, `image2`, picture IDs, `location_id`, `name`, `alias`, `description`, `general`
|
- Inputs: `image1`, `image2`, picture IDs, `location_id`, `name`, `alias`, `description`, `general`
|
||||||
- Outputs: `image1`, `image2`, `reference_prompt`
|
- Outputs: `image1`, `image2`, `reference_prompt`, `reference1`, `reference2`
|
||||||
- Matching general-purpose helper for environments/locations: pass two images through unchanged and emit location reference prompt text.
|
- 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`
|
||||||
@@ -105,6 +107,11 @@
|
|||||||
- 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, 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`
|
||||||
@@ -261,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 Helper` is the restored two-image/text helper for general H3 workflows, and `Dumas Location Helper` mirrors it for scene/environment references. 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.
|
`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.
|
||||||
|
|
||||||
|
|||||||
+182
-9
@@ -27,6 +27,12 @@ _H3_PLAN_IMAGE_BINDINGS_CAP = 128
|
|||||||
_H3_PLAN_IMAGE_SLOTS = 9
|
_H3_PLAN_IMAGE_SLOTS = 9
|
||||||
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
|
_FOLDER_IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".gif", ".tiff", ".tif")
|
||||||
_ANCHOR_STYLE_H3_NOTE = ""
|
_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_REF_SLOTS = 9
|
||||||
_H3_PROMPT_MAX_CHARS = 7000
|
_H3_PROMPT_MAX_CHARS = 7000
|
||||||
_PICTURE_TAG_RE = re.compile(r"<\s*picture[\s_\-]*(\d+)\s*>", re.I)
|
_PICTURE_TAG_RE = re.compile(r"<\s*picture[\s_\-]*(\d+)\s*>", re.I)
|
||||||
@@ -392,6 +398,51 @@ _SOUNDSCAPE_PRESETS = OrderedDict(
|
|||||||
("custom", ""),
|
("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():
|
def _soundscape_options():
|
||||||
@@ -400,6 +451,14 @@ def _soundscape_options():
|
|||||||
|
|
||||||
def _soundscape_description(soundscape_name):
|
def _soundscape_description(soundscape_name):
|
||||||
return _SOUNDSCAPE_PRESETS.get(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 = {
|
_LOAD_IMAGES_FOLDER_DEFAULT_STATE = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"folder": "",
|
"folder": "",
|
||||||
@@ -420,6 +479,11 @@ def _anchor_style_description(style_name):
|
|||||||
return _ANCHOR_STYLE_PRESETS.get(str(style_name or "").strip().lower(), "")
|
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):
|
def _clean_input_token_value(value):
|
||||||
cleaned = ""
|
cleaned = ""
|
||||||
if value is not None:
|
if value is not None:
|
||||||
@@ -1137,10 +1201,15 @@ def _reference_context(ref, compact_picture_number):
|
|||||||
|
|
||||||
def _subject_count_guard_text(selected_refs):
|
def _subject_count_guard_text(selected_refs):
|
||||||
character_labels = []
|
character_labels = []
|
||||||
|
seen_characters = set()
|
||||||
for picture_number, (_slot, ref) in enumerate(selected_refs or (), 1):
|
for picture_number, (_slot, ref) in enumerate(selected_refs or (), 1):
|
||||||
if ref.get("kind") != "character":
|
if ref.get("kind") != "character":
|
||||||
continue
|
continue
|
||||||
name = _reference_text(ref.get("name")) or _reference_text(ref.get("id"))
|
name = _reference_text(ref.get("name")) or _reference_text(ref.get("id"))
|
||||||
|
key = (_reference_text(ref.get("id")) or name or f"picture-{picture_number}").lower()
|
||||||
|
if key in seen_characters:
|
||||||
|
continue
|
||||||
|
seen_characters.add(key)
|
||||||
label = f"<Picture {picture_number}>"
|
label = f"<Picture {picture_number}>"
|
||||||
if name:
|
if name:
|
||||||
label = f"{label} {name}"
|
label = f"{label} {name}"
|
||||||
@@ -1171,6 +1240,7 @@ def curate_h3_prompt(
|
|||||||
action_prompt,
|
action_prompt,
|
||||||
anchor="",
|
anchor="",
|
||||||
soundscape="",
|
soundscape="",
|
||||||
|
bgm="",
|
||||||
refs=(),
|
refs=(),
|
||||||
anatomy_guard="auto",
|
anatomy_guard="auto",
|
||||||
subject_count_guard="auto",
|
subject_count_guard="auto",
|
||||||
@@ -1193,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, "Subject count guard", _subject_count_guard_text(selected))
|
||||||
_append_prompt_section(prompt_parts, "overall_soundscape", soundscape)
|
_append_prompt_section(prompt_parts, "overall_soundscape", soundscape)
|
||||||
|
_append_prompt_section(prompt_parts, "background_music", bgm)
|
||||||
|
|
||||||
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:
|
||||||
@@ -1960,8 +2031,8 @@ class DumasCharacterHelperNode:
|
|||||||
"IMAGE sockets plus simple identity fields, while passing both images "
|
"IMAGE sockets plus simple identity fields, while passing both images "
|
||||||
"through unchanged."
|
"through unchanged."
|
||||||
)
|
)
|
||||||
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", "STRING")
|
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", "STRING", _REFERENCE_TYPE, _REFERENCE_TYPE)
|
||||||
RETURN_NAMES = ("image1", "image2", "reference_prompt", "wardrobe")
|
RETURN_NAMES = ("image1", "image2", "reference_prompt", "wardrobe", "reference1", "reference2")
|
||||||
FUNCTION = "build_character_text"
|
FUNCTION = "build_character_text"
|
||||||
CATEGORY = "Dumas/MiniMax"
|
CATEGORY = "Dumas/MiniMax"
|
||||||
|
|
||||||
@@ -2122,7 +2193,36 @@ class DumasCharacterHelperNode:
|
|||||||
name,
|
name,
|
||||||
alias,
|
alias,
|
||||||
)
|
)
|
||||||
return (image1, image2, text, wardrobe_text)
|
facts = {
|
||||||
|
"gender": _normalize_free_text(gender),
|
||||||
|
"age": str(_parse_positive_int(age) or ""),
|
||||||
|
"nationality": _normalize_free_text(nationality),
|
||||||
|
"occupation": _normalize_free_text(occupation),
|
||||||
|
"height_feet": str(height_feet or "").strip(),
|
||||||
|
"height_inches": str(height_inches or "").strip(),
|
||||||
|
"accent": _normalize_free_text(accent),
|
||||||
|
}
|
||||||
|
common = {
|
||||||
|
"kind": "character",
|
||||||
|
"explicit_id": character_id,
|
||||||
|
"name": name,
|
||||||
|
"aliases": alias,
|
||||||
|
"description": general,
|
||||||
|
"wardrobe": wardrobe,
|
||||||
|
"general": general,
|
||||||
|
"facts": facts,
|
||||||
|
}
|
||||||
|
reference1 = make_reference(
|
||||||
|
image=image1,
|
||||||
|
summary="Primary full-body character reference.",
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
reference2 = make_reference(
|
||||||
|
image=image2,
|
||||||
|
summary="Secondary facial character reference.",
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
return (image1, image2, text, wardrobe_text, reference1, reference2)
|
||||||
|
|
||||||
|
|
||||||
class DumasLocationHelperNode:
|
class DumasLocationHelperNode:
|
||||||
@@ -2130,8 +2230,8 @@ class DumasLocationHelperNode:
|
|||||||
"Build a general location reference prompt from two IMAGE sockets plus "
|
"Build a general location reference prompt from two IMAGE sockets plus "
|
||||||
"simple environment fields, while passing both images through unchanged."
|
"simple environment fields, while passing both images through unchanged."
|
||||||
)
|
)
|
||||||
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING")
|
RETURN_TYPES = ("IMAGE", "IMAGE", "STRING", _REFERENCE_TYPE, _REFERENCE_TYPE)
|
||||||
RETURN_NAMES = ("image1", "image2", "reference_prompt")
|
RETURN_NAMES = ("image1", "image2", "reference_prompt", "reference1", "reference2")
|
||||||
FUNCTION = "build_location_text"
|
FUNCTION = "build_location_text"
|
||||||
CATEGORY = "Dumas/MiniMax"
|
CATEGORY = "Dumas/MiniMax"
|
||||||
|
|
||||||
@@ -2219,7 +2319,26 @@ class DumasLocationHelperNode:
|
|||||||
description,
|
description,
|
||||||
general,
|
general,
|
||||||
)
|
)
|
||||||
return (image1, image2, text)
|
common = {
|
||||||
|
"kind": "location",
|
||||||
|
"explicit_id": location_id,
|
||||||
|
"name": name,
|
||||||
|
"aliases": alias,
|
||||||
|
"description": description,
|
||||||
|
"general": general,
|
||||||
|
"facts": {},
|
||||||
|
}
|
||||||
|
reference1 = make_reference(
|
||||||
|
image=image1,
|
||||||
|
summary="Primary location reference.",
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
reference2 = make_reference(
|
||||||
|
image=image2,
|
||||||
|
summary="Secondary location reference.",
|
||||||
|
**common,
|
||||||
|
)
|
||||||
|
return (image1, image2, text, reference1, reference2)
|
||||||
|
|
||||||
|
|
||||||
class DumasCharacterReferenceNode:
|
class DumasCharacterReferenceNode:
|
||||||
@@ -2497,10 +2616,53 @@ class DumasSoundscapeHelperNode:
|
|||||||
return (text,)
|
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:
|
class DumasH3PromptCuratorNode:
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
"Curate one MiniMax H3 prompt from an action textbox, anchor text, "
|
"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> "
|
"compacted so only mentioned names, aliases, or explicit <Picture N>/<refN> "
|
||||||
"tags are sent onward."
|
"tags are sent onward."
|
||||||
)
|
)
|
||||||
@@ -2539,6 +2701,13 @@ class DumasH3PromptCuratorNode:
|
|||||||
"tooltip": "Optional soundscape text, usually from Dumas Soundscape Helper.",
|
"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):
|
for slot in range(1, _H3_PROMPT_REF_SLOTS + 1):
|
||||||
optional[f"ref_{slot}"] = (
|
optional[f"ref_{slot}"] = (
|
||||||
@@ -2594,6 +2763,7 @@ class DumasH3PromptCuratorNode:
|
|||||||
subject_count_guard,
|
subject_count_guard,
|
||||||
anchor="",
|
anchor="",
|
||||||
soundscape="",
|
soundscape="",
|
||||||
|
bgm="",
|
||||||
ref_1=None,
|
ref_1=None,
|
||||||
ref_2=None,
|
ref_2=None,
|
||||||
ref_3=None,
|
ref_3=None,
|
||||||
@@ -2608,6 +2778,7 @@ class DumasH3PromptCuratorNode:
|
|||||||
action_prompt,
|
action_prompt,
|
||||||
anchor=anchor,
|
anchor=anchor,
|
||||||
soundscape=soundscape,
|
soundscape=soundscape,
|
||||||
|
bgm=bgm,
|
||||||
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,
|
subject_count_guard=subject_count_guard,
|
||||||
@@ -2651,9 +2822,9 @@ class DumasAnchorStyleNode:
|
|||||||
}
|
}
|
||||||
|
|
||||||
def build_anchor(self, anchor_style, style_description):
|
def build_anchor(self, anchor_style, style_description):
|
||||||
text = str(style_description or "").strip()
|
text = _clean_anchor_style_text(style_description)
|
||||||
if not text:
|
if not text:
|
||||||
text = _anchor_style_description(anchor_style)
|
text = _clean_anchor_style_text(_anchor_style_description(anchor_style))
|
||||||
return (text,)
|
return (text,)
|
||||||
|
|
||||||
|
|
||||||
@@ -2666,6 +2837,7 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"DumasCharacterReference": DumasCharacterReferenceNode,
|
"DumasCharacterReference": DumasCharacterReferenceNode,
|
||||||
"DumasLocationReference": DumasLocationReferenceNode,
|
"DumasLocationReference": DumasLocationReferenceNode,
|
||||||
"DumasSoundscapeHelper": DumasSoundscapeHelperNode,
|
"DumasSoundscapeHelper": DumasSoundscapeHelperNode,
|
||||||
|
"DumasBackgroundMusicHelper": DumasBackgroundMusicHelperNode,
|
||||||
"DumasH3PromptCurator": DumasH3PromptCuratorNode,
|
"DumasH3PromptCurator": DumasH3PromptCuratorNode,
|
||||||
"DumasAnchorStyle": DumasAnchorStyleNode,
|
"DumasAnchorStyle": DumasAnchorStyleNode,
|
||||||
"DumasCharacterHelper": DumasCharacterHelperNode,
|
"DumasCharacterHelper": DumasCharacterHelperNode,
|
||||||
@@ -2682,6 +2854,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"DumasCharacterReference": "Dumas Character Reference",
|
"DumasCharacterReference": "Dumas Character Reference",
|
||||||
"DumasLocationReference": "Dumas Location Reference",
|
"DumasLocationReference": "Dumas Location Reference",
|
||||||
"DumasSoundscapeHelper": "Dumas Soundscape Helper",
|
"DumasSoundscapeHelper": "Dumas Soundscape Helper",
|
||||||
|
"DumasBackgroundMusicHelper": "Dumas Background Music Helper",
|
||||||
"DumasH3PromptCurator": "Dumas H3 Prompt Curator",
|
"DumasH3PromptCurator": "Dumas H3 Prompt Curator",
|
||||||
"DumasAnchorStyle": "Dumas Anchor Style",
|
"DumasAnchorStyle": "Dumas Anchor Style",
|
||||||
"DumasCharacterHelper": "Dumas Character Helper",
|
"DumasCharacterHelper": "Dumas Character Helper",
|
||||||
|
|||||||
@@ -386,6 +386,17 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
self.assertIn("Dave is also known as The Locksmith", result[2])
|
self.assertIn("Dave is also known as The Locksmith", result[2])
|
||||||
self.assertIn("is 41 years old", 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.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):
|
def test_location_helper_matches_character_helper_shape_without_wardrobe(self):
|
||||||
node = self.image_nodes.DumasLocationHelperNode()
|
node = self.image_nodes.DumasLocationHelperNode()
|
||||||
@@ -410,7 +421,17 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
self.assertIn("Coffee Shop is also known as Cafe Interior", 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("Warm tungsten lighting, narrow counter, rainy front window.", result[2])
|
||||||
self.assertIn("Evening ambience, cramped but cozy.", result[2])
|
self.assertIn("Evening ambience, cramped but cozy.", result[2])
|
||||||
self.assertEqual(len(result), 3)
|
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):
|
def test_soundscape_helper_defaults_to_selected_preset_description(self):
|
||||||
node = self.image_nodes.DumasSoundscapeHelperNode()
|
node = self.image_nodes.DumasSoundscapeHelperNode()
|
||||||
@@ -419,6 +440,13 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(result[0], "steady rain, wet pavement, distant traffic hum")
|
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):
|
def test_h3_prompt_curator_compacts_named_references(self):
|
||||||
node = self.image_nodes.DumasH3PromptCuratorNode()
|
node = self.image_nodes.DumasH3PromptCuratorNode()
|
||||||
dave_image = FakeTensorBatch()
|
dave_image = FakeTensorBatch()
|
||||||
@@ -451,6 +479,7 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
subject_count_guard="auto",
|
subject_count_guard="auto",
|
||||||
anchor="grounded handheld thriller",
|
anchor="grounded handheld thriller",
|
||||||
soundscape="steady rain",
|
soundscape="steady rain",
|
||||||
|
bgm="low suspense music",
|
||||||
ref_1=dave,
|
ref_1=dave,
|
||||||
ref_2=van,
|
ref_2=van,
|
||||||
ref_3=cafe,
|
ref_3=cafe,
|
||||||
@@ -462,6 +491,8 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
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("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.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)
|
||||||
@@ -508,6 +539,81 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
self.assertIn("Only include the people explicitly described", result[0])
|
self.assertIn("Only include the people explicitly described", result[0])
|
||||||
self.assertEqual(result[10], 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):
|
def test_h3_prompt_curator_defaults_anatomy_guard_to_on(self):
|
||||||
required = self.image_nodes.DumasH3PromptCuratorNode.INPUT_TYPES()["required"]
|
required = self.image_nodes.DumasH3PromptCuratorNode.INPUT_TYPES()["required"]
|
||||||
|
|
||||||
@@ -520,10 +626,12 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
self.assertIs(mappings["DumasCharacterHelper"], self.image_nodes.DumasCharacterHelperNode)
|
self.assertIs(mappings["DumasCharacterHelper"], self.image_nodes.DumasCharacterHelperNode)
|
||||||
self.assertIs(mappings["DumasLocationHelper"], self.image_nodes.DumasLocationHelperNode)
|
self.assertIs(mappings["DumasLocationHelper"], self.image_nodes.DumasLocationHelperNode)
|
||||||
self.assertIs(mappings["DumasSoundscapeHelper"], self.image_nodes.DumasSoundscapeHelperNode)
|
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.assertIs(mappings["DumasH3PromptCurator"], self.image_nodes.DumasH3PromptCuratorNode)
|
||||||
self.assertEqual(display["DumasCharacterHelper"], "Dumas Character Helper")
|
self.assertEqual(display["DumasCharacterHelper"], "Dumas Character Helper")
|
||||||
self.assertEqual(display["DumasLocationHelper"], "Dumas Location Helper")
|
self.assertEqual(display["DumasLocationHelper"], "Dumas Location Helper")
|
||||||
self.assertEqual(display["DumasSoundscapeHelper"], "Dumas Soundscape Helper")
|
self.assertEqual(display["DumasSoundscapeHelper"], "Dumas Soundscape Helper")
|
||||||
|
self.assertEqual(display["DumasBackgroundMusicHelper"], "Dumas Background Music Helper")
|
||||||
self.assertEqual(display["DumasH3PromptCurator"], "Dumas H3 Prompt Curator")
|
self.assertEqual(display["DumasH3PromptCurator"], "Dumas H3 Prompt Curator")
|
||||||
|
|
||||||
def test_h3_prompt_curator_uses_documented_reference_limits(self):
|
def test_h3_prompt_curator_uses_documented_reference_limits(self):
|
||||||
@@ -592,6 +700,19 @@ class DumasImageNodeTests(unittest.TestCase):
|
|||||||
self.assertIn("real time", result[0])
|
self.assertIn("real time", result[0])
|
||||||
self.assertNotIn("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()
|
||||||
custom = "Lo-fi pirate broadcast with smeared highlights and anxious zoom corrections."
|
custom = "Lo-fi pirate broadcast with smeared highlights and anxious zoom corrections."
|
||||||
|
|||||||
Reference in New Issue
Block a user