File size: 4,875 Bytes
7c2113f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 | """H3 Chain Preview -- the chain's progress panel, as its own node.
Split off `HandTieClips` deliberately. That node already carries a 21-widget shot
editor and a reference rail; folding a large media panel onto it as well makes
an unreadable node, and the panel wants to be somewhere else in the graph
anyway -- on the IMAGE wire, right before CreateVideo, where the thing being
previewed actually flows.
It is a passthrough: images and audio go straight out again, unchanged, so
dropping it into an existing chain costs nothing and removing it changes no
pixels. What it adds is a place for the live `h3_refchain_preview` events to
land, and an end-of-run report of the two numbers the chain never surfaced --
which pin mechanism each hop actually used, and how far the audio has drifted
from the video.
"""
from __future__ import annotations
try:
from server import PromptServer
except Exception: # noqa: BLE001 -- headless / API-only runs have no server
PromptServer = None
TAG = "HTCChainPreview"
FPS = 24
def _send(payload):
if PromptServer is None:
return
try:
PromptServer.instance.send_sync(
"h3_chain_preview", payload, PromptServer.instance.client_id)
except Exception as e: # noqa: BLE001
print(f"[{TAG}] preview send skipped: {e!r}", flush=True)
class HTCChainPreview:
"""Pass images and audio through, and report the join.
Wire between HandTieClips and CreateVideo.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE", {"tooltip": "From H3 Ref2VA Chain. Passed straight through."}),
},
"optional": {
"audio": ("AUDIO", {
"tooltip": (
"The chain's audio. Wire it and the panel reports A/V "
"drift, which accumulates roughly 40 ms per hop from the "
"audio crossfade at each join."
),
}),
"info": ("STRING", {
"forceInput": True,
"tooltip": "The chain's `info` output. Shown in the panel's detail view, never in the status strip.",
}),
},
"hidden": {"unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("IMAGE", "AUDIO")
RETURN_NAMES = ("images", "audio")
FUNCTION = "run"
CATEGORY = "Hand Tie Clips"
OUTPUT_NODE = False
DESCRIPTION = (
"Preview panel for an H3 Ref2VA chain. Sits on the IMAGE wire before "
"CreateVideo and passes images and audio through untouched. Shows the "
"live sample, per-hop progress, which pin mechanism each hop used "
"(Motion-Context vs the AddGuide fallback), and end-of-run A/V drift."
)
def run(self, images, audio=None, info="", unique_id=None):
frames = int(images.shape[0]) if images is not None else 0
video_s = frames / float(FPS)
payload = {
"node_id": unique_id,
"frames": frames,
"video_s": round(video_s, 3),
"width": int(images.shape[2]) if frames else 0,
"height": int(images.shape[1]) if frames else 0,
}
if isinstance(audio, dict) and audio.get("waveform") is not None:
wav = audio["waveform"]
sr = int(audio.get("sample_rate") or 0)
if sr:
audio_s = float(wav.shape[-1]) / float(sr)
drift_ms = (audio_s - video_s) * 1000.0
payload["audio_s"] = round(audio_s, 3)
payload["sample_rate"] = sr
payload["drift_ms"] = round(drift_ms, 1)
# Worth a console line too: it is cumulative across a chain and
# nothing else in the pack reports it.
print(f"[{TAG}] {frames}f / {video_s:.2f}s video, "
f"{audio_s:.2f}s audio, drift {drift_ms:+.0f} ms", flush=True)
if info:
payload["info"] = str(info)
_send(payload)
return (images, audio)
# -- pre-rename ids ----------------------------------------------------------
# A plain alias in NODE_CLASS_MAPPINGS keeps old workflows loading, but it also
# lists the node a second time in search: ComfyUI falls back to the mapping key
# when NODE_DISPLAY_NAME_MAPPINGS has no entry. Subclassing and setting
# DEPRECATED gets both -- server.py publishes `deprecated: True`, and the
# frontend's `Comfy.Node.ShowDeprecated` (off by default) hides it from search
# while leaving it fully functional in workflows that name it.
class _LegacyH3ChainPreview(HTCChainPreview):
DEPRECATED = True
NODE_CLASS_MAPPINGS = {
"HTCChainPreview": HTCChainPreview,
"H3ChainPreview": _LegacyH3ChainPreview,
}
NODE_DISPLAY_NAME_MAPPINGS = {"HTCChainPreview": "H3 Chain Preview"}
|