Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d68d04254f | ||
|
|
22398c5213 | ||
|
|
1a4cfbfd7e | ||
|
|
eac4e73b47 | ||
|
|
e973a0d785 | ||
|
|
388de1d837 | ||
|
|
338150648b | ||
|
|
7de62226f2 | ||
|
|
a51141cb26 | ||
|
|
dd8ef84379 | ||
|
|
d20b257134 | ||
|
|
173205ca51 |
@@ -56,6 +56,14 @@
|
|||||||
- Output: `prompt`
|
- Output: `prompt`
|
||||||
- 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`
|
||||||
|
- 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`, `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.
|
||||||
|
- 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.
|
||||||
|
|
||||||
- `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`
|
||||||
- Outputs: `seconds`, `frames`, `info`
|
- Outputs: `seconds`, `frames`, `info`
|
||||||
@@ -78,20 +86,32 @@
|
|||||||
|
|
||||||
- `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`
|
||||||
- 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`
|
||||||
|
- Inputs: `soundscape`, `soundscape_description`
|
||||||
|
- 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; the preset fills the editable textbox, and the edited text 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; 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`
|
||||||
@@ -249,7 +269,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 still want a single `REFERENCE` socket.
|
`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.
|
||||||
|
|
||||||
|
|||||||
+669
-8
@@ -27,6 +27,25 @@ _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_MAX_CHARS = 7000
|
||||||
|
_PICTURE_TAG_RE = re.compile(r"<\s*picture[\s_\-]*(\d+)\s*>", re.I)
|
||||||
|
_REF_TAG_RE = re.compile(r"<\s*ref[\s_\-]*(\d+)\s*>", re.I)
|
||||||
|
_ANATOMY_GUARD_TEXT = (
|
||||||
|
"Each person has one head, two arms, two hands with five fingers on each hand, "
|
||||||
|
"and two legs with two feet. Limbs stay attached to the correct body and move "
|
||||||
|
"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(
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
@@ -346,6 +365,100 @@ _ANCHOR_STYLE_PRESETS = OrderedDict(
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
_SOUNDSCAPE_PRESETS = OrderedDict(
|
||||||
|
[
|
||||||
|
(
|
||||||
|
"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", ""),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
_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():
|
||||||
|
return list(_SOUNDSCAPE_PRESETS.keys())
|
||||||
|
|
||||||
|
|
||||||
|
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 = {
|
_LOAD_IMAGES_FOLDER_DEFAULT_STATE = {
|
||||||
"version": 1,
|
"version": 1,
|
||||||
"folder": "",
|
"folder": "",
|
||||||
@@ -366,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:
|
||||||
@@ -920,6 +1038,266 @@ def normalize_reference(value, picture_id=None, allow_image_fallback=True):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_text(value):
|
||||||
|
return " ".join(str(value or "").split()).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_sentence(value):
|
||||||
|
text = _reference_text(value)
|
||||||
|
if text and text[-1] not in ".!?":
|
||||||
|
text += "."
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_name_keys(ref):
|
||||||
|
names = []
|
||||||
|
for key in ("name", "id"):
|
||||||
|
value = _reference_text(ref.get(key))
|
||||||
|
if value:
|
||||||
|
names.append(value)
|
||||||
|
for alias in ref.get("aliases") or []:
|
||||||
|
value = _reference_text(alias)
|
||||||
|
if value:
|
||||||
|
names.append(value)
|
||||||
|
seen = set()
|
||||||
|
out = []
|
||||||
|
for name in names:
|
||||||
|
key = name.lower()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
out.append(name)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_image(ref):
|
||||||
|
if not isinstance(ref, dict):
|
||||||
|
return None
|
||||||
|
return ref.get("image")
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_prompt_refs(raw_refs):
|
||||||
|
refs = []
|
||||||
|
for slot_number, raw in enumerate(raw_refs or (), 1):
|
||||||
|
if raw is None:
|
||||||
|
refs.append(None)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
ref = normalize_reference(raw, picture_id=slot_number, allow_image_fallback=False)
|
||||||
|
except Exception:
|
||||||
|
refs.append(None)
|
||||||
|
continue
|
||||||
|
if _reference_image(ref) is None:
|
||||||
|
refs.append(None)
|
||||||
|
else:
|
||||||
|
refs.append(ref)
|
||||||
|
return refs
|
||||||
|
|
||||||
|
|
||||||
|
def _explicit_reference_tags(text):
|
||||||
|
return sorted(
|
||||||
|
{
|
||||||
|
int(match.group(1))
|
||||||
|
for pattern in (_PICTURE_TAG_RE, _REF_TAG_RE)
|
||||||
|
for match in pattern.finditer(text or "")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _name_matches_reference(text, ref):
|
||||||
|
haystack = str(text or "")
|
||||||
|
for name in _reference_name_keys(ref):
|
||||||
|
if re.search(r"\b" + re.escape(name) + r"\b", haystack, re.I):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _selected_prompt_refs(action_prompt, refs):
|
||||||
|
selected = []
|
||||||
|
seen_slots = set()
|
||||||
|
for slot_number in _explicit_reference_tags(action_prompt):
|
||||||
|
if not (1 <= slot_number <= len(refs)):
|
||||||
|
continue
|
||||||
|
ref = refs[slot_number - 1]
|
||||||
|
if ref is None:
|
||||||
|
continue
|
||||||
|
selected.append((slot_number, ref))
|
||||||
|
seen_slots.add(slot_number)
|
||||||
|
for slot_number, ref in enumerate(refs, 1):
|
||||||
|
if slot_number in seen_slots or ref is None:
|
||||||
|
continue
|
||||||
|
if _name_matches_reference(action_prompt, ref):
|
||||||
|
selected.append((slot_number, ref))
|
||||||
|
seen_slots.add(slot_number)
|
||||||
|
return selected
|
||||||
|
|
||||||
|
|
||||||
|
def _replace_reference_tags(text, picture_map):
|
||||||
|
def repl(match):
|
||||||
|
original = int(match.group(1))
|
||||||
|
compacted = picture_map.get(original)
|
||||||
|
if compacted is None:
|
||||||
|
return ""
|
||||||
|
return f"<Picture {compacted}>"
|
||||||
|
|
||||||
|
rewritten = _PICTURE_TAG_RE.sub(repl, str(text or ""))
|
||||||
|
rewritten = _REF_TAG_RE.sub(repl, rewritten)
|
||||||
|
return re.sub(r"[ \t]{2,}", " ", rewritten).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_fact_sentence(ref, label):
|
||||||
|
if ref.get("kind") != "character":
|
||||||
|
return ""
|
||||||
|
facts = dict(ref.get("facts") or {})
|
||||||
|
bits = []
|
||||||
|
aliases = [_reference_text(alias) for alias in (ref.get("aliases") or []) if _reference_text(alias)]
|
||||||
|
if aliases:
|
||||||
|
bits.append(f"also known as {aliases[0]}")
|
||||||
|
for key in ("gender", "nationality", "occupation"):
|
||||||
|
value = _reference_text(facts.get(key))
|
||||||
|
if value:
|
||||||
|
bits.append(value if key != "occupation" else f"works as {value}")
|
||||||
|
age = _parse_positive_int(facts.get("age"))
|
||||||
|
if age is not None:
|
||||||
|
bits.append(f"{age} years old")
|
||||||
|
feet = _reference_text(facts.get("height_feet"))
|
||||||
|
inches = _reference_text(facts.get("height_inches"))
|
||||||
|
if feet and inches:
|
||||||
|
bits.append(f"{feet} foot {inches} tall")
|
||||||
|
elif feet:
|
||||||
|
bits.append(f"{feet} foot tall")
|
||||||
|
accent = _reference_text(facts.get("accent"))
|
||||||
|
if accent:
|
||||||
|
bits.append(f"speaks with a {accent} accent")
|
||||||
|
if not bits:
|
||||||
|
return ""
|
||||||
|
return f"Character facts for {label}: " + ", ".join(bits) + "."
|
||||||
|
|
||||||
|
|
||||||
|
def _reference_context(ref, compact_picture_number):
|
||||||
|
label_name = _reference_text(ref.get("name")) or _reference_text(ref.get("id")) or "this reference"
|
||||||
|
label = f"<Picture {compact_picture_number}> {label_name}"
|
||||||
|
parts = [_reference_sentence(_reference_summary(ref.get("kind"), label_name, compact_picture_number))]
|
||||||
|
description = _reference_sentence(ref.get("description"))
|
||||||
|
wardrobe = _reference_sentence(ref.get("wardrobe"))
|
||||||
|
general = _reference_sentence(ref.get("general"))
|
||||||
|
facts = _reference_fact_sentence(ref, label)
|
||||||
|
if ref.get("kind") == "location":
|
||||||
|
if description:
|
||||||
|
parts.append(f"Location context for {label}: {description}")
|
||||||
|
if general:
|
||||||
|
parts.append(f"Location notes for {label}: {general}")
|
||||||
|
else:
|
||||||
|
if facts:
|
||||||
|
parts.append(facts)
|
||||||
|
if description:
|
||||||
|
parts.append(f"Persistent appearance for {label}: {description}")
|
||||||
|
if wardrobe:
|
||||||
|
parts.append(f"Persistent wardrobe/style for {label}: {wardrobe}")
|
||||||
|
if general:
|
||||||
|
parts.append(f"Character notes for {label}: {general}")
|
||||||
|
return " ".join(part for part in parts if part).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _subject_count_guard_text(selected_refs):
|
||||||
|
character_labels = []
|
||||||
|
seen_characters = set()
|
||||||
|
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"))
|
||||||
|
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}>"
|
||||||
|
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):
|
||||||
|
clean = _reference_text(text)
|
||||||
|
if clean:
|
||||||
|
parts.append(f"{label}: {clean}")
|
||||||
|
|
||||||
|
|
||||||
|
def curate_h3_prompt(
|
||||||
|
action_prompt,
|
||||||
|
anchor="",
|
||||||
|
soundscape="",
|
||||||
|
bgm="",
|
||||||
|
refs=(),
|
||||||
|
anatomy_guard="auto",
|
||||||
|
subject_count_guard="auto",
|
||||||
|
):
|
||||||
|
normalized_refs = _normalize_prompt_refs(refs)
|
||||||
|
selected = _selected_prompt_refs(action_prompt, normalized_refs)
|
||||||
|
picture_map = {slot_number: index for index, (slot_number, _ref) in enumerate(selected, 1)}
|
||||||
|
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 = []
|
||||||
|
_append_prompt_section(prompt_parts, "Scene anchor", anchor_text)
|
||||||
|
_append_prompt_section(prompt_parts, "Reference context", reference_description)
|
||||||
|
_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)):
|
||||||
|
_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_text)
|
||||||
|
_append_prompt_section(prompt_parts, "background_music", bgm_text)
|
||||||
|
|
||||||
|
prompt = "\n\n".join(prompt_parts).strip()
|
||||||
|
if len(prompt) > _H3_PROMPT_MAX_CHARS:
|
||||||
|
prompt = prompt[: _H3_PROMPT_MAX_CHARS - 3].rstrip() + "..."
|
||||||
|
images = [_reference_image(ref) for _slot, ref in selected]
|
||||||
|
images.extend([None] * (_H3_PROMPT_REF_SLOTS - len(images)))
|
||||||
|
debug = (
|
||||||
|
f"Selected {len(selected)} reference(s): "
|
||||||
|
+ ", ".join(
|
||||||
|
f"input {slot}-><Picture {index}> {_reference_text(ref.get('name')) or ref.get('id')}"
|
||||||
|
for index, (slot, ref) in enumerate(selected, 1)
|
||||||
|
)
|
||||||
|
if selected
|
||||||
|
else "Selected 0 references."
|
||||||
|
)
|
||||||
|
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):
|
||||||
text = str(value or "").strip()
|
text = str(value or "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
@@ -1669,8 +2047,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"
|
||||||
|
|
||||||
@@ -1831,7 +2209,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:
|
||||||
@@ -1839,8 +2246,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"
|
||||||
|
|
||||||
@@ -1928,7 +2335,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:
|
||||||
@@ -2163,6 +2589,235 @@ class DumasLocationReferenceNode:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DumasSoundscapeHelperNode:
|
||||||
|
DESCRIPTION = (
|
||||||
|
"Choose a soundscape preset, auto-fill its editable description, and pass "
|
||||||
|
"the final soundscape text downstream for MiniMax H3 prompts."
|
||||||
|
)
|
||||||
|
RETURN_TYPES = ("STRING",)
|
||||||
|
RETURN_NAMES = ("soundscape",)
|
||||||
|
FUNCTION = "build_soundscape"
|
||||||
|
CATEGORY = "Dumas/MiniMax"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
default_soundscape = "quiet interior"
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"soundscape": (
|
||||||
|
_soundscape_options(),
|
||||||
|
{
|
||||||
|
"default": default_soundscape,
|
||||||
|
"tooltip": "Preset title used to seed the editable soundscape description.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"soundscape_description": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": _soundscape_description(default_soundscape),
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": (
|
||||||
|
"Editable environmental audio description. Whatever text is here "
|
||||||
|
"is what the node outputs to the soundscape socket."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def build_soundscape(self, soundscape, soundscape_description):
|
||||||
|
text = str(soundscape_description or "").strip()
|
||||||
|
if not text:
|
||||||
|
text = _soundscape_description(soundscape)
|
||||||
|
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/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."
|
||||||
|
)
|
||||||
|
RETURN_TYPES = (
|
||||||
|
("STRING",)
|
||||||
|
+ ("IMAGE",) * _H3_PROMPT_REF_SLOTS
|
||||||
|
+ ("INT", "STRING", "STRING", "STRING", "STRING")
|
||||||
|
+ ("IMAGE",) * _H3_PROMPT_REF_SLOTS
|
||||||
|
)
|
||||||
|
RETURN_NAMES = (
|
||||||
|
"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",
|
||||||
|
)
|
||||||
|
FUNCTION = "curate_prompt"
|
||||||
|
CATEGORY = "Dumas/MiniMax"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def INPUT_TYPES(cls):
|
||||||
|
optional = {
|
||||||
|
"anchor": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"forceInput": True,
|
||||||
|
"tooltip": "Optional anchor/style text, usually from Dumas Anchor Style.",
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"soundscape": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"forceInput": True,
|
||||||
|
"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}"] = (
|
||||||
|
_REFERENCE_TYPE,
|
||||||
|
{
|
||||||
|
"tooltip": (
|
||||||
|
f"Optional structured reference {slot}. The curator only outputs "
|
||||||
|
"it if the action prompt mentions its name/alias or an explicit "
|
||||||
|
f"<Picture {slot}>/<ref{slot}> tag."
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"required": {
|
||||||
|
"action_prompt": (
|
||||||
|
"STRING",
|
||||||
|
{
|
||||||
|
"default": "",
|
||||||
|
"multiline": True,
|
||||||
|
"tooltip": (
|
||||||
|
"Write the final shot action here using character/location names. "
|
||||||
|
"Mention a reference by name, alias, <Picture N>, or <refN> to use it."
|
||||||
|
),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
"anatomy_guard": (
|
||||||
|
["auto", "on", "off"],
|
||||||
|
{
|
||||||
|
"default": "on",
|
||||||
|
"tooltip": (
|
||||||
|
"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,
|
||||||
|
}
|
||||||
|
|
||||||
|
def curate_prompt(
|
||||||
|
self,
|
||||||
|
action_prompt,
|
||||||
|
anatomy_guard,
|
||||||
|
subject_count_guard,
|
||||||
|
anchor="",
|
||||||
|
soundscape="",
|
||||||
|
bgm="",
|
||||||
|
ref_1=None,
|
||||||
|
ref_2=None,
|
||||||
|
ref_3=None,
|
||||||
|
ref_4=None,
|
||||||
|
ref_5=None,
|
||||||
|
ref_6=None,
|
||||||
|
ref_7=None,
|
||||||
|
ref_8=None,
|
||||||
|
ref_9=None,
|
||||||
|
):
|
||||||
|
return curate_h3_prompt(
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DumasAnchorStyleNode:
|
class DumasAnchorStyleNode:
|
||||||
DESCRIPTION = (
|
DESCRIPTION = (
|
||||||
"Choose an anchor-style preset, auto-fill its full description, and pass "
|
"Choose an anchor-style preset, auto-fill its full description, and pass "
|
||||||
@@ -2200,9 +2855,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,)
|
||||||
|
|
||||||
|
|
||||||
@@ -2214,6 +2869,9 @@ NODE_CLASS_MAPPINGS = {
|
|||||||
"DumasH3PlanExtractSceneImages": DumasH3PlanExtractSceneImagesNode,
|
"DumasH3PlanExtractSceneImages": DumasH3PlanExtractSceneImagesNode,
|
||||||
"DumasCharacterReference": DumasCharacterReferenceNode,
|
"DumasCharacterReference": DumasCharacterReferenceNode,
|
||||||
"DumasLocationReference": DumasLocationReferenceNode,
|
"DumasLocationReference": DumasLocationReferenceNode,
|
||||||
|
"DumasSoundscapeHelper": DumasSoundscapeHelperNode,
|
||||||
|
"DumasBackgroundMusicHelper": DumasBackgroundMusicHelperNode,
|
||||||
|
"DumasH3PromptCurator": DumasH3PromptCuratorNode,
|
||||||
"DumasAnchorStyle": DumasAnchorStyleNode,
|
"DumasAnchorStyle": DumasAnchorStyleNode,
|
||||||
"DumasCharacterHelper": DumasCharacterHelperNode,
|
"DumasCharacterHelper": DumasCharacterHelperNode,
|
||||||
"DumasLocationHelper": DumasLocationHelperNode,
|
"DumasLocationHelper": DumasLocationHelperNode,
|
||||||
@@ -2228,6 +2886,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
|||||||
"DumasH3PlanExtractSceneImages": "Dumas H3 Plan Extract Scene Images",
|
"DumasH3PlanExtractSceneImages": "Dumas H3 Plan Extract Scene Images",
|
||||||
"DumasCharacterReference": "Dumas Character Reference",
|
"DumasCharacterReference": "Dumas Character Reference",
|
||||||
"DumasLocationReference": "Dumas Location Reference",
|
"DumasLocationReference": "Dumas Location Reference",
|
||||||
|
"DumasSoundscapeHelper": "Dumas Soundscape Helper",
|
||||||
|
"DumasBackgroundMusicHelper": "Dumas Background Music Helper",
|
||||||
|
"DumasH3PromptCurator": "Dumas H3 Prompt Curator",
|
||||||
"DumasAnchorStyle": "Dumas Anchor Style",
|
"DumasAnchorStyle": "Dumas Anchor Style",
|
||||||
"DumasCharacterHelper": "Dumas Character Helper",
|
"DumasCharacterHelper": "Dumas Character Helper",
|
||||||
"DumasLocationHelper": "Dumas Location Helper",
|
"DumasLocationHelper": "Dumas Location Helper",
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -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,209 @@ 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):
|
||||||
|
node = self.image_nodes.DumasSoundscapeHelperNode()
|
||||||
|
|
||||||
|
result = node.build_soundscape("rainy street", "")
|
||||||
|
|
||||||
|
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()
|
||||||
|
cafe_image = FakeTensorBatch()
|
||||||
|
van_image = FakeTensorBatch()
|
||||||
|
dave = self.image_nodes.make_reference(
|
||||||
|
kind="character",
|
||||||
|
image=dave_image,
|
||||||
|
name="Dave",
|
||||||
|
aliases="The Locksmith",
|
||||||
|
description="tired eyes, cropped brown hair",
|
||||||
|
wardrobe="red flight jacket",
|
||||||
|
)
|
||||||
|
cafe = self.image_nodes.make_reference(
|
||||||
|
kind="location",
|
||||||
|
image=cafe_image,
|
||||||
|
name="Coffee Shop",
|
||||||
|
description="warm tungsten lighting and rainy windows",
|
||||||
|
)
|
||||||
|
van = self.image_nodes.make_reference(
|
||||||
|
kind="location",
|
||||||
|
image=van_image,
|
||||||
|
name="Blue Van",
|
||||||
|
description="scuffed blue delivery van",
|
||||||
|
)
|
||||||
|
|
||||||
|
result = node.curate_prompt(
|
||||||
|
action_prompt="Dave runs from the Coffee Shop into the rain.",
|
||||||
|
anatomy_guard="auto",
|
||||||
|
subject_count_guard="auto",
|
||||||
|
anchor="grounded handheld thriller",
|
||||||
|
soundscape="steady rain",
|
||||||
|
bgm="low suspense music",
|
||||||
|
ref_1=dave,
|
||||||
|
ref_2=van,
|
||||||
|
ref_3=cafe,
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = result[0]
|
||||||
|
self.assertIn("<Picture 1> Dave", prompt)
|
||||||
|
self.assertIn("<Picture 2> Coffee Shop", prompt)
|
||||||
|
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)
|
||||||
|
self.assertIsNone(result[3])
|
||||||
|
self.assertEqual(result[10], 2)
|
||||||
|
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):
|
||||||
|
node = self.image_nodes.DumasH3PromptCuratorNode()
|
||||||
|
image1 = FakeTensorBatch()
|
||||||
|
image3 = FakeTensorBatch()
|
||||||
|
unused = FakeTensorBatch()
|
||||||
|
first = self.image_nodes.make_reference(kind="character", image=image1, name="Maya")
|
||||||
|
second = self.image_nodes.make_reference(kind="location", image=unused, name="Lobby")
|
||||||
|
third = self.image_nodes.make_reference(kind="location", image=image3, name="Rooftop")
|
||||||
|
|
||||||
|
result = node.curate_prompt(
|
||||||
|
action_prompt="<Picture 1> Maya crosses to <ref3> as the wind rises.",
|
||||||
|
anatomy_guard="off",
|
||||||
|
subject_count_guard="off",
|
||||||
|
ref_1=first,
|
||||||
|
ref_2=second,
|
||||||
|
ref_3=third,
|
||||||
|
)
|
||||||
|
|
||||||
|
prompt = result[0]
|
||||||
|
self.assertIn("<Picture 1> Maya crosses to <Picture 2>", prompt)
|
||||||
|
self.assertNotIn("<Picture 3>", prompt)
|
||||||
|
self.assertIs(result[1], image1)
|
||||||
|
self.assertIs(result[2], image3)
|
||||||
|
self.assertIsNone(result[3])
|
||||||
|
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_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):
|
||||||
|
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
|
||||||
@@ -418,8 +631,21 @@ 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["DumasBackgroundMusicHelper"], self.image_nodes.DumasBackgroundMusicHelperNode)
|
||||||
|
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["DumasBackgroundMusicHelper"], "Dumas Background Music Helper")
|
||||||
|
self.assertEqual(display["DumasH3PromptCurator"], "Dumas H3 Prompt Curator")
|
||||||
|
|
||||||
|
def test_h3_prompt_curator_uses_documented_reference_limits(self):
|
||||||
|
node = self.image_nodes.DumasH3PromptCuratorNode()
|
||||||
|
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[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()
|
||||||
@@ -482,6 +708,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