feat: integrate native image-list caching and exact serverless value overrides

This commit is contained in:
Robert Wojciechowski
2026-10-14 04:08:48 +00:00
55 changed files with 4414 additions and 1112 deletions
+44 -13
View File
@@ -6,8 +6,12 @@ executor `execute_media_request`. It does **not** start ComfyUI, provision an
endpoint, install models, or provide a provider worker/supervisor. The endpoint
needs this bridge, not the full ComfyUI-Distributed extension.
For collector setup, list ordering, supported native VIDEO states, and PR2
limitations, see [typed serverless media](../docs/serverless-media.md).
The bridge's generic contract supports `IMAGE`, `AUDIO`, `MASK`, `STRING`, and
native ComfyUI `VIDEO`. **Distributed Collector** (`DistributedCollector`) uses
only the image/audio outputs and combines their returned lists into ordinary
IMAGE/AUDIO outputs. It has no MASK/STRING/VIDEO outputs, and the separate
`DistributedMediaCollector` node has been removed. For Collector setup, ordering,
and limits, see [serverless images and audio](../docs/serverless-media.md).
## Install in the endpoint image
@@ -70,7 +74,8 @@ returning `{"error": ...}` or marking a provider job `FAILED` is not that policy
## Request and response
A minimal text request builder (no files or model required by this graph):
A minimal generic bridge text request builder (no files or model required by this
graph). This illustrates the bridge contract, not a Collector text output:
```python
import json
@@ -127,7 +132,7 @@ For typed inputs, `media.artifacts` contains records with exactly:
| Field | Value |
| --- | --- |
| `id`, `kind` | Unique artifact ID and one of the five native types |
| `id`, `kind` | Unique artifact ID and a native media type or internal `VALUE` |
| `descriptor` | Kind-specific codec metadata; use the shared codec, not pickle |
| `byte_length`, `sha256` | Exact payload byte count and lowercase SHA-256 hex |
| `transport` | `{"type": "inline", "data_base64": "..."}` or the URL transport below |
@@ -144,7 +149,7 @@ to owned staging paths. They are not an arbitrary filename-upload interface.
The returned `media` object has exactly `version`, `profile`, `request_id`,
`bindings`, `artifacts`, and `items`:
- `bindings`: every declared binding, unchanged, plus `count` (1–64).
- `bindings`: every declared binding, unchanged, plus `count` (1–64 for media; exactly 1 for End value overrides).
- `artifacts`: the same artifact schema above with inline or URL transport.
- `items`: `{ "binding_id": "...", "item_index": 0, "artifact_id": "..." }`
records covering every index exactly once, ordered by `item_index` when decoded.
@@ -155,20 +160,46 @@ only and must not be returned to the master. The executor reads validated
`media_exports` history records and bounded `/view` payloads before constructing
the external envelope. [contract.py](contract.py) is the schema authority.
## Internal image-list values
Image-list End uses sparse `value_1` … `value_10` bindings of kind `VALUE`.
These are internal exports, not new Collector sockets. `value-v1` descriptors
preserve BOOLEAN, arbitrary-size INT, finite binary64 FLOAT, STRING, IMAGE and
MASK distinctly; tensors/text reuse exact native codecs. Each binding returns
exactly one value. Only explicit End overrides are exported; the master rebuilds
unchanged values from its source items. Compression of the main IMAGE never
changes value-tensor fidelity.
Deploy the complete updated bridge, including `value_codec.py`, before using
End overrides. `MediaImportV1` retains its original five output positions and
appends the internal wildcard VALUE output at slot 5. Older bridge installations
do not support these bindings; ordinary provider health checks do not verify
this support. Request/result size limits still apply.
## Optional image return encoding
IMAGE `MediaExportV1` nodes may also contain literal `transfer_format` and
`transfer_quality` controls. Defaults are `exact` and `90`; old exact requests
omit the controls and retain their original representation. Other media exports
must not carry image controls. The master compiler supplies them from the typed
Collector's widgets; callers should not alter binding identity metadata.
`transfer_quality` controls. The **bridge-level defaults** are `exact` and `90`;
generic callers that omit the controls retain that representation. Other media
exports must not carry image controls. Callers should not alter binding identity
metadata.
Formats: `exact`, `legacy_png`, `png`, `ffv1`, `jpeg`, `webp`, `h264`, `h265`, `av1`.
The **Collector default is different**: its saved widget value remains
`legacy_png` with quality `90`. For a `comfy-media-v1` pool, the master compiler
maps that value to an explicit `png` export, so normal serverless image returns
use compressed 8-bit PNG. `exact` is opt-in at the Collector. Other selected
formats are unchanged. Direct workers also support opt-in exact IMAGE transport;
an older master rejection is an error, not an 8-bit retry. A Collector requesting
`exact` cannot dispatch to a `legacy-image-v1` pool.
Formats: `exact`, `legacy_png`, `png`, `ffv1`, `jpeg`, `webp`, `h264`,
`h265` (**experimental**), `av1`.
`exact` preserves float tensor bits. The image/video codecs quantize RGB/grayscale
to 8-bit; PNG/FFV1 are lossless at that precision, while the other codecs are
lossy. Quality is an integer 1–100 and affects only lossy formats. IMAGE batch
order, shape and channel count are retained, and alpha is not discarded.
Native VIDEO/MASK/AUDIO/STRING still use their original exact codecs.
Native VIDEO/MASK/AUDIO/STRING bridge values still use their original exact codecs;
the image-format controls do not add those sockets to the Collector.
Copy/update the complete bridge directory on both sides, including
`image_transfer.py` and `image_codec.py`. Pillow and the selected CPU/software
@@ -247,5 +278,5 @@ unidentifiable files are not claimed; directories and all unidentified leftovers
remain lifecycle-owned. This is not a temp-directory sweep. Independently decoded
VIDEO objects do not depend on either staging or exporter files.
Verification scope: source and offline tests. No live provider/GPU validation is
claimed, and this directory is not a tested turnkey provider deployment.
This guide describes the source contract, not a test result. No live provider/GPU
validation is claimed, and this directory is not a turnkey provider deployment.
+7 -1
View File
@@ -24,7 +24,7 @@ _LAYOUTS = {"IMAGE": "BHWC", "MASK": "BHW", "AUDIO": "BCT"}
def _validate_kind(kind):
if type(kind) is not str or kind not in ("IMAGE", "MASK", "STRING", "AUDIO"):
if type(kind) is not str or kind not in ("IMAGE", "MASK", "STRING", "AUDIO", "VALUE"):
raise ValueError("Unsupported payload kind")
@@ -92,6 +92,9 @@ def encode_payload(kind: str, value, *, max_bytes: int = MAX_PAYLOAD_BYTES) -> t
"""
_validate_kind(kind)
limit = _byte_limit(max_bytes)
if kind == "VALUE":
from .value_codec import encode_value
return encode_value(value, max_bytes=limit)
if kind == "STRING":
return _encode_string(value, limit)
@@ -146,6 +149,9 @@ def decode_payload(kind: str, descriptor: dict, payload: bytes, *, max_bytes: in
raise ValueError("Payload must be bytes")
if len(payload) > limit:
raise ValueError("Payload exceeds byte limit")
if kind == "VALUE":
from .value_codec import decode_value
return decode_value(descriptor, payload, max_bytes=limit)
if kind == "IMAGE" and descriptor.get("encoding") == "image-transfer-v1":
# Lazy and IMAGE-only: exact tensors and other kinds stay unchanged.
from .image_codec import decode_image
+8 -3
View File
@@ -19,6 +19,9 @@ MAX_ARTIFACTS = 128
MAX_ITEMS = 64
MAX_DESCRIPTOR_BYTES = 128 * 1024
SOCKET_KINDS = {"images": "IMAGE", "audio": "AUDIO", "masks": "MASK", "texts": "STRING", "video": "VIDEO"}
# Internal End slots do not extend the public Collector or legacy media sockets.
VALUE_SOCKETS = tuple(f"value_{i}" for i in range(1, 11))
ARTIFACT_KINDS = frozenset(SOCKET_KINDS.values()) | {"VALUE"}
BINDING_FIELDS = {"binding_id", "collector_id", "job_id", "participant_id", "node_id", "socket", "kind"}
_ID = re.compile(r"[A-Za-z0-9_.:/-]{1,200}\Z")
_HASH = re.compile(r"[a-f0-9]{64}\Z")
@@ -68,7 +71,8 @@ def validate_binding(binding):
raise ValueError("Invalid media binding")
for field in BINDING_FIELDS - {"socket", "kind"}:
identifier(binding[field])
if SOCKET_KINDS.get(binding["socket"]) != binding["kind"]:
kind = "VALUE" if binding["socket"] in VALUE_SOCKETS else SOCKET_KINDS.get(binding["socket"])
if kind is None or kind != binding["kind"]:
raise ValueError("Media socket/type mismatch")
return binding
@@ -113,7 +117,7 @@ def validate_artifact(artifact, *, max_bytes=MAX_ARTIFACT_BYTES):
if not isinstance(artifact, dict) or set(artifact) != fields:
raise ValueError("Invalid media artifact")
identifier(artifact["id"])
if artifact["kind"] not in SOCKET_KINDS.values():
if artifact["kind"] not in ARTIFACT_KINDS:
raise ValueError("Unsupported media kind")
if not isinstance(artifact["descriptor"], dict):
raise ValueError("Invalid media descriptor")
@@ -191,7 +195,8 @@ def validate_response(response, *, request_id, bindings, max_total_bytes=MAX_TOT
if bid in counts or expected.get(bid) != binding:
raise ValueError("Media binding identity mismatch")
# Connected outputs must produce at least one value. Empty STRING is a value.
counts[bid] = bounded_int(row["count"], MAX_ITEMS, minimum=1)
maximum = 1 if binding["socket"] in VALUE_SOCKETS else MAX_ITEMS
counts[bid] = bounded_int(row["count"], maximum, minimum=1)
artifacts = response["artifacts"]
if not isinstance(artifacts, list) or not 1 <= len(artifacts) <= MAX_ARTIFACTS:
raise ValueError("Invalid media artifacts count")
+7 -3
View File
@@ -40,7 +40,7 @@ DRAIN_GRACE_SECONDS = 10.0
HTTP_TIMEOUT_SECONDS = 15.0
_UNCERTAIN_WORK = set()
GET_ATTEMPTS = 3
_IMPORT_SLOTS = {"IMAGE": 0, "AUDIO": 1, "MASK": 2, "STRING": 3, "VIDEO": 4}
_IMPORT_SLOTS = {"IMAGE": 0, "AUDIO": 1, "MASK": 2, "STRING": 3, "VIDEO": 4, "VALUE": 5}
# Used ONLY to run the shared manifest validator before /view. Never fetched.
_VALIDATION_TRANSPORT = {"type": "url", "url": "https://media-validation.invalid/artifact",
"expires_at": "9999-01-01T00:00:00Z"}
@@ -194,8 +194,12 @@ def _validate_request(input_data):
or aid not in indexed or (node_id, name) in destinations):
raise ValueError("Unknown or duplicate media input reference")
binding = exporters.get(node_id)
if binding is not None and (name != binding["socket"] or indexed[aid]["kind"] != binding["kind"]):
raise ValueError("Exporter input must match its declared media socket and kind")
if binding is not None:
# VALUE exports accept the six explicit runtime types. IMAGE/MASK
# imports can feed both native consumers and a sparse override.
allowed = {"VALUE", "IMAGE", "MASK", "STRING"} if binding["kind"] == "VALUE" else {binding["kind"]}
if name != binding["socket"] or indexed[aid]["kind"] not in allowed:
raise ValueError("Exporter input must match its declared media socket and kind")
destinations.add((node_id, name))
used.add(aid)
if used != set(indexed):
+15 -6
View File
@@ -11,7 +11,7 @@ from .codec import decode_payload, encode_payload
from .image_transfer import (FORMATS, DEFAULT_FORMAT, DEFAULT_QUALITY,
FORMAT_TOOLTIP, QUALITY_TOOLTIP, validate_options)
from .contract import (
MAX_ARTIFACT_BYTES, MAX_ITEMS, MAX_TOTAL_BYTES, SOCKET_KINDS,
MAX_ARTIFACT_BYTES, MAX_ITEMS, MAX_TOTAL_BYTES, SOCKET_KINDS, VALUE_SOCKETS, ARTIFACT_KINDS,
bounded_int, identifier, validate_binding, validate_json,
)
@@ -38,6 +38,9 @@ def _json(text):
def _cpu(kind, value):
# Runtime transfer only, never sampling or device discovery. Pure codecs
# deliberately reject non-CPU storage rather than making hidden transfers.
if kind == "VALUE":
from .value_codec import value_kind
kind = value_kind(value)
if kind in ("IMAGE", "MASK"):
return value.detach().cpu()
if kind == "AUDIO":
@@ -72,6 +75,8 @@ def export_values(request_id, binding, values, *, transfer_format=DEFAULT_FORMAT
raise ValueError("Image transfer controls apply only to IMAGE exports")
if not isinstance(values, list) or not 1 <= len(values) <= MAX_ITEMS:
raise ValueError("Connected media output has no values or exceeds item limit")
if binding["socket"] in VALUE_SOCKETS and len(values) != 1:
raise ValueError("End override requires exactly one value")
request_key = hashlib.sha256(request_id.encode()).hexdigest()
subfolder = f"distributed-media-v1/{request_key}"
root = Path(folder_paths.get_temp_directory()).resolve()
@@ -114,7 +119,7 @@ def import_value(artifact, filename):
if not isinstance(artifact, dict) or set(artifact) != fields:
raise ValueError("Invalid staged media metadata")
identifier(artifact["id"])
if artifact["kind"] not in SOCKET_KINDS.values():
if artifact["kind"] not in ARTIFACT_KINDS:
raise ValueError("Invalid staged media kind")
size = bounded_int(artifact["byte_length"], MAX_ARTIFACT_BYTES)
if not isinstance(filename, str):
@@ -138,13 +143,14 @@ class MediaImportV1(io.ComfyNode):
def define_schema(cls):
return io.Schema(node_id="MediaImportV1", category="distributed/internal", is_deprecated=True,
inputs=[io.String.Input("artifact_json"), io.String.Input("filename")],
outputs=[io.Image.Output(), io.Audio.Output(), io.Mask.Output(), io.String.Output(), io.Video.Output()])
outputs=[io.Image.Output(), io.Audio.Output(), io.Mask.Output(), io.String.Output(), io.Video.Output(),
io.AnyType.Output()])
@classmethod
def execute(cls, artifact_json, filename):
artifact = _json(artifact_json)
value = import_value(artifact, filename)
return io.NodeOutput(*(value if kind == artifact["kind"] else None for kind in SOCKET_KINDS.values()))
return io.NodeOutput(*(value if kind == artifact["kind"] else None for kind in (*SOCKET_KINDS.values(), "VALUE")))
class MediaExportV1(io.ComfyNode):
@@ -155,6 +161,7 @@ class MediaExportV1(io.ComfyNode):
io.Image.Input("images", optional=True), io.Audio.Input("audio", optional=True),
io.Mask.Input("masks", optional=True), io.String.Input("texts", optional=True, force_input=True),
io.Video.Input("video", optional=True),
*[io.AnyType.Input(name, optional=True) for name in VALUE_SOCKETS],
io.Combo.Input("transfer_format", options=list(FORMATS), default=DEFAULT_FORMAT,
optional=True, tooltip=FORMAT_TOOLTIP),
io.Int.Input("transfer_quality", default=DEFAULT_QUALITY, min=1, max=100,
@@ -163,10 +170,12 @@ class MediaExportV1(io.ComfyNode):
@classmethod
def execute(cls, request_id, binding_json, images=None, audio=None, masks=None, texts=None, video=None,
transfer_format=DEFAULT_FORMAT, transfer_quality=DEFAULT_QUALITY):
transfer_format=DEFAULT_FORMAT, transfer_quality=DEFAULT_QUALITY, **overrides):
binding = _json(_scalar(binding_json))
validate_binding(binding)
provided = dict(images=images, audio=audio, masks=masks, texts=texts, video=video)
if set(overrides) - set(VALUE_SOCKETS):
raise ValueError("Unknown End value slot")
provided = dict(images=images, audio=audio, masks=masks, texts=texts, video=video, **overrides)
if {name for name, value in provided.items() if value is not None} != {binding["socket"]}:
raise ValueError("Media export must receive exactly its declared typed socket")
exported = export_values(_scalar(request_id), binding, provided[binding["socket"]],
+85
View File
@@ -0,0 +1,85 @@
"""Exact six-type values for internal sparse End exports; no pack imports.
Tensor/text payloads reuse the existing media codec. Scalars have distinct
encodings: BOOLEAN is one byte, INT is minimal signed little-endian, FLOAT is
IEEE binary64. No scalar passes through float() or a lossy JSON number parser.
"""
import math
import struct
import torch
KINDS = frozenset(("IMAGE", "MASK", "STRING", "BOOLEAN", "INT", "FLOAT"))
_SCALAR_ENCODINGS = {"BOOLEAN": "bool-v1", "INT": "int-le-v1", "FLOAT": "float64-le-v1"}
def value_kind(value):
if isinstance(value, torch.Tensor):
if value.ndim == 4 and value.shape[-1] in (3, 4):
return "IMAGE"
if value.ndim == 3:
return "MASK"
raise ValueError("VALUE tensor requires IMAGE [B,H,W,3|4] or MASK [B,H,W]")
kind = {bool: "BOOLEAN", int: "INT", float: "FLOAT", str: "STRING"}.get(type(value))
if kind is None:
raise ValueError("VALUE requires IMAGE, MASK, STRING, INT, FLOAT or BOOLEAN")
if kind == "FLOAT" and not math.isfinite(value):
raise ValueError("VALUE numbers must be finite")
return kind
def encode_value(value, *, max_bytes):
from .codec import encode_payload
kind = value_kind(value)
if kind in ("IMAGE", "MASK", "STRING"):
descriptor, payload = encode_payload(kind, value, max_bytes=max_bytes)
else:
descriptor = {"encoding": _SCALAR_ENCODINGS[kind]}
size = ((value if value >= 0 else ~value).bit_length() + 8) // 8 if kind == "INT" else (8 if kind == "FLOAT" else 1)
if size > max_bytes:
raise ValueError("VALUE payload exceeds byte limit")
if kind == "BOOLEAN":
payload = b"\x01" if value else b"\x00"
elif kind == "INT":
payload = value.to_bytes(size, "little", signed=True)
else:
payload = struct.pack("<d", value)
return {"encoding": "value-v1", "type": kind, "descriptor": descriptor}, payload
def decode_value(descriptor, payload, *, max_bytes):
from .codec import decode_payload
if (set(descriptor) != {"encoding", "type", "descriptor"}
or descriptor["encoding"] != "value-v1"
or type(descriptor["type"]) is not str or descriptor["type"] not in KINDS):
raise ValueError("Invalid VALUE descriptor")
kind, inner = descriptor["type"], descriptor["descriptor"]
if kind in ("IMAGE", "MASK", "STRING"):
# Generic values always stay exact even if End.images is compressed.
if not isinstance(inner, dict) or inner.get("encoding") != (
"utf8-v1" if kind == "STRING" else "tensor-le-v1"):
raise ValueError("VALUE requires exact typed payloads")
value = decode_payload(kind, inner, payload, max_bytes=max_bytes)
if value_kind(value) != kind:
raise ValueError("VALUE tensor/type mismatch")
return value
if inner != {"encoding": _SCALAR_ENCODINGS[kind]}:
raise ValueError("Invalid VALUE scalar encoding")
if kind == "BOOLEAN":
if payload not in (b"\x00", b"\x01"):
raise ValueError("Invalid BOOLEAN payload")
return payload == b"\x01"
if kind == "FLOAT":
if len(payload) != 8:
raise ValueError("Invalid FLOAT payload length")
value = struct.unpack("<d", payload)[0]
if not math.isfinite(value):
raise ValueError("VALUE numbers must be finite")
return value
if not payload:
raise ValueError("Invalid INT payload length")
value = int.from_bytes(payload, "little", signed=True)
if ((value if value >= 0 else ~value).bit_length() + 8) // 8 != len(payload):
raise ValueError("Invalid INT payload length")
return value