Skip to content

submitted by mistake#14583

Closed
ccssu wants to merge 1 commit into
Comfy-Org:masterfrom
siliconflow:master
Closed

submitted by mistake#14583
ccssu wants to merge 1 commit into
Comfy-Org:masterfrom
siliconflow:master

Conversation

@ccssu

@ccssu ccssu commented Jun 22, 2026

Copy link
Copy Markdown

This commit was submitted by mistake, please close this PR or disregard this commit.

* [Partner Nodes] feat: add Krea 2 Medium Turbo model (Comfy-Org#14280)

* [Partner Nodes] feat: add seed input to Flux Erase node (Comfy-Org#14283)

Signed-off-by: bigcat88 <bigcat88@icloud.com>

* chore: update workflow templates to v0.9.98 (Comfy-Org#14284)

* Bump comfyui-frontend-package to 1.45.15 (Comfy-Org#14265)

* Fix ideogram if model dtype gets set to fp8. (Comfy-Org#14291)

* Consolidate audio nodes into SaveAudioAdvanced node (CORE-202) (Comfy-Org#13871)

* Enable cfg1 optimization for DualModelGuider with CFGGuider (Comfy-Org#14290)

* Enable cfg1 optimization for DualModelGuider

* Fix CFG Override tooltip

* Fix interoperation with external source of pinned memory pressure (Comfy-Org#14252)

* mm: split off registration helper to doer and headroom calc

* pinned_memory: implement registration comfy side

Move away from Aimdo buffer registrations which seem fraught with
danger and do it comfy side. Just start with the basic move.

* pinned_memory: do registrations as portable memory

* pinned_memory: discard async errors on registration fail

Like the good ol days.

* pinned_memory: implement abs shortfall retry

If pinned registration happens to fail despite the previous budget
ensures, consider the allocation shortfall, ensure it again, and
try again. This allows comfy pins to interoperate with other software
that might be doing substantive pinning.

* aimdo 049 (Comfy-Org#14300)

* [Partner Nodes] feat: add new Gemini text node (Comfy-Org#14299)

* [Partner Nodes] feat: add temperature and top_p to NanoBanan node (Comfy-Org#14305)

* feat: add PreviewGaussianSplat + PreviewPointCloud nodes (Comfy-Org#14194)

* Update AMD portable readme. (Comfy-Org#14303)

* BE-1172 fix(3d): save Preview3DAdvanced / PreviewGaussianSplat / PreviewPointCloud to temp/, rename viewport input (Comfy-Org#14294)

* feat(3d): reorder Preview3DAdvanced / PreviewGaussianSplat / PreviewPointCloud inputs and outputs (Comfy-Org#14308)

* Update line endings check to ignore .ci files. (Comfy-Org#14319)

* Use windows line endings for windows portable readmes. (Comfy-Org#14334)

* Add SeedVR2 support (CORE-6) (Comfy-Org#14110)

* chore: update embedded docs to v0.5.3 (Comfy-Org#14350)

* Add Color primitive (Comfy-Org#14260)

* Improve ResolutionSelector (Comfy-Org#14309)

* feat(assets): extract image dimensions at ingest and emit on asset responses (Comfy-Org#13991)

* feat(assets): extract image dimensions at ingest and emit on asset responses

Image assets now carry width/height under the existing `metadata` field on
asset responses, shaped as `{"kind": "image", "width": W, "height": H}`.
This lets consumers get original dimensions (e.g. for clients that render
server-side thumbnails and can't recover them from naturalWidth/Height)
without an extra round-trip.

Dimensions are written to AssetReference.system_metadata across three
ingest paths:

- Direct file ingest (upload, in-place registration): Pillow reads the
  image header right after hashing, while the file is still in OS page
  cache. Non-image MIME types are skipped without touching the file.
- From-hash registration: this path never reads the file bytes, so
  dimensions are best-effort copied from any prior sibling reference of
  the same asset that already carries kind=image metadata. Missing
  siblings, non-image siblings, or absent dimension keys leave the new
  reference's metadata unchanged.
- Scanner enrichment: extends the existing system_metadata write in
  enrich_asset so scanner-registered images get the same treatment as
  uploaded ones.

Existing system_metadata keys (e.g. safetensors fields written by the
enricher, download provenance) are preserved through merge. Existing
assets ingested before this change retain their current metadata — no
automatic backfill in this PR.

Tests cover image emission, non-image no-op, merge preservation, and the
from-hash sibling back-fill (including the no-sibling and non-image-sibling
cases).

* fix(assets): validate sibling dimensions before backfilling

Per CodeRabbit review on Comfy-Org#13991: the previous loop accepted any sibling
with `kind == "image"` and copied whichever dimension keys happened to
be present, then returned. A partial sibling (kind set but missing or
invalid width/height) could persist incomplete metadata onto the new
reference even when a later sibling had valid dimensions.

Now we validate that the sibling has both width and height as positive
integers before adopting its dimensions, and continue scanning to the
next sibling otherwise.

* fix(assets): reject booleans in sibling dimension validation (use type-is)

Per CodeRabbit follow-up on Comfy-Org#13991: bool is a subclass of int in Python,
so isinstance(True, int) is True. The previous strict-int gate would
have accepted width=True (truthy + > 0) as a valid dimension.
Realistic occurrence is low (extract_image_dimensions returns proper
ints, JSON doesn't serialize bools as numbers), but the validation gate
exists for defense-in-depth so it should be actually strict.

---------

Co-authored-by: guill <jacob.e.segal@gmail.com>

* Revert "Add SeedVR2 support (CORE-6) (Comfy-Org#14110)" (Comfy-Org#14359)

This reverts commit 7863cf0.

* chore(openapi): sync shared API contract from cloud@5273c30 (Comfy-Org#14266)

* fix: Add back apply_rotary_emb for Qwen Image (Comfy-Org#14364)

* Allow custom templates with Ideogram4 TE (Comfy-Org#14374)

* main/server: Add --debug-hang (Comfy-Org#14371)

Add an option to debug a hang with ctrl-C, dumping the backtraces to
see where its stuck or slow.

* Add LoRA key mapping for LTXV/LTXAV models (Comfy-Org#14349)

* feat: Add model support for SCAIL-2 (Comfy-Org#14373)

* initial SCAIL2 support

* Move bg_removal_model input socket to first position for nicer display (Comfy-Org#14353)

* mm: dont reset cast buffers in cleanup_models_gc() (Comfy-Org#14372)

cleanup_models_gc can be called once per load_models_gpu via
free_memory, which in turn can de-activate an active model via
this reset_cast_buffers.

cleanup_models_gc() could also come via obscure garbage collector
paths so limit reset_cast_buffers to the post-node callsite instead.

* Ensure conditions are not trainable to avoid bugs (Comfy-Org#14368)

* feat: Add Bernini-R model support (Wan video) (CORE-279) (Comfy-Org#14216)

* Depth anything 3 (Core-135) (Comfy-Org#13853)

Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>

* Always enable cuda malloc on cu130 and higher. (Comfy-Org#14381)

* chore(openapi): sync shared API contract from cloud@ca12913 (Comfy-Org#14367)

* [Trainer/bug] Ensure model is not inference mode (CORE-72) (Comfy-Org#13400)

* Ensure model is not inference mode

* force clone inside training mode to avoid inference tensor

* Allow force deepcopy for model patcher

* chore(assets): drop vestigial tags.tag_type column (Comfy-Org#14248)

tag_type was always "user" in practice — no code path ever set it to anything
else (no system/seeded classification was wired up) and nothing queried it. The
column, its ix_tags_tag_type index, and the TagUsage.type API field were dead
weight, so they're removed. Adds alembic migration 0004 to drop the column and
index.

Verified: asset-seeder tests pass; migration applies cleanly on a fresh SQLite
(tags retains only name; tag_type column + index dropped).

Co-authored-by: guill <jacob.e.segal@gmail.com>

* feat(assets): cursor-based pagination on GET /api/assets (Comfy-Org#14014)

* spec(assets): add cursor pagination params to GET /api/assets

Add 'after' query param and 'next_cursor' response field for keyset
pagination. Matches the cloud Go implementation (BE-893) so frontend
sees a unified contract across runtimes. Offset/limit remain as a
deprecated fallback.

* feat(assets): add cursor encode/decode helpers for keyset pagination

Port of cloud common/pagination/cursor.go. Wire format is base64url of
{"s", "v", "id"} JSON; times are Unix microseconds UTC to match
PostgreSQL timestamp precision.

Includes a byte-identity fixture pinned against the cloud Go wire
format so cross-runtime FE pagination can't silently drift.

* feat(assets): thread cursor through schemas, service, and query layer

list_assets_page accepts an opaque 'after' cursor and returns
next_cursor when more pages are available. The query applies a keyset
WHERE clause and a secondary ORDER BY id for deterministic tiebreak.

Cursor sort field is validated against the request sort, and a
last_access_time sort (OSS-only) falls back to offset/limit. Offset is
ignored whenever a cursor is supplied.

* feat(assets): wire cursor pagination through GET /api/assets handler

Adds integration tests for: full cursor walk, invalid-cursor 400,
sort/cursor mismatch 400, cursor-wins-over-offset, absent next_cursor
when no more results, and pagination stability across deletes.

* fix(assets): address cursor-review verified findings

- Mint next_cursor on every cursor-supported sort, not only when 'after'
  was supplied. A first request (no 'after') previously returned
  next_cursor=None, leaving cursor mode unreachable from a clean start.
- Over-fetch limit+1 so an exactly-full terminal page doesn't mint a
  spurious cursor pointing at a phantom next page.
- Map crafted out-of-range microsecond cursors (OverflowError / OSError
  in datetime construction) to 400 INVALID_CURSOR instead of leaking 500.
- Bump MAX_CURSOR_VALUE_LENGTH 256 -> 512 to match the AssetReference
  name column max; without this, a long-named asset minted a cursor the
  same server then refused on the next request. Cross-runtime byte
  identity with cloud is unaffected because no cloud cursor ever carries
  a value > 256 (cloud schema doesn't permit it).
- Return None from _encode_next_cursor when the boundary row carries a
  NULL sort value (e.g. an Asset without size_bytes backfilled), instead
  of silently encoding 0 and mis-positioning the keyset.
- Fix schemas_in.py comment so it matches actual handler behavior
  (last_access_time + 'after' raises 400, does not fall back).
- Add AssetsApiError schema + 400 response to GET /api/assets in
  openapi.yaml so generated clients know the INVALID_CURSOR envelope.
- Extend integration coverage: first-page mint, exact-multiple terminal
  page, cursor walks for created_at/updated_at/size sorts, datetime
  overflow surfaces as 400 not 500.
- Add unit coverage for datetime overflow and 512-char round-trip.

* feat(assets): bind cursor to sort order + Go-compat JSON escaping

Address three needs-judgment items from the cursor-review judge synthesis:

1. Cursor wire format now includes an "o" key carrying the sort
   direction ("asc" / "desc") it was minted under. A request that
   replays the cursor with a flipped `order` parameter is rejected
   with 400 INVALID_CURSOR instead of silently walking the wrong
   direction. Legacy cursors without "o" still decode (the binding
   is best-effort until cloud mirrors the field — follow-up filed
   separately).

2. JSON serialization now escapes `<`, `>`, `&`, U+2028, U+2029
   to mirror Go's default `json.Marshal` behavior. Without this, an
   asset name containing those characters produced different bytes on
   Python vs cloud Go. The escaped form is what both runtimes emit.

3. Add direct query-layer tests for the keyset tiebreaker — the secondary
   ORDER BY id branch was previously unexercised. Two scenarios: all
   rows share a primary sort value, and mixed ties straddle page
   boundaries. Both assert no row is dropped or duplicated across the
   walk.

Wire-format note: Python cursors now differ from current cloud cursors
by exactly the "o" key. Cloud follow-up will bring the two back into
byte alignment.

* fix(assets): address bot review comments

- Soften offset param prose: it's not deprecated, just not preferred for
  sequential walks. Random-access UIs (jump-to-page, item count displays)
  legitimately still want offset, so dropping the 'deprecated' framing
  rather than promoting it to a machine-readable deprecated:true flag.
- Add explicit HTTP status assertions before every json() / next_cursor
  read in test_list_cursor.py so a failing request surfaces as an HTTP
  error instead of a confusing KeyError on a 4xx/5xx body.

* feat(assets): require cursor o field, drop legacy permissive path

Cursor pagination hasn't shipped on either runtime yet — this PR is
still draft and cloud's mirror is just behind it — so there are no
legacy no-o cursors in the wild. Make o mandatory from day one
rather than landing permissive and tightening later.

decode_cursor now rejects any payload without o (or with a non-string
o) as malformed. CursorPayload.order becomes a required str. Tests
that constructed CursorPayload directly now pass order="desc";
test_legacy_cursor_without_order_accepted flips to
test_cursor_without_order_rejected.

* chore(assets): drop cross-repo prose from cursor comments

Strip prose references to sibling Go implementations and external
ticket IDs from cursor.py, the cursor tests, the keyset integration
tests, asset_management's sort-field comment, and the legacy
prompt_id alias comment. Pure docstring/comment scrub — no behavior
or wire-format changes. x-runtime: [cloud] field annotations in
openapi.yaml are unchanged; those are the spec's structural
cross-runtime convention, not internal references.

* test(assets): include 'o' in microsecond-boundary cursor payload

The boundary test was building a cursor without the required `o` key, so
decode failed on the missing-order branch before reaching the µs-overflow
path the test is asserting. Both paths return 400 INVALID_CURSOR so the
assertion passed for the wrong reason. Add `o` to the payload and matching
`order=` to the request so the decode reaches the intended branch.

* fix(assets): address ultrareview findings on cursor pagination

Six fact-checked findings from the multi-model review pass:

- Encoder/decoder length asymmetry: encode_cursor now rejects empty id,
  oversized id (>128), oversized value (>512), and invalid order tokens
  symmetrically with decode_cursor. Prevents the same server from minting
  a cursor it then 400s on the next request (e.g. a filesystem-scanned
  asset name >512 chars). The bad-order path now raises InvalidCursorError
  (still subclasses ValueError) so route-layer handling stays uniform.
- Raw U+2028/U+2029 in cursor.py source: ripgrep treated those lines as
  line-terminators, confirming the bytes were the actual separators. Any
  editor save / autoformat / git tooling that normalizes invisibles would
  silently break the encoder. Replaced with explicit 
 / 

  Python escape sequences.
- set(seen) == set(names) hid ordering regressions: a cursor walk that
  dropped a row at a page boundary or returned duplicates could pass.
  Reworked the assertion to (1) reject duplicates, (2) require full
  coverage, and (3) assert strict positional order for size sort, the
  only field with a clock-independent ordering.
- Flaky time.sleep(0.05) between inserts: Windows CI clock resolution is
  ~15ms, so back-to-back inserts under load could collide and exercise
  the tiebreaker instead of the documented path. Removed the sleep and
  let the strengthened assertion above carry coverage / no-duplicates,
  with size sort carrying strict order.
- Cursor error envelope diverged from the rest of routes.py: cursor 400s
  emitted {error: {code, message}} while every other 400 in the file
  emits {error: {code, message, details}} via _build_error_response.
  Switched to _build_error_response and added the details field to the
  AssetsApiError schema in openapi.yaml.
- "Byte-identity fixtures" only checked substring containment, defeating
  the test class's stated purpose of pinning the wire format. Switched
  to exact-bytes equality against an inline expected payload string per
  fixture, so any whitespace / key-order / escape drift fails loudly.

Also dropped Go / json.Marshal references from docstrings — the byte
format is the contract, not the runtime that mints it.

* fix(assets): cap cursors by encoded wire size, not just char count

Char-count guards on value/id can still let multibyte or escape-heavy
inputs blow past MAX_ENCODED_CURSOR_LENGTH once UTF-8 + escape expansion
+ base64url runs. A 512-character name of 'é' (2 bytes UTF-8) or '<'
(serializes to the 6-byte '<' escape) passes the char check, mints
a ~1500-byte cursor, then 400s when handed back on the next request.

Compute the final encoded form and reject it before returning if it
exceeds the wire cap. Adds regression tests for both inflation paths.

* refactor(assets): extract cursor JSON escaping helper; size wire cap above per-field caps

Addresses review feedback on cursor.py:

- Extract the inline escape chain into _apply_wire_compatible_json_escapes()
  with a comment pinning it to the wire format's escape set, so the parity
  intent is explicit rather than reading as an ad-hoc transform.
- Raise MAX_ENCODED_CURSOR_LENGTH to 8192 (comfortably above the ~5.2KB
  worst-case the per-field caps can produce) and drop the mint-time length
  guard. Encoder/decoder symmetry now holds by construction: the encoder
  can't produce a cursor the decode path rejects, so there is no confusing
  user-visible 'cursor too long' failure at mint time.
- Rewrite the two over-wire-cap tests to assert worst-case multibyte and
  escape-heavy values mint and round-trip, instead of being rejected.

* refactor(assets): drop cross-runtime cursor escaping; cursors are opaque

The custom JSON escaping of <, >, &, U+2028, and U+2029 existed only to
keep the encoded cursor byte-identical with the Cloud implementation of
the same payload format. Cursors are opaque tokens, so byte-level
compatibility across implementations is not needed — plain json.dumps
output is sufficient. Remove the escaping helper and the byte-identity
test fixtures that pinned the wire format; keep round-trip coverage for
the affected characters.

---------

Co-authored-by: guill <jacob.e.segal@gmail.com>

* fix(assets): remove unused delete_content param from deleteAsset (Comfy-Org#14241)

* fix(assets): remove unused delete_content param from deleteAsset

The delete_content query param on DELETE /api/assets/{id} was introduced
in Comfy-Org#12125 and had its default flipped to false in Comfy-Org#12621. In practice no
client sends it: the frontend issues a bare DELETE /assets/{id}, so every
real caller already gets the default soft-delete (the reference is hidden,
content preserved). The only thing that set delete_content=true was this
repo's own test teardown.

Remove the param from the route and the OpenAPI spec so the contract
matches what clients actually use (and lines up with the cloud surface).
The route now always soft-deletes. The underlying delete_asset_reference
helper keeps its delete_content_if_orphan option, so orphan reclamation
remains available internally for a future GC path — it's just no longer
exposed on the public endpoint. Tests that used delete_content=true for
hard cleanup now soft-delete; test_delete_upon_reference_count asserts
content preservation instead of orphan removal.

* test/docs: address review on deleteAsset delete_content removal

- Rename test_delete_upon_reference_count ->
  test_soft_delete_preserves_asset_identity_across_references; the old name
  implied last-ref cleanup, but it now verifies the opposite (soft delete
  preserves identity across references).
- Strengthen the re-association assertion: also check asset_hash == src_hash
  so it proves content reuse rather than relying on the now-tautological
  created_new is False.
- Document delete_asset_reference: the orphan-reclamation branch is
  intentionally internal-only; the public endpoint always soft-deletes.
- Normalize the soft-delete comment phrasing.

* test(assets): make seed content unique per test for isolation

Removing the delete_content param means delete is always a soft delete, so
content created by one test now survives into the next. The suite had been
relying on hard-delete teardown for isolation, so shared fixed-content
fixtures started colliding: seeded_asset (b"A"*4096) and
make_asset_bytes (deterministic on name) produced the same hash every test,
so the second seed deduped to the surviving asset and returned 200 instead
of 201, cascading into ~14 failures/errors.

Salt both fixtures with a per-test uuid so each test creates fresh content
(created_new True, 201), while keeping content deterministic within a test
(same name/size -> same bytes) and preserving exact byte length so size-based
list/sort assertions are unaffected.

* main: force cudnn.benchmark to false (Comfy-Org#14390)

Some custom nodes try to set this true globally. It messes with dynamic
VRAM with one-off spikes that can OOM but this is also very high risk
for windows where such allocations might get serviced by shared memory
fallback.

Trump it.

* feat(assets): add job_ids filter to GET /api/assets (Comfy-Org#13998)

* feat(assets): add job_ids filter to GET /api/assets

Mirrors the existing cloud `job_ids` query param on the local Python server:
clients can pass a comma-separated list (or repeated query params) of UUIDs
to filter assets by their associated job.

The `AssetReference.job_id` column already exists, so no migration is
needed — this just plumbs the filter through schema → service → query.

Marks the parameter as available in both runtimes by dropping the
`[cloud-only]` description prefix and the `x-runtime: [cloud]` tag from
the OpenAPI spec, per the OSS field-drift convention (absent runtime tag
= populated by both local and cloud).

* fix(assets): tighten job_ids — array schema, max_length, narrow except

From cursor-reviews on the parent commit:

- OpenAPI: declare job_ids as `type: array, items: string format: uuid`
  with `style: form, explode: true` so it matches the documented
  contract (and matches sibling include_tags/exclude_tags shape).
  Description now states both accepted shapes explicitly.
- Schema: cap `job_ids` at 500 entries (max_length on the Pydantic
  field) so a client can't splice an unbounded list into the IN clauses.
- Schema: drop `AttributeError` from the except — `raw` only contains
  `str` items by construction, so `uuid.UUID(<str>)` raises `ValueError`
  exclusively; the second clause was dead code.

* fix(assets): tighten job_ids validator + add schema-level tests

Aligns with the parallel hardening from draft PR Comfy-Org#13848 (now closed as
a duplicate). The validator now:

- Raises ValueError on non-string list items (was: silently dropped).
- Raises ValueError on non-string / non-list top-level values like dict
  or int (was: silently passed through to Pydantic's downstream coercion).

Adds tests-unit/assets_test/queries/test_list_assets_query.py covering
the validator end-to-end: CSV canonicalization, dedup order, default
empty, invalid UUID, non-string list item, non-string non-list value,
and the max_length=500 boundary.

* feat(prompt): enforce canonical UUID prompt_id at job creation

POST /prompt previously accepted any client-supplied prompt_id verbatim,
str()-coercing even non-strings, and minting the literal job id "None"
for an explicit JSON null. The new GET /api/assets job_ids filter matches
stored job ids as canonical UUIDs exactly, so a non-UUID id minted a job
whose assets could never be filtered.

- validate_job_id (comfy_execution/jobs.py): requires a string in the
  canonical lowercase hyphenated UUID form; raises ValueError otherwise,
  including parseable-but-non-canonical spellings (uppercase, braced, URN,
  bare hex), which would otherwise be silently rewritten and then miss
  every exact-match lookup downstream (history keys, websocket
  correlation, /interrupt, the assets job_ids filter).
- POST /prompt: absent or null prompt_id means the server mints uuid4;
  invalid means 400 invalid_prompt_id on the standard error envelope.
- openapi.yaml: document the request-side prompt_id (format uuid,
  nullable) on PromptRequest.
- tests: unit matrix for validate_job_id; integration tests against the
  booted server covering rejection, acceptance, and null handling.

---------

Co-authored-by: guill <jacob.e.segal@gmail.com>

* feat(assets): include asset id in executed WebSocket message (Comfy-Org#13862)

* feat(assets): enrich executed WS message with asset metadata

When --enable-assets is set, each file-type output entry in the
`executed` WebSocket message now includes id, name, asset_hash, size,
and mime_type — matching the shape already returned by /upload/image.

The enrichment lives in comfy_execution/asset_enrichment.py (no torch
dependency) and is called from both send sites in execution.py: freshly
executed nodes register the file inline via register_file_in_place;
cached node re-sends look up the existing AssetReference by file path
to avoid re-hashing. Errors are caught per-entry so a failure never
blocks the WS message from sending.

* fix(assets): inject only id in executed WS message per Asset Identity RFC

Per the Asset Identity RFC, the executed WebSocket payload should carry
id alone — hash is already encoded in the filename, and name/preview_url/
size belong behind GET /api/assets/{id} rather than being pushed eagerly.

Simplifies the DB lookup path: we only need ref.id, so the asset.hash
null-check is no longer required as a fallback trigger.

* fix(assets): reject path traversal when resolving output abs_path

Subfolder/filename were joined and absolutized without containment check,
so '..' segments or an absolute filename could escape the type's base
directory and register an unrelated on-disk file as an asset.

Add commonpath-based containment check; skip enrichment (warn, leave
entry unchanged) when the resolved path escapes base. Catches ValueError
from cross-drive paths on Windows.

* docs(assets): drop Asset Identity RFC reference from docstring

* docs(assets): trim docstring to what enrichment does, not what it doesn't

* test(assets): use real platform paths so containment check works on Windows

The previous test setup patched os.path.abspath to identity and used a
POSIX-style '/output' base, which collided with Windows path separators
in os.path.commonpath. Drop the abspath/join patches and use a real
tempdir-rooted base so the containment check runs against actual
platform paths.

* refactor(assets): enrich at output-processing time, not in the WS send path

Per review: enrichment lived inside the client_id-guarded send sites, so a
headless run (no websocket client) never registered assets at all, and
ui_outputs/history stored the un-enriched entries.

Now output_ui is enriched once, right after the node produces it and before
it is stored in ui_outputs — so registration happens regardless of connected
clients, and the asset id flows into history and the execution cache for
free. _send_cached_ui re-sends the stored (already-enriched) dict verbatim,
which lets the DB-lookup-by-path fallback be deleted: every enrichment is
now a fresh output, and register_file_in_place re-hashes on upsert so an
overwritten path can never carry a stale id.

* revert(assets): drop job_ids filter from GET /api/assets (Comfy-Org#14408)

The job_ids query filter added in Comfy-Org#13998 has no live consumer: the
frontend Generated tab kept sourcing from GET /jobs, and the cloud side
removed its equivalent filter from the shared asset spec. Carrying it on
the local server only re-introduces Core<->Cloud drift on the shared
contract, so remove it to match.

Removed: the job_ids field + validator on ListAssetsQuery, the IN(...)
clauses in list_references_page, the service/route passthrough, and the
filter-only tests.

Kept: the canonical-UUID prompt_id enforcement at job creation (also
landed in Comfy-Org#13998). It stands on its own -- job ids are matched verbatim
by history keys, websocket correlation, and /interrupt -- and cloud
inherits it by running core for execution, so no divergence is created.

* chore(openapi): sync shared API contract from cloud@e3c52ad (Comfy-Org#14406)

* I don't think this actually works anymore. (Comfy-Org#14403)

* ops: tolerate already force casted dynamic weight (Comfy-Org#14410)

Some custom nodes .to weights completely out of load context which
can wreak havoc if its for a model that is not active. Detect this
condition and just let it fall-through to the non-dynamic loader
straight up.

---------

Signed-off-by: bigcat88 <bigcat88@icloud.com>
Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
Co-authored-by: Daxiong (Lin) <contact@comfyui-wiki.com>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com>
Co-authored-by: Alexis Rolland <alexisrolland@hotmail.com>
Co-authored-by: Jukka Seppänen <40791699+kijai@users.noreply.github.com>
Co-authored-by: rattus <46076784+rattus128@users.noreply.github.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: John Pollock <pollockjj@users.noreply.github.com>
Co-authored-by: Silver <65376327+silveroxides@users.noreply.github.com>
Co-authored-by: Matt Miller <mattmiller@comfy.org>
Co-authored-by: guill <jacob.e.segal@gmail.com>
Co-authored-by: kelseyee <971704395@qq.com>
Co-authored-by: Kohaku-Blueleaf <59680068+KohakuBlueleaf@users.noreply.github.com>
Co-authored-by: Talmaj <Talmaj@users.noreply.github.com>
@ccssu ccssu marked this pull request as draft June 22, 2026 08:15
@ccssu ccssu changed the title sync upstream (#19) submitted by mistake Jun 22, 2026
@ccssu

ccssu commented Jun 22, 2026

Copy link
Copy Markdown
Author

@comfy-pr-bot This PR was submitted by mistake.
Please help close PR #14583. Thank you!

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This pull request adds Depth Anything 3 (DA3) multi-view depth estimation as a new supported model, including a DINOv2 backbone with RoPE/QK-norm extensions, DPT/DualDPT depth heads, ray-to-pose conversion, camera encoder/decoder, and ComfyUI inference and geometry-export nodes. It introduces WAN21 SCAIL2 support with SAM3 mask conditioning streams and a Bernini in-context conditioning node for Wan2.2. On the asset management side, it removes the tag_type column from the Tags table (with Alembic migration), adds opaque keyset cursor pagination for GET /api/assets, switches public asset deletion to soft-delete, adds image-dimension metadata extraction during ingest, and enforces canonical UUID format for prompt IDs. New 3D IO types (File3DSplatAny, File3DPointCloudAny) and associated preview/export nodes are added. API nodes gain Gemini V2 with multimodal media budgeting, Krea 2 Medium Turbo, a BFL erase seed input, and a SaveAudioAdvanced node. Runtime changes include a --debug-hang traceback-dump flag, cuDNN benchmark refactoring, pinned memory CUDA host registration hardening, and various dtype/control-flow fixes.

🚥 Pre-merge checks | ✅ 2 | ❌ 3

❌ Failed checks (2 warnings, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.77% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The PR description is completely absent, leaving reviewers without author-provided context or rationale for the changeset. Add a concise PR description summarizing the main changes, key features, and any migration notes or testing guidance for reviewers.
Title check ❓ Inconclusive The title "sync upstream (#19)" is overly vague and does not convey meaningful information about the specific changes made in this large changeset. Consider a more descriptive title that highlights the primary feature or change area, such as "Add cursor-based pagination for assets, image dimensions, and SCAIL-2 model support" or "Implement keyset pagination and DA3 depth estimation features".
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/assets/database/queries/tags.py (1)

259-267: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update list_tags_with_usage return annotation to match the new tuple shape.

The function now returns (name, count) rows, but the signature still advertises a 3-item tuple including the removed tag_type. This leaves an incorrect type contract.

Suggested fix
 def list_tags_with_usage(
     session: Session,
@@
-) -> tuple[list[tuple[str, str, int]], int]:
+) -> tuple[list[tuple[str, int]], int]:

Also applies to: 331-331

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/assets/database/queries/tags.py` around lines 259 - 267, The return type
annotation for the `list_tags_with_usage` function incorrectly specifies a
3-item tuple (str, str, int) when the function now only returns 2-item tuples
(name, count) after removing the tag_type field. Update the return annotation
from tuple[list[tuple[str, str, int]], int] to tuple[list[tuple[str, int]], int]
to accurately reflect the new tuple shape being returned by the function.
🧹 Nitpick comments (5)
comfy_extras/nodes_depth_anything_3.py (1)

154-165: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Fail fast if the selected checkpoint is not a DA3 model.

model_name is sourced from the broad geometry_estimation list, so selecting a non-DA3 checkpoint can fail later with unclear attribute errors in inference. Add an immediate model-type guard after loading.

💡 Proposed fix
         path = folder_paths.get_full_path_or_raise("geometry_estimation", model_name)
         model = comfy.sd.load_diffusion_model(path, model_options=model_options)
+        image_model = getattr(getattr(model, "model_config", None), "unet_config", {}).get("image_model")
+        if image_model != "DepthAnything3":
+            raise ValueError(
+                f"Selected checkpoint '{model_name}' is not a Depth Anything 3 model "
+                f"(detected image_model={image_model!r})."
+            )
         return io.NodeOutput(model)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy_extras/nodes_depth_anything_3.py` around lines 154 - 165, In the
execute classmethod, immediately after loading the model with
comfy.sd.load_diffusion_model, add a type guard to validate that the loaded
model is actually a Depth Anything 3 model. Since model_name is sourced from the
broad geometry_estimation folder list, a non-DA3 checkpoint could be selected,
causing unclear attribute errors during inference. Add a check to verify the
model type and raise a descriptive error if the model is not a DA3 model,
ensuring fast failure with a clear message.
comfy_extras/nodes_audio.py (1)

285-292: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add an explicit audio is None guard in SaveAudioAdvanced.execute.

The legacy save nodes raise a clear ValueError for missing audio, but this new path forwards directly and will fail later with a less actionable error.

Proposed patch
     `@classmethod`
     def execute(cls, audio, filename_prefix: str, format: dict) -> IO.NodeOutput:
+        if audio is None:
+            raise ValueError("SaveAudioAdvanced: input audio is None (source video may have no audio track).")
         file_format = format.get("format", None)
         quality = format.get("quality", None)
         if quality:
             ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format, quality=quality)
         else:
             ui=UI.AudioSaveHelper.get_save_audio_ui(audio, filename_prefix=filename_prefix, cls=cls, format=file_format)
         return IO.NodeOutput(ui=ui)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy_extras/nodes_audio.py` around lines 285 - 292, Add an explicit guard at
the beginning of the SaveAudioAdvanced.execute method to check if the audio
parameter is None, and raise a ValueError with a clear error message if it is.
This validation should occur before extracting the format and quality values,
ensuring that missing audio input is caught early with an actionable error
message consistent with legacy save node behavior.
comfy/ldm/depth_anything_3/camera.py (1)

32-40: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

_LayerScale.gamma is uninitialized; consider initializing to init_values.

torch.empty leaves gamma with arbitrary values. While checkpoint loading will overwrite this, the parameter should still be initialized for robustness (debugging, testing without weights, or if a checkpoint is missing this key).

Proposed fix
 class _LayerScale(nn.Module):
     """Per-channel learnable scaling. Matches upstream LayerScale."""

-    def __init__(self, dim, *, device=None, dtype=None):
+    def __init__(self, dim, init_values=1e-4, *, device=None, dtype=None):
         super().__init__()
-        self.gamma = nn.Parameter(torch.empty(dim, device=device, dtype=dtype))
+        self.gamma = nn.Parameter(init_values * torch.ones(dim, device=device, dtype=dtype))

Then in _Block, forward init_values:

-        self.ls1 = _LayerScale(dim, device=device, dtype=dtype) if init_values else nn.Identity()
+        self.ls1 = _LayerScale(dim, init_values=init_values, device=device, dtype=dtype) if init_values else nn.Identity()
         ...
-        self.ls2 = _LayerScale(dim, device=device, dtype=dtype) if init_values else nn.Identity()
+        self.ls2 = _LayerScale(dim, init_values=init_values, device=device, dtype=dtype) if init_values else nn.Identity()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy/ldm/depth_anything_3/camera.py` around lines 32 - 40, In the
_LayerScale class __init__ method, the gamma parameter is being initialized with
torch.empty which leaves it with arbitrary values. Add an init_values parameter
to the __init__ method with a sensible default value (typically 1.0), and use it
to properly initialize self.gamma instead of using torch.empty. This ensures
gamma has meaningful initial values for robustness when debugging, testing
without weights, or if a checkpoint is missing this key.
comfy/ldm/depth_anything_3/ray_pose.py (1)

248-252: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Potential division by zero when all confidence values are zero.

If all pixels fail the z_thresh check (line 196-197), all weights become zero. The sum at line 251 would then be zero, causing a division-by-zero producing NaN translations.

This is an unlikely edge case (would require an entirely invalid ray output), but a guard would make it more robust.

💡 Optional guard
     cf = confidence.flatten(0, 1).flatten(1, 2)
     T = (camray.flatten(0, 1).flatten(1, 2)[..., 3:] * cf.unsqueeze(-1)).sum(dim=1)
-    T = T / cf.sum(dim=-1, keepdim=True)
+    cf_sum = cf.sum(dim=-1, keepdim=True)
+    T = T / cf_sum.clamp(min=1e-8)
     T = T.reshape(B, S, 3)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy/ldm/depth_anything_3/ray_pose.py` around lines 248 - 252, The
translation calculation using confidence-weighted averaging can encounter
division by zero when all confidence values are zero, resulting in NaN values.
In the section where variable T is computed by dividing by cf.sum(dim=-1,
keepdim=True), add a guard that clamps or prevents division by zero. This can be
done by either adding a small epsilon value to the denominator before dividing,
or by using a clamp operation on the denominator to ensure it never reaches
exactly zero, so the division operation remains numerically stable and does not
produce NaN translations.
comfy/ldm/depth_anything_3/model.py (1)

170-171: 🧹 Nitpick | 🔵 Trivial | 💤 Low value

Potential thread-safety issue with runtime enable_aux toggle.

Modifying self.head.enable_aux during forward pass is not thread-safe if the same model instance is used concurrently with different use_ray_pose values. One thread could set it to True while another expects False.

This is likely acceptable for typical single-threaded inference, but worth noting for concurrent execution scenarios.

💡 Thread-safe alternative

Pass enable_aux as a forward argument to DualDPT.forward() instead of storing it as instance state:

-        if isinstance(self.head, DualDPT):
-            self.head.enable_aux = bool(use_ray_pose)
-
-        feats, aux_feats = self.backbone.get_intermediate_layers_da3(
+        feats, aux_feats = self.backbone.get_intermediate_layers_da3(
             ...
         )
-        head_out = self.head(feats, H=H, W=W, patch_start_idx=0)
+        head_out = self.head(feats, H=H, W=W, patch_start_idx=0,
+                             enable_aux=bool(use_ray_pose) if isinstance(self.head, DualDPT) else False)

This would require updating DualDPT.forward() to accept enable_aux as a parameter.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy/ldm/depth_anything_3/model.py` around lines 170 - 171, The current
implementation sets `self.head.enable_aux` as instance state which creates
thread-safety issues. Instead, refactor to pass `enable_aux` as a parameter
through the forward pass. First, update the `DualDPT.forward()` method signature
to accept `enable_aux` as a parameter, then replace the direct assignment
`self.head.enable_aux = bool(use_ray_pose)` with passing `bool(use_ray_pose)` as
an argument when invoking the forward method on the DualDPT head. This ensures
that the auxiliary head behavior is determined per-invocation rather than stored
as mutable instance state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/assets/database/queries/asset_reference.py`:
- Around line 311-324: The keyset pagination logic in the if block checking
after_cursor_value and after_cursor_id does not handle NULL values in the sort
column, which causes cursor pagination to skip NULL values since SQL comparisons
with NULL never match. Modify the keyset predicate construction in both the
descending and non-descending branches to explicitly handle NULL boundaries:
when building the sa.or_ conditions, add logic to detect if the sort column
(sort_col) is NULL and compare appropriately, or use NULL-safe comparison
operators/expressions that can correctly navigate through rows where the sort
column is NULL. Alternatively, document and enforce that the size field cannot
be used as a cursor-sortable column if NULL values are not handled.

In `@app/assets/services/ingest.py`:
- Around line 370-373: The conditional check before calling
extract_image_dimensions() returns early for both None and non-image MIME types,
preventing the function from attempting header-based detection on files with
unknown MIME types. Modify the condition to only return early when mime_type is
explicitly present and does not start with "image/", allowing None values to
pass through to extract_image_dimensions() so it can use Pillow's header
detection as intended.

In `@comfy_extras/nodes_custom_sampler.py`:
- Around line 937-939: The early return condition in the uncond model check is
bypassing the cfg_function path when self.cfg is driven to 1.0 on this step,
which prevents CFG hooks and wrappers from being applied. Modify the conditional
check that determines when to return early with just cond so that it only
returns when self.uncond_inner is None (unconditional model not loaded), but
removes the math.isclose(self.cfg, 1.0) condition from the OR clause. This
allows the CFG processing to continue even when cfg equals 1.0 on this
particular sigma step, ensuring that any cfg_function hooks and custom CFG
behavior are still executed.

In `@comfy_extras/nodes_depth_anything_3.py`:
- Around line 600-633: The mask at the end of the execute method only checks if
depth values are finite using torch.isfinite, but does not exclude zero or
negative depth values that were explicitly set to 0.0 earlier when depth was not
finite. This allows spurious points to be created near the camera origin. Modify
the mask assignment line to also check that depth values are positive (greater
than 0) in addition to checking finiteness, so the final mask only includes
valid positive finite depth values.

In `@comfy_extras/nodes_load_3d.py`:
- Around line 176-177: The filename generation using the f-string with
model_3d.format can result in trailing-dot filenames when the format field is
empty or unset for stream-backed Types.File3D objects. Fix this issue at all
three locations (lines 176, 244, and 303) by adding a guard condition that
checks if model_3d.format is empty or None before appending the format extension
to the filename. Conditionally include the dot and format extension only when
model_3d.format has a valid value, otherwise generate the filename without the
extension suffix. This prevents invalid filenames that fail on certain
platforms.

In `@comfy_extras/nodes_resolution.py`:
- Around line 67-70: The tooltip text for the "width" and "height" io.Int.Output
definitions incorrectly describes the behavior as "multiplied by the selected
multiple" when the actual behavior is that dimensions are rounded to the nearest
selected multiple. Update the tooltip strings for both the "width" and "height"
outputs to accurately reflect that the dimensions are rounded to the nearest
selected multiple rather than multiplied by it.

In `@comfy/model_patcher.py`:
- Around line 384-386: The condition logic around line 384-386 in the
model_patcher.py file has a flaw where force_deepcopy does not properly enforce
a non-dynamic clone class. When force_deepcopy is True and disable_dynamic is
False, the outer condition is satisfied but the inner condition checking for
both is_dynamic() and disable_dynamic fails, leaving class_ as self.__class__
instead of ModelPatcher. To fix this, add an additional condition in the
assignment block to also set class_ = ModelPatcher when force_deepcopy is True,
ensuring that force_deepcopy actually forces a non-dynamic clone regardless of
the disable_dynamic parameter value.

In `@tests-unit/assets_test/services/test_cursor.py`:
- Around line 260-262: The test_invalid_order_token_rejected_on_encode method is
catching the wrong exception type. The encode_cursor function raises
InvalidCursorError when given an unsupported order token like "sideways", not
ValueError. Replace the ValueError in the pytest.raises call with
InvalidCursorError to match the actual exception that encode_cursor throws.

---

Outside diff comments:
In `@app/assets/database/queries/tags.py`:
- Around line 259-267: The return type annotation for the `list_tags_with_usage`
function incorrectly specifies a 3-item tuple (str, str, int) when the function
now only returns 2-item tuples (name, count) after removing the tag_type field.
Update the return annotation from tuple[list[tuple[str, str, int]], int] to
tuple[list[tuple[str, int]], int] to accurately reflect the new tuple shape
being returned by the function.

---

Nitpick comments:
In `@comfy_extras/nodes_audio.py`:
- Around line 285-292: Add an explicit guard at the beginning of the
SaveAudioAdvanced.execute method to check if the audio parameter is None, and
raise a ValueError with a clear error message if it is. This validation should
occur before extracting the format and quality values, ensuring that missing
audio input is caught early with an actionable error message consistent with
legacy save node behavior.

In `@comfy_extras/nodes_depth_anything_3.py`:
- Around line 154-165: In the execute classmethod, immediately after loading the
model with comfy.sd.load_diffusion_model, add a type guard to validate that the
loaded model is actually a Depth Anything 3 model. Since model_name is sourced
from the broad geometry_estimation folder list, a non-DA3 checkpoint could be
selected, causing unclear attribute errors during inference. Add a check to
verify the model type and raise a descriptive error if the model is not a DA3
model, ensuring fast failure with a clear message.

In `@comfy/ldm/depth_anything_3/camera.py`:
- Around line 32-40: In the _LayerScale class __init__ method, the gamma
parameter is being initialized with torch.empty which leaves it with arbitrary
values. Add an init_values parameter to the __init__ method with a sensible
default value (typically 1.0), and use it to properly initialize self.gamma
instead of using torch.empty. This ensures gamma has meaningful initial values
for robustness when debugging, testing without weights, or if a checkpoint is
missing this key.

In `@comfy/ldm/depth_anything_3/model.py`:
- Around line 170-171: The current implementation sets `self.head.enable_aux` as
instance state which creates thread-safety issues. Instead, refactor to pass
`enable_aux` as a parameter through the forward pass. First, update the
`DualDPT.forward()` method signature to accept `enable_aux` as a parameter, then
replace the direct assignment `self.head.enable_aux = bool(use_ray_pose)` with
passing `bool(use_ray_pose)` as an argument when invoking the forward method on
the DualDPT head. This ensures that the auxiliary head behavior is determined
per-invocation rather than stored as mutable instance state.

In `@comfy/ldm/depth_anything_3/ray_pose.py`:
- Around line 248-252: The translation calculation using confidence-weighted
averaging can encounter division by zero when all confidence values are zero,
resulting in NaN values. In the section where variable T is computed by dividing
by cf.sum(dim=-1, keepdim=True), add a guard that clamps or prevents division by
zero. This can be done by either adding a small epsilon value to the denominator
before dividing, or by using a clamp operation on the denominator to ensure it
never reaches exactly zero, so the division operation remains numerically stable
and does not produce NaN translations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ea12feb2-1c09-47b0-9938-1ac13c693dda

📥 Commits

Reviewing files that changed from the base of the PR and between b0f9e32 and 7073a93.

⛔ Files ignored due to path filters (3)
  • .ci/windows_amd_base_files/README_VERY_IMPORTANT.txt is excluded by !.ci/**
  • comfy_api_nodes/apis/bfl.py is excluded by !comfy_api_nodes/apis/**
  • comfy_api_nodes/apis/gemini.py is excluded by !comfy_api_nodes/apis/**
📒 Files selected for processing (81)
  • .github/workflows/check-line-endings.yml
  • README.md
  • alembic_db/versions/0004_drop_tag_type.py
  • app/assets/api/routes.py
  • app/assets/api/schemas_in.py
  • app/assets/api/schemas_out.py
  • app/assets/database/models.py
  • app/assets/database/queries/asset_reference.py
  • app/assets/database/queries/tags.py
  • app/assets/scanner.py
  • app/assets/services/asset_management.py
  • app/assets/services/cursor.py
  • app/assets/services/image_dimensions.py
  • app/assets/services/ingest.py
  • app/assets/services/schemas.py
  • app/assets/services/tagging.py
  • comfy/cli_args.py
  • comfy/image_encoders/dino2.py
  • comfy/ldm/colormap.py
  • comfy/ldm/depth_anything_3/camera.py
  • comfy/ldm/depth_anything_3/dpt.py
  • comfy/ldm/depth_anything_3/model.py
  • comfy/ldm/depth_anything_3/preprocess.py
  • comfy/ldm/depth_anything_3/ray_pose.py
  • comfy/ldm/depth_anything_3/reference_view_selector.py
  • comfy/ldm/depth_anything_3/transform.py
  • comfy/ldm/ideogram4/model.py
  • comfy/ldm/qwen_image/model.py
  • comfy/ldm/wan/model.py
  • comfy/lora.py
  • comfy/model_base.py
  • comfy/model_detection.py
  • comfy/model_management.py
  • comfy/model_patcher.py
  • comfy/ops.py
  • comfy/pinned_memory.py
  • comfy/supported_models.py
  • comfy/text_encoders/ideogram4.py
  • comfy_api/latest/_io.py
  • comfy_api/latest/_ui.py
  • comfy_api_nodes/nodes_bfl.py
  • comfy_api_nodes/nodes_gemini.py
  • comfy_api_nodes/nodes_krea.py
  • comfy_execution/asset_enrichment.py
  • comfy_execution/jobs.py
  • comfy_extras/nodes_audio.py
  • comfy_extras/nodes_bernini.py
  • comfy_extras/nodes_bg_removal.py
  • comfy_extras/nodes_color.py
  • comfy_extras/nodes_custom_sampler.py
  • comfy_extras/nodes_depth_anything_3.py
  • comfy_extras/nodes_gaussian_splat.py
  • comfy_extras/nodes_load_3d.py
  • comfy_extras/nodes_moge.py
  • comfy_extras/nodes_resolution.py
  • comfy_extras/nodes_save_3d.py
  • comfy_extras/nodes_scail.py
  • comfy_extras/nodes_train.py
  • comfy_extras/nodes_wan.py
  • cuda_malloc.py
  • execution.py
  • main.py
  • nodes.py
  • openapi.yaml
  • requirements.txt
  • server.py
  • tests-unit/assets_test/conftest.py
  • tests-unit/assets_test/queries/test_asset_reference_keyset.py
  • tests-unit/assets_test/queries/test_tags.py
  • tests-unit/assets_test/services/test_cursor.py
  • tests-unit/assets_test/services/test_image_dimensions.py
  • tests-unit/assets_test/services/test_ingest.py
  • tests-unit/assets_test/services/test_tagging.py
  • tests-unit/assets_test/test_crud.py
  • tests-unit/assets_test/test_downloads.py
  • tests-unit/assets_test/test_list_cursor.py
  • tests-unit/assets_test/test_prompt_id_enforcement.py
  • tests-unit/assets_test/test_sync_references.py
  • tests-unit/assets_test/test_tags_api.py
  • tests-unit/execution_test/test_enrich_output.py
  • tests/execution/test_jobs.py
💤 Files with no reviewable changes (3)
  • README.md
  • app/assets/database/models.py
  • comfy_extras/nodes_wan.py

Comment on lines +311 to +324
# Keyset WHERE: (sort_col, id) strictly less-than / greater-than the cursor.
# Equivalent to: sort_col <op> v OR (sort_col = v AND id <op> cursor_id).
if after_cursor_value is not None and after_cursor_id is not None:
if descending:
keyset = sa.or_(
sort_col < after_cursor_value,
sa.and_(sort_col == after_cursor_value, AssetReference.id < after_cursor_id),
)
else:
keyset = sa.or_(
sort_col > after_cursor_value,
sa.and_(sort_col == after_cursor_value, AssetReference.id > after_cursor_id),
)
base = base.where(keyset)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make the keyset predicate NULL-aware for size.

size is exposed as cursor-supported, but this predicate cannot advance into Asset.size_bytes IS NULL rows because SQL comparisons with NULL never match. Size-sorted cursor pagination can therefore skip or prematurely hide null-sized assets; either remove size from cursor mode or encode/compare NULL boundaries explicitly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/assets/database/queries/asset_reference.py` around lines 311 - 324, The
keyset pagination logic in the if block checking after_cursor_value and
after_cursor_id does not handle NULL values in the sort column, which causes
cursor pagination to skip NULL values since SQL comparisons with NULL never
match. Modify the keyset predicate construction in both the descending and
non-descending branches to explicitly handle NULL boundaries: when building the
sa.or_ conditions, add logic to detect if the sort column (sort_col) is NULL and
compare appropriately, or use NULL-safe comparison operators/expressions that
can correctly navigate through rows where the sort column is NULL.
Alternatively, document and enforce that the size field cannot be used as a
cursor-sortable column if NULL values are not handled.

Comment on lines +370 to +373
if not mime_type or not mime_type.startswith("image/"):
return

dims = extract_image_dimensions(file_path, mime_type=mime_type)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Allow MIME-unknown files to use header detection.

extract_image_dimensions() already treats mime_type=None as “try Pillow”, but this wrapper returns early before calling it. Images with unknown/extensionless MIME types will never get width/height metadata.

Proposed fix
-    if not mime_type or not mime_type.startswith("image/"):
+    if mime_type is not None and not mime_type.startswith("image/"):
         return
 
     dims = extract_image_dimensions(file_path, mime_type=mime_type)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if not mime_type or not mime_type.startswith("image/"):
return
dims = extract_image_dimensions(file_path, mime_type=mime_type)
if mime_type is not None and not mime_type.startswith("image/"):
return
dims = extract_image_dimensions(file_path, mime_type=mime_type)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/assets/services/ingest.py` around lines 370 - 373, The conditional check
before calling extract_image_dimensions() returns early for both None and
non-image MIME types, preventing the function from attempting header-based
detection on files with unknown MIME types. Modify the condition to only return
early when mime_type is explicitly present and does not start with "image/",
allowing None values to pass through to extract_image_dimensions() so it can use
Pillow's header detection as intended.

Comment on lines +937 to +939
# uncond model not loaded (base cfg==1/no negative), or cfg driven to 1.0 this step -> single model, cond only
if self.uncond_inner is None or (math.isclose(self.cfg, 1.0) and not model_options.get("disable_cfg1_optimization", False)):
return cond

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Keep CFG hooks on the step-local cfg == 1 fast path.

This new early return skips the cfg_function path when CFGOverride drives self.cfg to 1.0 for only this sigma. Standard sampling still short-circuits uncond at CFG=1 while continuing through CFG processing, so wrappers/custom CFG behavior can be bypassed here.

🐛 Proposed adjustment
-        # uncond model not loaded (base cfg==1/no negative), or cfg driven to 1.0 this step -> single model, cond only
-        if self.uncond_inner is None or (math.isclose(self.cfg, 1.0) and not model_options.get("disable_cfg1_optimization", False)):
+        # uncond model not loaded (base cfg==1/no negative) -> single model, cond only
+        if self.uncond_inner is None:
             return cond
+        if math.isclose(self.cfg, 1.0) and not model_options.get("disable_cfg1_optimization", False):
+            return comfy.samplers.cfg_function(
+                self.inner_model, cond, cond, self.cfg, x, timestep,
+                model_options=model_options, cond=positive, uncond=None,
+            )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy_extras/nodes_custom_sampler.py` around lines 937 - 939, The early
return condition in the uncond model check is bypassing the cfg_function path
when self.cfg is driven to 1.0 on this step, which prevents CFG hooks and
wrappers from being applied. Modify the conditional check that determines when
to return early with just cond so that it only returns when self.uncond_inner is
None (unconditional model not loaded), but removes the math.isclose(self.cfg,
1.0) condition from the OR clause. This allows the CFG processing to continue
even when cfg equals 1.0 on this particular sigma step, ensuring that any
cfg_function hooks and custom CFG behavior are still executed.

Comment on lines +600 to +633
def execute(cls, da3_geometry, batch_index, confidence_threshold, use_sky_mask, downsample) -> io.NodeOutput:
depth_all = da3_geometry["depth"] # (B, H, W)
B = depth_all.shape[0]
if batch_index >= B:
raise ValueError(f"batch_index {batch_index} is out of range; DA3_GEOMETRY has batch size {B}.")

depth = depth_all[batch_index].clone() # (H, W)
depth[~torch.isfinite(depth)] = 0.0
H, W = depth.shape

K = _da3_get_K(da3_geometry, batch_index, H, W)

if downsample > 1:
depth = depth[::downsample, ::downsample].contiguous()
# Scale intrinsics to the downsampled grid.
K = K.clone()
K[0, :] /= downsample
K[1, :] /= downsample

H_ds, W_ds = depth.shape
points = _da3_unproject(depth, K) # (H_ds, W_ds, 3) in OpenCV camera space

# Apply world-to-camera inverse so multi-view frames share a common world frame.
E = _da3_get_extrinsic(da3_geometry, batch_index)
if E is not None:
points = _da3_apply_extrinsic(points, E)

# Rebuild mask at downsampled resolution.
mask = _da3_build_mask(da3_geometry, batch_index, H, W, confidence_threshold, use_sky_mask)
if downsample > 1:
mask = mask[::downsample, ::downsample]

mask = mask & torch.isfinite(depth)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Exclude zero/negative depth values from point-cloud mask.

At Line 632, the mask only checks finiteness. Depth values zeroed at Line 607 still pass and create spurious points at/near the camera origin.

💡 Proposed fix
-        mask = mask & torch.isfinite(depth)
+        mask = mask & torch.isfinite(depth) & (depth > 0)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def execute(cls, da3_geometry, batch_index, confidence_threshold, use_sky_mask, downsample) -> io.NodeOutput:
depth_all = da3_geometry["depth"] # (B, H, W)
B = depth_all.shape[0]
if batch_index >= B:
raise ValueError(f"batch_index {batch_index} is out of range; DA3_GEOMETRY has batch size {B}.")
depth = depth_all[batch_index].clone() # (H, W)
depth[~torch.isfinite(depth)] = 0.0
H, W = depth.shape
K = _da3_get_K(da3_geometry, batch_index, H, W)
if downsample > 1:
depth = depth[::downsample, ::downsample].contiguous()
# Scale intrinsics to the downsampled grid.
K = K.clone()
K[0, :] /= downsample
K[1, :] /= downsample
H_ds, W_ds = depth.shape
points = _da3_unproject(depth, K) # (H_ds, W_ds, 3) in OpenCV camera space
# Apply world-to-camera inverse so multi-view frames share a common world frame.
E = _da3_get_extrinsic(da3_geometry, batch_index)
if E is not None:
points = _da3_apply_extrinsic(points, E)
# Rebuild mask at downsampled resolution.
mask = _da3_build_mask(da3_geometry, batch_index, H, W, confidence_threshold, use_sky_mask)
if downsample > 1:
mask = mask[::downsample, ::downsample]
mask = mask & torch.isfinite(depth)
def execute(cls, da3_geometry, batch_index, confidence_threshold, use_sky_mask, downsample) -> io.NodeOutput:
depth_all = da3_geometry["depth"] # (B, H, W)
B = depth_all.shape[0]
if batch_index >= B:
raise ValueError(f"batch_index {batch_index} is out of range; DA3_GEOMETRY has batch size {B}.")
depth = depth_all[batch_index].clone() # (H, W)
depth[~torch.isfinite(depth)] = 0.0
H, W = depth.shape
K = _da3_get_K(da3_geometry, batch_index, H, W)
if downsample > 1:
depth = depth[::downsample, ::downsample].contiguous()
# Scale intrinsics to the downsampled grid.
K = K.clone()
K[0, :] /= downsample
K[1, :] /= downsample
H_ds, W_ds = depth.shape
points = _da3_unproject(depth, K) # (H_ds, W_ds, 3) in OpenCV camera space
# Apply world-to-camera inverse so multi-view frames share a common world frame.
E = _da3_get_extrinsic(da3_geometry, batch_index)
if E is not None:
points = _da3_apply_extrinsic(points, E)
# Rebuild mask at downsampled resolution.
mask = _da3_build_mask(da3_geometry, batch_index, H, W, confidence_threshold, use_sky_mask)
if downsample > 1:
mask = mask[::downsample, ::downsample]
mask = mask & torch.isfinite(depth) & (depth > 0)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy_extras/nodes_depth_anything_3.py` around lines 600 - 633, The mask at
the end of the execute method only checks if depth values are finite using
torch.isfinite, but does not exclude zero or negative depth values that were
explicitly set to 0.0 earlier when depth was not finite. This allows spurious
points to be created near the camera origin. Modify the mask assignment line to
also check that depth values are positive (greater than 0) in addition to
checking finiteness, so the final mask only includes valid positive finite depth
values.

Comment on lines +176 to +177
filename = f"preview3d_advanced_{uuid.uuid4().hex}.{model_3d.format}"
model_3d.save_to(os.path.join(folder_paths.get_temp_directory(), filename))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Guard against empty model_3d.format when generating preview temp filenames.

Line 176, Line 244, and Line 303 always append .{model_3d.format}. For stream-backed Types.File3D objects with unset format, this can produce trailing-dot filenames and fail temp-file writes/loads on some platforms.

🛠️ Proposed fix
+def _preview_filename(prefix: str, model_3d: Types.File3D, default_ext: str) -> str:
+    ext = (model_3d.format or default_ext).lstrip(".").lower()
+    return f"{prefix}_{uuid.uuid4().hex}.{ext}"
+
 class Preview3DAdvanced(IO.ComfyNode):
@@
-        filename = f"preview3d_advanced_{uuid.uuid4().hex}.{model_3d.format}"
+        filename = _preview_filename("preview3d_advanced", model_3d, "glb")
@@
 class PreviewGaussianSplat(IO.ComfyNode):
@@
-        filename = f"preview_splat_{uuid.uuid4().hex}.{model_3d.format}"
+        filename = _preview_filename("preview_splat", model_3d, "ply")
@@
 class PreviewPointCloud(IO.ComfyNode):
@@
-        filename = f"preview_pointcloud_{uuid.uuid4().hex}.{model_3d.format}"
+        filename = _preview_filename("preview_pointcloud", model_3d, "ply")

Also applies to: 244-245, 303-304

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy_extras/nodes_load_3d.py` around lines 176 - 177, The filename
generation using the f-string with model_3d.format can result in trailing-dot
filenames when the format field is empty or unset for stream-backed Types.File3D
objects. Fix this issue at all three locations (lines 176, 244, and 303) by
adding a guard condition that checks if model_3d.format is empty or None before
appending the format extension to the filename. Conditionally include the dot
and format extension only when model_3d.format has a valid value, otherwise
generate the filename without the extension suffix. This prevents invalid
filenames that fail on certain platforms.

Comment on lines +67 to +70
"width", tooltip="Calculated width in pixels multiplied by the selected multiple."
),
io.Int.Output(
"height", tooltip="Calculated height in pixels (multiple of 8)."
"height", tooltip="Calculated height in pixels multiplied by the selected multiple."

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix tooltip wording to match the rounding behavior.

The node rounds dimensions to the nearest selected multiple; it does not multiply final dimensions by that multiple.

Proposed patch
-                io.Int.Output(
-                    "width", tooltip="Calculated width in pixels multiplied by the selected multiple."
-                ),
+                io.Int.Output(
+                    "width", tooltip="Calculated width in pixels, rounded to the selected multiple."
+                ),
                 io.Int.Output(
-                    "height", tooltip="Calculated height in pixels multiplied by the selected multiple."
+                    "height", tooltip="Calculated height in pixels, rounded to the selected multiple."
                 ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"width", tooltip="Calculated width in pixels multiplied by the selected multiple."
),
io.Int.Output(
"height", tooltip="Calculated height in pixels (multiple of 8)."
"height", tooltip="Calculated height in pixels multiplied by the selected multiple."
"width", tooltip="Calculated width in pixels, rounded to the selected multiple."
),
io.Int.Output(
"height", tooltip="Calculated height in pixels, rounded to the selected multiple."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy_extras/nodes_resolution.py` around lines 67 - 70, The tooltip text for
the "width" and "height" io.Int.Output definitions incorrectly describes the
behavior as "multiplied by the selected multiple" when the actual behavior is
that dimensions are rounded to the nearest selected multiple. Update the tooltip
strings for both the "width" and "height" outputs to accurately reflect that the
dimensions are rounded to the nearest selected multiple rather than multiplied
by it.

Comment thread comfy/model_patcher.py
Comment on lines +384 to +386
if self.is_dynamic() and disable_dynamic or force_deepcopy:
if self.is_dynamic() and disable_dynamic:
class_ = ModelPatcher

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

force_deepcopy does not force a non-dynamic clone class for dynamic patchers.

On Line 384-Line 386, when force_deepcopy=True and disable_dynamic=False, the code enters the branch but leaves class_ as self.__class__. That contradicts the new force-deepcopy intent and can keep dynamic behavior in paths that expect a non-dynamic clone.

Proposed fix
-        if self.is_dynamic() and disable_dynamic or force_deepcopy:
-            if self.is_dynamic() and disable_dynamic:
+        if (self.is_dynamic() and disable_dynamic) or force_deepcopy:
+            if self.is_dynamic() and (disable_dynamic or force_deepcopy):
                 class_ = ModelPatcher
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if self.is_dynamic() and disable_dynamic or force_deepcopy:
if self.is_dynamic() and disable_dynamic:
class_ = ModelPatcher
if (self.is_dynamic() and disable_dynamic) or force_deepcopy:
if self.is_dynamic() and (disable_dynamic or force_deepcopy):
class_ = ModelPatcher
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@comfy/model_patcher.py` around lines 384 - 386, The condition logic around
line 384-386 in the model_patcher.py file has a flaw where force_deepcopy does
not properly enforce a non-dynamic clone class. When force_deepcopy is True and
disable_dynamic is False, the outer condition is satisfied but the inner
condition checking for both is_dynamic() and disable_dynamic fails, leaving
class_ as self.__class__ instead of ModelPatcher. To fix this, add an additional
condition in the assignment block to also set class_ = ModelPatcher when
force_deepcopy is True, ensuring that force_deepcopy actually forces a
non-dynamic clone regardless of the disable_dynamic parameter value.

Comment on lines +260 to +262
def test_invalid_order_token_rejected_on_encode(self):
with pytest.raises(ValueError):
encode_cursor("created_at", "1", "id-1", order="sideways")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Incorrect exception type expected for invalid order on encode.

At Line 261, pytest.raises(ValueError) is inconsistent with encode_cursor, which raises InvalidCursorError for unsupported order tokens. This makes the test fail on the intended path.

Suggested fix
     def test_invalid_order_token_rejected_on_encode(self):
-        with pytest.raises(ValueError):
+        with pytest.raises(InvalidCursorError, match="order must be one of"):
             encode_cursor("created_at", "1", "id-1", order="sideways")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_invalid_order_token_rejected_on_encode(self):
with pytest.raises(ValueError):
encode_cursor("created_at", "1", "id-1", order="sideways")
def test_invalid_order_token_rejected_on_encode(self):
with pytest.raises(InvalidCursorError, match="order must be one of"):
encode_cursor("created_at", "1", "id-1", order="sideways")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests-unit/assets_test/services/test_cursor.py` around lines 260 - 262, The
test_invalid_order_token_rejected_on_encode method is catching the wrong
exception type. The encode_cursor function raises InvalidCursorError when given
an unsupported order token like "sideways", not ValueError. Replace the
ValueError in the pytest.raises call with InvalidCursorError to match the actual
exception that encode_cursor throws.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants