Jedrzej KosinskiandAmp c962fa5adf Add client-side multi-file 3D bundle decoder + BundledFile3D
Closes the client side of the multi-file 3D bundle envelope landed in
comfy-rnp-server#38 (commit 50f9f4f on main).

* Vendored `protocol.py`: mirrors the new
  `Capability.MODEL_3D_BUNDLE_INLINE = "model_3d:bundle_inline"`
  constant with a full docstring describing the wire shape, path /
  role semantics, primary_path / format invariants, capability
  negotiation discipline, and the planned future
  `MODEL_3D_BUNDLE_URI` sibling. HEAVY_TYPES is unchanged
  (`type="model_3d"` already covers the new encoding).

* `serialization.py`: adds `BundledFile3D` as a `File3D` subclass
  built lazily via a `_bundled_file3d_class()` factory (so importing
  the heavy `comfy_api.latest._util` package doesn't happen at
  module load — keeps the smoke-test stubbing pattern intact for
  callers that don't exercise the bundle path). The class stores
  the primary mesh file plus N companion files in memory as a
  `path -> bytes` dict and:

  - `get_data()` / `get_bytes()` return the primary file's bytes
    without touching disk.
  - `get_source()` lazily materialises the whole bundle into a temp
    directory using the producer-supplied relative paths, then
    returns the on-disk path to the primary file — so a .obj that
    references its .mtl by filename (and the .mtl that references
    `textures/albedo.png`) resolves via the filesystem.
  - `save_to(dest)` copies the WHOLE bundle into the destination
    directory: the primary file under the caller-specified name,
    sidecars under their ORIGINAL relative paths so cross-file refs
    inside the .obj that point at `model.mtl` still resolve when
    the user saves the model somewhere stable.
  - New convenience accessors `primary_path`, `file_paths`,
    `file_bytes(path)`, `file_role(path)`, `paths_with_role(role)`,
    and `materialize_to_temp_dir()`.
  - Temp directory cleanup on garbage collect (best-effort
    `shutil.rmtree(td, ignore_errors=True)` in `__del__`).
  - `super().__init__(BytesIO(primary), primary_fmt)` keeps the
    upstream `_source` / `_format` attributes populated so existing
    downstream consumers that read those plain attributes still
    work.

  `decode_model3d_envelope` now dispatches on `encoding`:
  `glb_inline` falls through to the existing single-file path
  unchanged; `bundle_inline` walks the new wire shape, decodes
  per-file base64 `data`, hoists `role` / `format` hints into
  parallel dicts, and constructs a `BundledFile3D`. Malformed
  envelopes (missing `primary_path`, missing `files`, primary not
  present in files, file entry missing `data`) raise
  `RnpProtocolError(code=INTERNAL)` rather than falling through to
  any opaque bucket.

* `client.py`: advertises `Capability.MODEL_3D_BUNDLE_INLINE` in
  `CLIENT_CAPABILITIES` with a full docstring matching the
  established pattern for the other 3D capabilities.

Design decisions (from oracle consult on wire-shape surface):

* Extend the existing `model_3d` heavy type with a new encoding
  (rather than introducing `model_3d_bundle` as a separate
  top-level type) — keeps HEAVY_TYPES unchanged, lets the
  single-file `glb_inline` decoder stay untouched, and lets
  capability gating stay parallel.

* `path` is the authoritative cross-file reference key (not
  `role`) — roles are conceptually non-unique (multiple meshes,
  LODs, buffers, animations) so `primary_path` pins the entry
  point unambiguously.

* `role` is an OPTIONAL producer hint, NOT a closed enum. Unknown
  roles round-trip without error.

* Subclass `File3D` directly rather than introducing a new opaque
  `ModelBundle3D` type — existing downstream loaders that
  type-check or duck-type-check on `File3D` keep working.
  `isinstance(bundled, File3D)` succeeds.

* Each file's `data` is base64 (mirrors the single-file
  `glb_inline` payload mode). Mixed `data`/`uri` inside one
  encoding was rejected per the parallel-encoding pattern already
  in use across the rest of the protocol; a future sibling
  `Capability.MODEL_3D_BUNDLE_URI` would handle URL-fetch
  semantics.

