Implement structured H3 reference objects
This commit is contained in:
+204
-57
@@ -8,7 +8,7 @@ One node covering both of H3's conditioning tasks:
|
||||
* REF2VA -- reference images condition the shot on what a character LOOKS like,
|
||||
independent of any frame.
|
||||
|
||||
Connect nothing to ref_image_* and it behaves exactly as the FL2VA node always
|
||||
Connect nothing to ref_* and it behaves exactly as the FL2VA node always
|
||||
did. Connect a reference and `ref_mode` decides which shots use it.
|
||||
|
||||
THE ONE RULE: a shot carries EITHER references or the last-frame handoff, never
|
||||
@@ -275,7 +275,7 @@ ADDED_WIDGETS = (
|
||||
"exposed_terms", "anatomy_guard", "lock_restraints", "solidity_guard",
|
||||
"motion_guard", "contact_guard",
|
||||
"auto_soundscape", "allow_nonspeech_vocals",
|
||||
"ref_image_5", "ref_image_6", "ref_image_7", "ref_image_8", "ref_image_9",
|
||||
"ref_5", "ref_6", "ref_7", "ref_8", "ref_9",
|
||||
"plan", "plan_scene_index",
|
||||
)
|
||||
|
||||
@@ -3868,17 +3868,18 @@ def ref_image_canvas(w, h, gen_w, gen_h, mode="match"):
|
||||
return snap(w), snap(h)
|
||||
|
||||
|
||||
def _build_ref_images(vae, images, gen_w, gen_h, mode="match"):
|
||||
"""(tokenizer items, DiT blocks) for a list of reference IMAGE tensors.
|
||||
|
||||
The tokenizer labels each one `<Picture N>:` itself, in the order given here --
|
||||
so the roster the prompt refers to is decided by input order, not by anything
|
||||
written in the prompt."""
|
||||
items, blocks = [], []
|
||||
for img in images:
|
||||
if img is None:
|
||||
continue
|
||||
h, w = int(img.shape[1]), int(img.shape[2])
|
||||
def _build_ref_images(vae, images, gen_w, gen_h, mode="match"):
|
||||
"""(tokenizer items, DiT blocks) for a list of reference images.
|
||||
|
||||
The tokenizer labels each one `<Picture N>:` itself, in the order given here --
|
||||
so the roster the prompt refers to is decided by input order, not by anything
|
||||
written in the prompt."""
|
||||
items, blocks = [], []
|
||||
for source in images:
|
||||
img = _reference_image(source)
|
||||
if img is None:
|
||||
continue
|
||||
h, w = int(img.shape[1]), int(img.shape[2])
|
||||
tw, th = ref_image_canvas(w, h, gen_w, gen_h, mode)
|
||||
resized = _resize(img[:1], tw, th, "disabled")
|
||||
items.append({"type": "image", "data": resized})
|
||||
@@ -3887,11 +3888,11 @@ def _build_ref_images(vae, images, gen_w, gen_h, mode="match"):
|
||||
return items, blocks
|
||||
|
||||
|
||||
def _build_shot_conditioning(clip, vae, prompt, width, height, length, fps, handoff,
|
||||
ref_images=None, ref_image_size="match", ref_noise_aug=None,
|
||||
audio_vae=None, silent=False):
|
||||
latent, fc = _empty_av_latent(width, height, length, fps)
|
||||
refs = [r for r in (ref_images or []) if r is not None]
|
||||
def _build_shot_conditioning(clip, vae, prompt, width, height, length, fps, handoff,
|
||||
ref_images=None, ref_image_size="match", ref_noise_aug=None,
|
||||
audio_vae=None, silent=False):
|
||||
latent, fc = _empty_av_latent(width, height, length, fps)
|
||||
refs = [r for r in (ref_images or []) if _reference_image(r) is not None]
|
||||
if refs:
|
||||
# ref2va: this shot is reference-conditioned rather than keyframe-conditioned,
|
||||
# and run() decides which per shot. A tagged shot is handed the previous
|
||||
@@ -4044,9 +4045,135 @@ def picture_tags(text):
|
||||
return sorted({int(m.group(1)) for m in _PICTURE_TAG.finditer(text or "")})
|
||||
|
||||
|
||||
def _reference_slot(ref, slot_index=None):
|
||||
return _image_nodes.normalize_reference(ref, picture_id=slot_index, allow_image_fallback=True)
|
||||
|
||||
|
||||
def _reference_image(ref):
|
||||
try:
|
||||
normalized = _reference_slot(ref)
|
||||
except Exception:
|
||||
return None
|
||||
return normalized.get("image")
|
||||
|
||||
|
||||
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 _slot_refs_for_text(text, ref_slots):
|
||||
refs = []
|
||||
for slot_number in picture_tags(text):
|
||||
if not (1 <= slot_number <= len(ref_slots or [])):
|
||||
continue
|
||||
ref = ref_slots[slot_number - 1]
|
||||
if _reference_image(ref) is None:
|
||||
continue
|
||||
refs.append((slot_number, _reference_slot(ref, slot_number)))
|
||||
return refs
|
||||
|
||||
|
||||
def _named_character_refs_for_text(text, ref_slots):
|
||||
haystack = str(text or "")
|
||||
matched = []
|
||||
for slot_number, raw in enumerate(ref_slots or [], 1):
|
||||
ref = _reference_slot(raw, slot_number)
|
||||
if ref.get("kind") != "character" or _reference_image(ref) is None:
|
||||
continue
|
||||
for name in _reference_name_keys(ref):
|
||||
if re.search(r"\b" + re.escape(name) + r"\b", haystack, re.I):
|
||||
matched.append((slot_number, ref))
|
||||
break
|
||||
return matched
|
||||
|
||||
|
||||
def _matched_reference_slots(text, ref_slots):
|
||||
matched = []
|
||||
seen = set()
|
||||
for slot_number, ref in _slot_refs_for_text(text, ref_slots) + _named_character_refs_for_text(text, ref_slots):
|
||||
if slot_number in seen:
|
||||
continue
|
||||
seen.add(slot_number)
|
||||
matched.append((slot_number, ref))
|
||||
return matched
|
||||
|
||||
|
||||
def _reference_context_for_text(text, ref_slots):
|
||||
parts = []
|
||||
for slot_number, ref in _matched_reference_slots(text, ref_slots):
|
||||
label = _reference_text(ref.get("name")) or _reference_text(ref.get("id")) or f"reference {slot_number}"
|
||||
description = _reference_sentence(ref.get("description"))
|
||||
wardrobe = _reference_sentence(ref.get("wardrobe"))
|
||||
general = _reference_sentence(ref.get("general"))
|
||||
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}")
|
||||
continue
|
||||
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(parts).strip()
|
||||
|
||||
|
||||
def _reference_character_memory(ref_slots):
|
||||
lines = []
|
||||
seen = set()
|
||||
for slot_number, raw in enumerate(ref_slots or [], 1):
|
||||
ref = _reference_slot(raw, slot_number)
|
||||
if ref.get("kind") != "character":
|
||||
continue
|
||||
wardrobe = _reference_text(ref.get("wardrobe"))
|
||||
if not wardrobe:
|
||||
continue
|
||||
label = (
|
||||
_reference_text(ref.get("name"))
|
||||
or (_reference_name_keys(ref)[0] if _reference_name_keys(ref) else "")
|
||||
)
|
||||
line = f"{label} = {wardrobe}" if label else wardrobe
|
||||
key = line.lower()
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
lines.append(line)
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def _connected_refs(ref_slots):
|
||||
"""Connected refs only, preserving slot order and skipping empty sockets."""
|
||||
return [ref for ref in (ref_slots or []) if ref is not None]
|
||||
return [ref for ref in (ref_slots or []) if _reference_image(ref) is not None]
|
||||
|
||||
|
||||
def _plan_scene_refs(plan, scene_index):
|
||||
@@ -4054,7 +4181,13 @@ def _plan_scene_refs(plan, scene_index):
|
||||
if plan is None:
|
||||
return (None,) * _image_nodes._H3_PLAN_IMAGE_SLOTS
|
||||
extracted = _image_nodes.DumasH3PlanExtractSceneImagesNode().extract(plan, scene_index)
|
||||
return tuple(extracted[1:1 + _image_nodes._H3_PLAN_IMAGE_SLOTS])
|
||||
refs = []
|
||||
for slot_number, image in enumerate(
|
||||
extracted[1:1 + _image_nodes._H3_PLAN_IMAGE_SLOTS],
|
||||
1,
|
||||
):
|
||||
refs.append(_reference_slot(image, slot_number) if image is not None else None)
|
||||
return tuple(refs)
|
||||
|
||||
|
||||
def _merge_ref_slots(direct_slots, plan_slots):
|
||||
@@ -5328,18 +5461,18 @@ class H3LongVideos:
|
||||
# the order they are handed that shot.
|
||||
# Refer to socket tags in the prompt if you want a reference bound to
|
||||
# a named character ("Kristy, <Picture 7>, walks in").
|
||||
"ref_image_1": ("IMAGE", {"tooltip": "Reference image <Picture 1> -- identity/appearance "
|
||||
"ref_1": ("REFERENCE", {"tooltip": "Reference object for <Picture 1> -- image plus identity/environment metadata "
|
||||
"carried into the shots. Which shots receive it is set by ref_mode (or <Picture N> "
|
||||
"tags in the beats); a referenced shot ALSO carries the previous frame as its "
|
||||
"keyframe, so taking a reference never costs continuity."}),
|
||||
"ref_image_2": ("IMAGE", {"tooltip": "Reference image <Picture 2>."}),
|
||||
"ref_image_3": ("IMAGE", {"tooltip": "Reference image <Picture 3>."}),
|
||||
"ref_image_4": ("IMAGE", {"tooltip": "Reference image <Picture 4>."}),
|
||||
"ref_image_5": ("IMAGE", {"tooltip": "Reference image <Picture 5>."}),
|
||||
"ref_image_6": ("IMAGE", {"tooltip": "Reference image <Picture 6>."}),
|
||||
"ref_image_7": ("IMAGE", {"tooltip": "Reference image <Picture 7>."}),
|
||||
"ref_image_8": ("IMAGE", {"tooltip": "Reference image <Picture 8>."}),
|
||||
"ref_image_9": ("IMAGE", {"tooltip": "Reference image <Picture 9>."}),
|
||||
"ref_2": ("REFERENCE", {"tooltip": "Reference object for <Picture 2>."}),
|
||||
"ref_3": ("REFERENCE", {"tooltip": "Reference object for <Picture 3>."}),
|
||||
"ref_4": ("REFERENCE", {"tooltip": "Reference object for <Picture 4>."}),
|
||||
"ref_5": ("REFERENCE", {"tooltip": "Reference object for <Picture 5>."}),
|
||||
"ref_6": ("REFERENCE", {"tooltip": "Reference object for <Picture 6>."}),
|
||||
"ref_7": ("REFERENCE", {"tooltip": "Reference object for <Picture 7>."}),
|
||||
"ref_8": ("REFERENCE", {"tooltip": "Reference object for <Picture 8>."}),
|
||||
"ref_9": ("REFERENCE", {"tooltip": "Reference object for <Picture 9>."}),
|
||||
"plan": (
|
||||
"H3_CHAIN_PLAN",
|
||||
{
|
||||
@@ -5356,7 +5489,7 @@ class H3LongVideos:
|
||||
"max": 9999,
|
||||
"step": 1,
|
||||
"tooltip": "1-based plan scene index to read from `plan`. "
|
||||
"Any directly-wired ref_image socket overrides the same slot "
|
||||
"Any directly-wired ref socket overrides the same slot "
|
||||
"from the plan scene."
|
||||
},
|
||||
),
|
||||
@@ -5853,9 +5986,9 @@ class H3LongVideos:
|
||||
watermark_opacity=0.75, watermark_margin=3.0,
|
||||
intro_text="", intro_position="center", intro_seconds=3.0, intro_fade=0.6,
|
||||
intro_size=9.0, overlay_font="arial.ttf", overlay_stroke=0,
|
||||
ref_image_1=None, ref_image_2=None, ref_image_3=None, ref_image_4=None,
|
||||
ref_image_5=None, ref_image_6=None, ref_image_7=None, ref_image_8=None,
|
||||
ref_image_9=None,
|
||||
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,
|
||||
plan=None, plan_scene_index=1,
|
||||
ref_mode="where tagged", ref_image_size="match", ref_noise_aug=0.999,
|
||||
graph=None, node_id=None):
|
||||
@@ -5872,19 +6005,21 @@ class H3LongVideos:
|
||||
|
||||
# H3 renders 24 fps, always. Honor the widget only as a warning: a lower value
|
||||
# used to silently shorten every shot (10s -> 124f -> 5.2s of real time).
|
||||
fps_note = ("" if int(fps) == H3_FPS else
|
||||
f"fps widget is {int(fps)} but H3 always renders {H3_FPS} fps -- all durations "
|
||||
f"computed at {H3_FPS}; set your video-save node to {H3_FPS} too")
|
||||
fps_note = ("" if int(fps) == H3_FPS else
|
||||
f"fps widget is {int(fps)} but H3 always renders {H3_FPS} fps -- all durations "
|
||||
f"computed at {H3_FPS}; set your video-save node to {H3_FPS} too")
|
||||
fps = H3_FPS
|
||||
w, h = parse_resolution(resolution)
|
||||
direct_ref_slots = (
|
||||
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,
|
||||
ref_1, ref_2, ref_3, ref_4, ref_5,
|
||||
ref_6, ref_7, ref_8, ref_9,
|
||||
)
|
||||
plan_ref_slots = _plan_scene_refs(plan, plan_scene_index)
|
||||
ref_slots = _merge_ref_slots(direct_ref_slots, plan_ref_slots)
|
||||
plan_ref_count = len(_connected_refs(plan_ref_slots))
|
||||
direct_ref_count = len(_connected_refs(direct_ref_slots))
|
||||
derived_character_memory = _reference_character_memory(ref_slots)
|
||||
effective_character_memory = (character_memory or "").strip() or derived_character_memory
|
||||
# A pixel budget overrides the preset's SIZE while keeping its aspect ratio,
|
||||
# so the dropdown chooses the shape and this chooses how big. Scaling from
|
||||
# the preset's own dimensions is what makes 1.00MP reproduce each native
|
||||
@@ -5932,9 +6067,9 @@ class H3LongVideos:
|
||||
# anchor to avoid introducing them twice). Keep it as a BEAT and say so loudly,
|
||||
# rather than losing a shot and the scene text along with it.
|
||||
anchor_note = ""
|
||||
if (not anchor_override.strip()) and paras and \
|
||||
(anchor_contributes_nothing(anchor, character_memory.strip())
|
||||
or anchor_is_action_beat(anchor, paras[1:])):
|
||||
if (not anchor_override.strip()) and paras and \
|
||||
(anchor_contributes_nothing(anchor, effective_character_memory)
|
||||
or anchor_is_action_beat(anchor, paras[1:])):
|
||||
preview = " ".join(anchor.split())[:60]
|
||||
anchor, beat_paras = "", paras
|
||||
anchor_note = (
|
||||
@@ -6077,23 +6212,35 @@ class H3LongVideos:
|
||||
"BABBLE RISK -- " + "; ".join(filler_warnings)
|
||||
+ ". Turn per_beat_length ON to size these shots from their line, or set "
|
||||
"'seconds:' on the beat")
|
||||
wardrobe_notes = []
|
||||
strip_shots = [] # shots that newly bared a zone -> the NEXT shot starts fresh
|
||||
gens = distribute_generations(anchor, beats, global_soundscape.strip(),
|
||||
non_diegetic_music.strip(), character_memory.strip(),
|
||||
wardrobe_notes = []
|
||||
strip_shots = [] # shots that newly bared a zone -> the NEXT shot starts fresh
|
||||
gens = distribute_generations(anchor, beats, global_soundscape.strip(),
|
||||
non_diegetic_music.strip(), effective_character_memory,
|
||||
auto_wardrobe, auto_silence_nonspeech, allow_nonspeech_vocals, count_subjects,
|
||||
lora_on, notes_out=wardrobe_notes, auto_props=auto_props,
|
||||
prevent_nudity=prevent_nudity,
|
||||
exposed_terms=exposed_terms, strip_out=strip_shots,
|
||||
anatomy_guard=anatomy_on,
|
||||
anatomy_auto=anatomy_auto,
|
||||
lock_restraints=lock_restraints,
|
||||
solidity_guard=solidity_guard,
|
||||
motion_guard=motion_guard,
|
||||
contact_guard=contact_guard,
|
||||
count_auto=(subject_count_guard == "auto"))
|
||||
|
||||
# A scenery beat mid-chain hands the next shot a frame with no people in
|
||||
lora_on, notes_out=wardrobe_notes, auto_props=auto_props,
|
||||
prevent_nudity=prevent_nudity,
|
||||
exposed_terms=exposed_terms, strip_out=strip_shots,
|
||||
anatomy_guard=anatomy_on,
|
||||
anatomy_auto=anatomy_auto,
|
||||
lock_restraints=lock_restraints,
|
||||
solidity_guard=solidity_guard,
|
||||
motion_guard=motion_guard,
|
||||
contact_guard=contact_guard,
|
||||
count_auto=(subject_count_guard == "auto"))
|
||||
enriched_gens = []
|
||||
for block in gens:
|
||||
context = _reference_context_for_text(block, ref_slots)
|
||||
if context:
|
||||
block = re.sub(
|
||||
r"^(\[Generation \d+\]\s*)",
|
||||
lambda m: m.group(1) + context + " ",
|
||||
block,
|
||||
count=1,
|
||||
)
|
||||
enriched_gens.append(block)
|
||||
gens = enriched_gens
|
||||
|
||||
# A scenery beat mid-chain hands the next shot a frame with no people in
|
||||
# it. Both prompts are individually correct, so this is invisible without
|
||||
# looking at the sequence -- which is why chains lose their cast in the
|
||||
# middle rather than degrading steadily.
|
||||
|
||||
Reference in New Issue
Block a user