Files

283 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Standalone `comfy-media-v1` endpoint bridge
This directory is independently installable. It registers two internal Comfy V3
nodes, `MediaImportV1` and `MediaExportV1`, and provides the asynchronous HTTP
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.
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
1. Copy this complete directory to `<ComfyUI>/custom_nodes/serverless_bridge`.
Use a compatible ComfyUI with `comfy_api.v0_0_2` and the native VIDEO classes
used by [video_codec.py](video_codec.py). These exact-class/private-state
adapters are tied to the supported core implementation, not arbitrary VIDEO
plugins or future core layouts.
2. Restart ComfyUI so `comfy_entrypoint()` registers the two nodes. Keep the
workflow's own models and custom nodes installed separately.
3. Make `<ComfyUI>/custom_nodes` importable by the handler process (for example,
via its `PYTHONPATH`). The handler uses `aiohttp`; the nodes/codecs use
ComfyUI's PyTorch/native media dependencies. The handler module itself does
not import ComfyUI, PyTorch, Runpod, or Distributed bootstrap code.
4. Give the handler filesystem access to **the same input directory** used by
that ComfyUI instance, plus its trusted HTTP URL. These are operator settings,
never request-controlled paths/URLs. Staging/cleanup requires POSIX directory
descriptor and no-follow file operations.
Installing the nodes alone is insufficient: the endpoint must route media-v1
inputs through this executor and implement the uncertainty policy below. A
legacy image handler cannot interpret the typed envelope.
## Callable integration
```python
from serverless_bridge.handler import execute_media_request
async def execute_endpoint_input(
input_data, *, comfy_url, input_dir, temp_dir=None,
download=None, publish=None,
):
# Invoke only under a supervisor implementing the uncertainty policy below.
# Exceptions intentionally propagate; this is NOT a provider worker loop.
return await execute_media_request(
input_data,
comfy_url=comfy_url,
input_dir=input_dir,
timeout_seconds=1800,
session=None,
download=download,
publish=publish,
temp_dir=temp_dir,
)
```
Pass the provider job's **`input` object**, not the whole job. Return the resulting
`{"media": ...}` object as provider output; Runpod exposes it as `output.media`.
Do not add another `output` wrapper inside it. `timeout_seconds` must be finite
and positive. If supplied, `session` is a caller-owned `aiohttp.ClientSession`
for local Comfy HTTP and remains open; otherwise the executor creates/closes its
own session. Input dictionaries are copied before graph rewriting.
**This callable is not a turnkey Runpod handler.** An outer SDK that catches every
exception and immediately reuses the worker is unsafe. The supervisor must
recognize `serverless_bridge.handler.MediaExecutionUncertain`, stop intake, and
quarantine/reconcile or terminate the owning environment before reuse. Merely
returning `{"error": ...}` or marking a provider job `FAILED` is not that policy.
## Request and response
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
import uuid
def make_text_request(text="hello"):
request_id = uuid.uuid4().hex
binding = {
"binding_id": "text-binding",
"collector_id": "collector-1",
"job_id": "collector-job-1",
"participant_id": "serverless:example:0",
"node_id": "export-1",
"socket": "texts",
"kind": "STRING",
}
return {
"workflow": {
"text-1": {"class_type": "PrimitiveString", "inputs": {"value": text}},
"export-1": {
"class_type": "MediaExportV1",
"inputs": {
"request_id": request_id,
"binding_json": json.dumps(binding),
"texts": ["text-1", 0],
},
},
},
"images": [],
"media": {
"version": 1,
"profile": "comfy-media-v1",
"request_id": request_id,
"artifacts": [],
"inputs": [],
"outputs": [binding],
},
}
```
`workflow` is an API graph, not an editor workflow. Top-level input fields are
exactly `workflow`, `media`, and optional `images`. `media` has exactly the six
fields shown above. `media.outputs` declares one exporter per connected socket;
its binding must match that node's `binding_json` and request ID exactly. Socket
names/types are `images/IMAGE`, `audio/AUDIO`, `masks/MASK`, `texts/STRING`, and
`video/VIDEO`. Use unique request IDs. Identifiers are 1–200 characters from
`[A-Za-z0-9_.:/-]`; request scopes additionally reject empty, `.` or `..` slash
components. The executor validates the graph and submits **once**, with exactly
the declared exporter IDs as `partial_execution_targets`. Unrelated graph roots,
cycles, undeclared exporters, or caller-supplied `MediaImportV1` nodes fail.
For typed inputs, `media.artifacts` contains records with exactly:
| Field | Value |
| --- | --- |
| `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 |
Each `media.inputs` entry is
`{"node_id": "consumer", "input_name": "socket", "artifact_id": "artifact-id"}`.
It replaces an existing consumer input with a generated import link; every
artifact must be used, and destinations cannot repeat. The executor verifies
bytes/checksums, stages owned files, inserts imports, then prunes disconnected
loader ancestors. Optional legacy `images` entries are `{ "name": "input.png",
"image": "<base64>" }`, must match stock `LoadImage` references, and are rewritten
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 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.
All identity, completeness, type, size, and checksum checks must pass; do not
consume a partial result. The internal history transport `comfy-file` is local
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. 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.
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 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
PyAV codec must be available; missing encoders fail without automatic fallback.
Compressed IMAGE artifacts use `image-transfer-v1`; decode through the shared
`codec.decode_payload`, which enforces payload and decoded-shape limits.
See [image transfer choices](../docs/serverless-media.md#image-return-format-and-quality).
## Transport budgets and ownership
- Contract maxima: 256 MiB per artifact, 512 MiB aggregate artifact bytes,
128 artifacts, 64 items per binding. Request input totals include decoded
legacy-image bytes. Decoders validate tensor/VIDEO sizes and layouts before
allocating native storage; these are payload limits, **not peak-RSS guarantees**.
- The built-in master currently emits inline inputs only. Its complete Runpod
request JSON is capped at **10,000,000 bytes**, including base64, graph and
policy. The larger contract limits do not bypass this cap; there is no
built-in large-input/object-storage uploader.
- The executor's result JSON is capped at **10,000,000 bytes**. Inline results
are the default. When needed, `publish` is called for selected larger
artifacts to fit that budget; supplying it does not force all results to URLs.
Without a publisher, an oversized response fails instead of being truncated.
- Optional **async** `download(url, *, max_bytes)` returns `bytes`. The executor
supplies the declared artifact length and checks exact length and SHA-256.
The callback must enforce public HTTPS, DNS/IP restrictions, redirect checks,
and bounded streaming **before allocation**; syntactic contract validation
does not provide SSRF protection. No downloader is installed by default.
- Optional **async** `publish(metadata, payload)` uploads the exact `bytes` and
returns exactly `{"type": "url", "url": "https://...", "expires_at": "..."}`.
`metadata` is the artifact record without `transport`. Expiry must be a future,
timezone-aware ISO timestamp; URLs use HTTPS port 443, no userinfo/fragment,
and at most 8192 characters. Keep objects available long enough for collection.
The callback/operator owns upload credentials, object expiry and cleanup,
including orphan uploads after failure. There is no bridge storage backend.
- The master uses a separate bounded public-HTTPS downloader without provider
credentials. Never send provider credentials to artifact hosts or log signed
URLs. Endpoint callbacks must enforce equivalent protections themselves.
Callbacks must cooperate with async cancellation, finish/abort their own I/O,
and not detach readers/uploads that outlive the callback. Synchronous code that
blocks the event loop cannot be hard-timed-out by this executor.
## Cancellation, quarantine, and file retention
`timeout_seconds` covers staging, submission, polling, downloads and publication.
On failure/cancellation, there is a separate **10-second callback-drain grace**
and, when needed, a **10-second prompt-reconciliation grace**. These are recovery
allowances, not evidence that execution stopped. Ambiguous `/prompt` submission
is never retried. An identified pending prompt may be deleted from `/queue`,
with exact read-back; running prompts are never sent `/interrupt`. The pinned
core route has a snapshot/global-interrupt race even when given a prompt ID.
`MediaExecutionUncertain` means prompt acceptance/termination **or callback
teardown** is unconfirmed. Even successful terminal Comfy history is insufficient
if an async downloader/publisher ignores cancellation. The bridge retains input
and exporter files in that case; late callback exit does not automatically
remove retained files. Before accepting another job on that environment:
1. Quarantine it and stop new intake; do not automatically resubmit the request.
2. Reconcile the local prompt and all readers, or terminate the owning execution
environment when their termination cannot be established.
3. Only then clean retained files and allow reuse/replacement. A provider terminal
job status alone cannot certify a surviving local Comfy process or callback.
Staged inputs use random exclusive files under
`input_dir/distributed-media-v1/<sha256(request_id)>/`. Cleanup removes only files
created by that invocation, after confirmed terminal state (or before submission)
and reader teardown; it never recursively deletes a shared namespace.
`temp_dir=None` leaves exporter retention to the Comfy temp-directory/environment
lifecycle. Optionally pass the **trusted local Comfy temp root** to enable
best-effort cleanup of validated, invocation-owned exporter UUID `.bin` files
under the corresponding hashed namespace, only after terminal history and
collection/publisher teardown. Symlinked, hard-linked, replaced, missing, or
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.
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.