Compare commits
8
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01f71bd806 | ||
|
|
569212f1e7 | ||
|
|
04ca70a853 | ||
|
|
ed5823e8e7 | ||
|
|
22e0742d47 | ||
|
|
90ecd3218c | ||
|
|
1f136526cf | ||
|
|
e5db513585 |
@@ -1546,6 +1546,8 @@ async def _encode_inputs(
|
||||
* ``torch.Tensor`` rank 3 with last-dim != 3/4 → mask envelope (B,H,W).
|
||||
Rank-2 (H,W) tensors are also treated as masks.
|
||||
* ``dict`` with ``waveform`` + ``sample_rate`` → audio envelope.
|
||||
* ``VideoInput`` subclass (duck-typed via ``save_to`` +
|
||||
``get_duration``) → mp4-base64 video envelope.
|
||||
* Everything else passes through as-is (already JSON-serializable).
|
||||
|
||||
When ``max_inline_bytes`` is set, any envelope whose inline payload
|
||||
@@ -1694,6 +1696,10 @@ def _encode_one(
|
||||
) -> Any:
|
||||
if serialization.is_audio_input(value):
|
||||
return serialization.encode_audio_input(value)
|
||||
if serialization.is_video_input(value):
|
||||
return serialization.encode_video_input(value)
|
||||
if serialization.is_model3d_input(value):
|
||||
return serialization.encode_model3d_input(value)
|
||||
if not serialization._is_torch_tensor(value):
|
||||
return value
|
||||
rank = value.dim()
|
||||
|
||||
+142
-2
@@ -465,9 +465,44 @@ def decode_audio_envelope(envelope: dict[str, Any]) -> dict[str, Any]:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Video decode (encode lands when a remote node accepts VIDEO inputs)
|
||||
# Video encode / decode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def encode_video_input(video: Any) -> dict[str, Any]:
|
||||
"""Encode a ComfyUI VIDEO (``VideoInput`` subclass) as an mp4-base64
|
||||
video envelope.
|
||||
|
||||
Mirrors the AUDIO encoder above: re-encodes the Video object to an
|
||||
in-memory mp4/H.264 byte buffer (matches upstream
|
||||
``comfy_api_nodes/util/conversions.py`` ``video_to_base64_string``)
|
||||
and base64-encodes it. Populates ``duration_s`` metadata when the
|
||||
Video object exposes ``get_duration()`` so server-side providers
|
||||
(e.g. ``GrokVideoExtendProvider._validate_video_duration_envelope``)
|
||||
can range-check without demuxing the MP4.
|
||||
"""
|
||||
from comfy_api_nodes.util.conversions import video_to_base64_string
|
||||
|
||||
b64 = video_to_base64_string(video)
|
||||
extra: dict[str, Any] = {}
|
||||
duration_s: float | None = None
|
||||
try:
|
||||
getter = getattr(video, "get_duration", None)
|
||||
if callable(getter):
|
||||
d = getter()
|
||||
if d is not None:
|
||||
duration_s = float(d)
|
||||
except Exception:
|
||||
duration_s = None
|
||||
if duration_s is not None:
|
||||
extra["duration_s"] = duration_s
|
||||
return {
|
||||
"type": "video",
|
||||
"encoding": "mp4_base64",
|
||||
"data": b64,
|
||||
**extra,
|
||||
}
|
||||
|
||||
|
||||
def decode_video_envelope(envelope: dict[str, Any]) -> Any:
|
||||
"""Decode a video envelope into a ComfyUI Video object (mp4 inline)."""
|
||||
encoding = envelope.get("encoding")
|
||||
@@ -481,9 +516,69 @@ def decode_video_envelope(envelope: dict[str, Any]) -> Any:
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3D model decode (encode lands when a remote node accepts MODEL_3D inputs)
|
||||
# 3D model encode / decode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def encode_model3d_input(model_3d: Any) -> dict[str, Any]:
|
||||
"""Encode a ComfyUI ``File3D`` (or compatible) as an inline
|
||||
single-file 3D-model envelope.
|
||||
|
||||
Mirrors the AUDIO / VIDEO encoders above: reads the primary mesh
|
||||
bytes (via the ``File3D`` API: ``get_data().read()`` returns a
|
||||
``BytesIO`` payload; falling back to ``read_bytes()`` for older
|
||||
sources) and base64-encodes them. The envelope's ``encoding`` is
|
||||
``"glb_inline"`` (the generic single-file inline encoding the
|
||||
server-side decoder dispatches on at :func:`decode_model3d_envelope`
|
||||
— the decoder hands the bytes to ``File3D(BytesIO(...), format)``
|
||||
regardless of whether the actual format is glb / obj / fbx / etc.,
|
||||
so the ``encoding`` name is a logical "inline single-file 3D" tag
|
||||
and the ``format`` extra picks the file extension).
|
||||
|
||||
Multi-file bundles (``BundledFile3D``-style values where every
|
||||
companion file matters — e.g. an OBJ with a sibling .mtl + texture
|
||||
PNGs) are not yet supported on the encode path: this PR ships the
|
||||
minimal wire shape needed for the Tencent Hunyuan3D MODEL_3D-input
|
||||
nodes which all upload a single file via ``upload_3d_model_to_comfyapi``
|
||||
upstream. Bundle encode (``bundle_inline``) lands when a partner
|
||||
node actually needs the companion bytes.
|
||||
"""
|
||||
file_format = ""
|
||||
fmt_attr = getattr(model_3d, "format", None)
|
||||
if isinstance(fmt_attr, str):
|
||||
file_format = fmt_attr.lower()
|
||||
|
||||
# ``File3D.get_data()`` returns a ``BytesIO`` (per
|
||||
# ``comfy_api.latest._util.geometry_types.File3D``). Other File3D-
|
||||
# compatible classes may instead expose ``read_bytes`` returning
|
||||
# raw bytes; fall back to that for compatibility.
|
||||
raw: bytes
|
||||
get_data = getattr(model_3d, "get_data", None)
|
||||
if callable(get_data):
|
||||
buf = get_data()
|
||||
if hasattr(buf, "seek"):
|
||||
buf.seek(0)
|
||||
raw = buf.read() if hasattr(buf, "read") else bytes(buf)
|
||||
else:
|
||||
read_bytes = getattr(model_3d, "read_bytes", None)
|
||||
if callable(read_bytes):
|
||||
raw = read_bytes()
|
||||
else:
|
||||
raise RnpProtocolError(
|
||||
f"unsupported 3D model value: {type(model_3d).__name__!r} "
|
||||
f"has no get_data() or read_bytes()",
|
||||
code=ErrorCode.INTERNAL,
|
||||
)
|
||||
|
||||
envelope: dict[str, Any] = {
|
||||
"type": "model_3d",
|
||||
"encoding": "glb_inline",
|
||||
"data": base64.b64encode(raw).decode("ascii"),
|
||||
}
|
||||
if file_format:
|
||||
envelope["format"] = file_format
|
||||
return envelope
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Multi-file 3D bundle: a ``File3D``-compatible class that carries the
|
||||
# primary mesh plus N companion files (textures / .mtl / .bin / etc.).
|
||||
@@ -837,6 +932,47 @@ def is_audio_input(value: Any) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def is_video_input(value: Any) -> bool:
|
||||
"""Best-effort check for a ComfyUI ``VideoInput`` subclass.
|
||||
|
||||
Duck-types on the ``save_to`` + ``get_duration`` method pair: both
|
||||
are defined on the ``VideoInput`` abstract base in
|
||||
``comfy_api.latest._input`` and present on every concrete subclass
|
||||
(``VideoFromFile`` / ``VideoFromComponents``). Avoids importing
|
||||
``VideoInput`` at module load (which would pull torch via the
|
||||
``comfy_api`` tree).
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
return False
|
||||
return (
|
||||
callable(getattr(value, "save_to", None))
|
||||
and callable(getattr(value, "get_duration", None))
|
||||
)
|
||||
|
||||
|
||||
def is_model3d_input(value: Any) -> bool:
|
||||
"""Best-effort check for a ComfyUI ``File3D`` (or compatible).
|
||||
|
||||
Duck-types on the ``format`` string attribute + ``get_data``
|
||||
callable: both are present on the ``File3D`` class in
|
||||
``comfy_api.latest._util.geometry_types`` and the
|
||||
``BundledFile3D`` subclass built lazily in
|
||||
:func:`_bundled_file3d_class`. Avoids importing ``File3D`` at
|
||||
module load (which would pull torch via the ``comfy_api`` tree).
|
||||
|
||||
The ``VideoInput`` shape (``save_to`` + ``get_duration``) does not
|
||||
expose a ``format`` string property, so VIDEO and MODEL_3D values
|
||||
don't collide; caller (:func:`_encode_one`) still dispatches
|
||||
AUDIO / VIDEO before MODEL_3D as defense-in-depth.
|
||||
"""
|
||||
if isinstance(value, dict):
|
||||
return False
|
||||
return (
|
||||
isinstance(getattr(value, "format", None), str)
|
||||
and callable(getattr(value, "get_data", None))
|
||||
)
|
||||
|
||||
|
||||
def _is_torch_tensor(value: Any) -> bool:
|
||||
try:
|
||||
import torch
|
||||
@@ -853,7 +989,9 @@ __all__ = [
|
||||
"decode_mask_envelope",
|
||||
"encode_audio_input",
|
||||
"decode_audio_envelope",
|
||||
"encode_video_input",
|
||||
"decode_video_envelope",
|
||||
"encode_model3d_input",
|
||||
"decode_model3d_envelope",
|
||||
"_bundled_file3d_class",
|
||||
"decode_envelope",
|
||||
@@ -861,4 +999,6 @@ __all__ = [
|
||||
"is_image_tensor",
|
||||
"is_mask_tensor",
|
||||
"is_audio_input",
|
||||
"is_video_input",
|
||||
"is_model3d_input",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user