* Capability gating with whole-descriptor enforcement — if a node
  has both bundle and non-bundle outputs and the cap is absent,
  the whole descriptor is skipped rather than synthesising a
  partial-socket variant.

Smoke test at `notes/run_model3d_bundle_decoder.py` — 9 checks:

1. Capability constant intact at protocol layer.
2. is_envelope recognises bundle_inline envelopes.
3. decode dispatch returns BundledFile3D (a File3D subclass) +
   primary bytes round-trip via get_data / get_bytes.
4. get_source materialises whole bundle on disk at producer-
   supplied paths.
5. save_to copies whole bundle: primary renamed, sidecars keep
   filenames.
6. Bundle accessors (primary_path / file_paths / file_role /
   paths_with_role) work; unknown roles round-trip.
7. Malformed bundle envelopes raise RnpProtocolError(INTERNAL).
8. End-to-end against server's make_model3d_bundle_envelope helper
   (from PR #38) — all files / roles / bytes preserve. Server-side
   path sanitization rejects `..` at envelope build time
   (defence-in-depth).
9. CLIENT_CAPABILITIES advertises model_3d:bundle_inline.

All 9 pass; PR #1's `notes/run_model3d_decoder.py` (8 checks) and
PR #2's `notes/run_dynamic_combo_parser.py` (8 checks) also still
pass — no regression on existing paths.

Hunyuan3D / Tencent / future Meshy / Rodin / Tripo bundle
providers land in a follow-up PR that consumes this decoder; this
PR is protocol + client decoder only per the planned sequence.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019e331c-c202-746b-96ec-86ce0b9c54a5
2026-05-16 21:15:29 -07:00
2026-05-11 12:27:26 -07:00

comfy-remote-nodes

ComfyUI custom-node package that fetches node descriptors from an RNP/1 (Remote Node Protocol v1) server and exposes them as native V3 nodes inside ComfyUI.

The reference server lives at Comfy-Org/comfy-rnp-server (private). This client is intentionally provider-agnostic: any server that speaks RNP/1 can publish nodes through it.

Install

Clone into your ComfyUI custom_nodes/ directory:

cd ComfyUI/custom_nodes
git clone https://github.com/Comfy-Org/comfy-remote-nodes.git

(The directory name on disk doesn't matter — ComfyUI loads the package via spec_from_file_location, so comfy-remote-nodes/ is fine.)

Configure

Point the extension at one RNP server URL via env var before starting ComfyUI:

export RNP_SERVER_URL=http://127.0.0.1:9190
python main.py

If RNP_SERVER_URL is unset, the extension registers zero nodes and logs a single line — ComfyUI starts normally with no remote nodes.

What works today

  • Manifest + capability handshake
  • object_info fetch and dynamic V3 class generation (with ETag conditional GETs)
  • Sync execute and async execute_async + poll/cancel
  • Image / mask / audio / video envelope encode + decode
  • Presigned upload helper for >8 MiB inputs
  • Tier 3 (recent):
    • TASK_LOST detection with idempotent resubmission — survives an RNP server restart mid-task without losing the workflow
    • SERVER_BUSY / MAINTENANCE 503 envelopes treated as rate limits with Retry-After honoring and friendly per-second progress text ("Server busy, retrying in 30s...")
    • Out-of-band url_fetch for URI-only output envelopes, driven by the descriptor's remote.url_fetch policy
    • Per-poll auth refresh so long-running tasks can rotate tokens

What does not (yet)

  • validate_inputs / fingerprint_inputs / check_lazy_status — the server returns 501 NOT_IMPLEMENTED; the client falls through to ComfyUI's defaults
  • Multi-server config file (comfy_remote_nodes.yaml or similar) — today's setup is one server per ComfyUI process via env var
  • Streaming execute (server-sent events for incremental previews)
S
Description
No description provided
Readme GPL-3.0
243 KiB
Languages
Python 100%