Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
1f9e7df
[Partner Nodes] feat: add Krea 2 Medium Turbo model (#14280)
bigcat88 Jun 4, 2026
27b5c42
[Partner Nodes] feat: add seed input to Flux Erase node (#14283)
bigcat88 Jun 4, 2026
6ecca5f
chore: update workflow templates to v0.9.98 (#14284)
comfyui-wiki Jun 4, 2026
4e1f7cb
Bump comfyui-frontend-package to 1.45.15 (#14265)
comfy-pr-bot Jun 4, 2026
514bb8b
Fix ideogram if model dtype gets set to fp8. (#14291)
comfyanonymous Jun 5, 2026
ab0d8a9
Consolidate audio nodes into SaveAudioAdvanced node (CORE-202) (#13871)
alexisrolland Jun 5, 2026
5aa71b9
Enable cfg1 optimization for DualModelGuider with CFGGuider (#14290)
kijai Jun 5, 2026
410df27
Fix interoperation with external source of pinned memory pressure (#1…
rattus128 Jun 5, 2026
ec6aa97
aimdo 049 (#14300)
rattus128 Jun 5, 2026
4a00126
[Partner Nodes] feat: add new Gemini text node (#14299)
bigcat88 Jun 5, 2026
aeee53f
[Partner Nodes] feat: add temperature and top_p to NanoBanan node (#1…
bigcat88 Jun 5, 2026
2ef2cf1
feat: add PreviewGaussianSplat + PreviewPointCloud nodes (#14194)
jtydhr88 Jun 5, 2026
986ce5b
Update AMD portable readme. (#14303)
comfyanonymous Jun 5, 2026
a65a546
BE-1172 fix(3d): save Preview3DAdvanced / PreviewGaussianSplat / Prev…
jtydhr88 Jun 5, 2026
ea36cb1
feat(3d): reorder Preview3DAdvanced / PreviewGaussianSplat / PreviewP…
jtydhr88 Jun 6, 2026
2cdaaf4
Update line endings check to ignore .ci files. (#14319)
comfyanonymous Jun 7, 2026
739061d
Use windows line endings for windows portable readmes. (#14334)
comfyanonymous Jun 8, 2026
7863cf0
Add SeedVR2 support (CORE-6) (#14110)
pollockjj Jun 8, 2026
38f750d
chore: update embedded docs to v0.5.3 (#14350)
comfyui-wiki Jun 8, 2026
fc258b1
Add Color primitive (#14260)
kijai Jun 8, 2026
a1c434e
Improve ResolutionSelector (#14309)
silveroxides Jun 8, 2026
a0a055b
feat(assets): extract image dimensions at ingest and emit on asset re…
mattmillerai Jun 8, 2026
00b633f
Revert "Add SeedVR2 support (CORE-6) (#14110)" (#14359)
comfyanonymous Jun 8, 2026
cb9f639
chore(openapi): sync shared API contract from cloud@5273c30 (#14266)
comfy-pr-bot Jun 9, 2026
f899992
fix: Add back apply_rotary_emb for Qwen Image (#14364)
alexisrolland Jun 9, 2026
8ed7f45
Allow custom templates with Ideogram4 TE (#14374)
kijai Jun 9, 2026
1639dc7
main/server: Add --debug-hang (#14371)
rattus128 Jun 9, 2026
07c53f8
Add LoRA key mapping for LTXV/LTXAV models (#14349)
kelseyee Jun 9, 2026
184009c
feat: Add model support for SCAIL-2 (#14373)
kijai Jun 9, 2026
9fc6f5f
Move bg_removal_model input socket to first position for nicer displa…
alexisrolland Jun 9, 2026
6f01b24
mm: dont reset cast buffers in cleanup_models_gc() (#14372)
rattus128 Jun 9, 2026
ad56489
Ensure conditions are not trainable to avoid bugs (#14368)
KohakuBlueleaf Jun 9, 2026
f8e51b6
feat: Add Bernini-R model support (Wan video) (CORE-279) (#14216)
kijai Jun 9, 2026
5ece24e
Depth anything 3 (Core-135) (#13853)
Talmaj Jun 10, 2026
5fcf7a4
Always enable cuda malloc on cu130 and higher. (#14381)
comfyanonymous Jun 10, 2026
46d45aa
chore(openapi): sync shared API contract from cloud@ca12913 (#14367)
comfy-pr-bot Jun 10, 2026
f350acd
[Trainer/bug] Ensure model is not inference mode (CORE-72) (#13400)
KohakuBlueleaf Jun 10, 2026
a76bb43
chore(assets): drop vestigial tags.tag_type column (#14248)
mattmillerai Jun 10, 2026
84e0692
feat(assets): cursor-based pagination on GET /api/assets (#14014)
mattmillerai Jun 10, 2026
039ed38
fix(assets): remove unused delete_content param from deleteAsset (#14…
mattmillerai Jun 10, 2026
6d18f4a
main: force cudnn.benchmark to false (#14390)
rattus128 Jun 10, 2026
e5b7140
feat(assets): add job_ids filter to GET /api/assets (#13998)
mattmillerai Jun 10, 2026
ce200c0
feat(assets): include asset id in executed WebSocket message (#13862)
mattmillerai Jun 11, 2026
431a188
revert(assets): drop job_ids filter from GET /api/assets (#14408)
mattmillerai Jun 11, 2026
74ee826
chore(openapi): sync shared API contract from cloud@e3c52ad (#14406)
comfy-pr-bot Jun 11, 2026
33e6ebd
I don't think this actually works anymore. (#14403)
comfyanonymous Jun 11, 2026
bda19b2
ops: tolerate already force casted dynamic weight (#14410)
rattus128 Jun 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .ci/windows_amd_base_files/README_VERY_IMPORTANT.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
As of the time of writing this you need this driver for best results:
https://www.amd.com/en/resources/support-articles/release-notes/RN-AMDGPU-WINDOWS-PYTORCH-7-1-1.html
As of the time of writing this you need a recent driver. Updating to the latest driver is recommended.

HOW TO RUN:

If you have a AMD gpu:

run_amd_gpu.bat

If you have memory issues you can try disabling the smart memory management by running comfyui with:
If you have memory issues you can try enabling the new dynamic memory management by running comfyui with:

run_amd_gpu_disable_smart_memory.bat
run_amd_gpu_enable_dynamic_vram.bat

IF YOU GET A RED ERROR IN THE UI MAKE SURE YOU HAVE A MODEL/CHECKPOINT IN: ComfyUI\models\checkpoints

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/check-line-endings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ jobs:
- name: Check for Windows line endings (CRLF)
run: |
# Get the list of changed files in the PR
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }})
CHANGED_FILES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} -- ':!.ci')
# Flag to track if CRLF is found
CRLF_FOUND=false
Expand Down
10 changes: 0 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -462,16 +462,6 @@ To use the most up-to-date frontend version:

This approach allows you to easily switch between the stable fortnightly release and the cutting-edge daily updates, or even specific versions for testing purposes.

### Accessing the Legacy Frontend

If you need to use the legacy frontend for any reason, you can access it using the following command line argument:

```
--front-end-version Comfy-Org/ComfyUI_legacy_frontend@latest
```

This will use a snapshot of the legacy frontend preserved in the [ComfyUI Legacy Frontend repository](https://github.com/Comfy-Org/ComfyUI_legacy_frontend).

# QA

### Which GPU should I buy for this?
Expand Down
39 changes: 39 additions & 0 deletions alembic_db/versions/0004_drop_tag_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""
Drop the vestigial tags.tag_type column.

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

Revision ID: 0004_drop_tag_type
Revises: 0003_add_metadata_job_id
Create Date: 2026-06-03
"""

from alembic import op
import sqlalchemy as sa

revision = "0004_drop_tag_type"
down_revision = "0003_add_metadata_job_id"
branch_labels = None
depends_on = None


def upgrade() -> None:
with op.batch_alter_table("tags") as batch_op:
batch_op.drop_index("ix_tags_tag_type")
batch_op.drop_column("tag_type")


def downgrade() -> None:
with op.batch_alter_table("tags") as batch_op:
batch_op.add_column(
sa.Column(
"tag_type",
sa.String(length=32),
nullable=False,
server_default="user",
)
)
batch_op.create_index("ix_tags_tag_type", ["tag_type"])
54 changes: 32 additions & 22 deletions app/assets/api/routes.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
update_asset_metadata,
upload_from_temp_path,
)
from app.assets.services.cursor import InvalidCursorError
from app.assets.services.tagging import list_tag_histogram

ROUTES = web.RouteTableDef()
Expand Down Expand Up @@ -174,7 +175,7 @@ def _build_asset_response(result: schemas.AssetDetailResult | schemas.UploadResu
user_metadata=result.ref.user_metadata or {},
metadata=result.ref.system_metadata,
job_id=result.ref.job_id,
prompt_id=result.ref.job_id, # deprecated: mirrors job_id for cloud compat
prompt_id=result.ref.job_id, # deprecated alias of job_id, kept for compatibility
created_at=result.ref.created_at,
updated_at=result.ref.updated_at,
last_access_time=result.ref.last_access_time,
Expand Down Expand Up @@ -211,24 +212,37 @@ async def list_assets_route(request: web.Request) -> web.Response:
order_candidate = (q.order or "desc").lower()
order = order_candidate if order_candidate in {"asc", "desc"} else "desc"

result = list_assets_page(
owner_id=USER_MANAGER.get_request_user_id(request),
include_tags=q.include_tags,
exclude_tags=q.exclude_tags,
name_contains=q.name_contains,
metadata_filter=q.metadata_filter,
limit=q.limit,
offset=q.offset,
sort=sort,
order=order,
)
try:
result = list_assets_page(
owner_id=USER_MANAGER.get_request_user_id(request),
include_tags=q.include_tags,
exclude_tags=q.exclude_tags,
name_contains=q.name_contains,
metadata_filter=q.metadata_filter,
limit=q.limit,
offset=q.offset,
sort=sort,
order=order,
after=q.after,
)
except InvalidCursorError as e:
return _build_error_response(400, "INVALID_CURSOR", str(e))

summaries = [_build_asset_response(item) for item in result.items]

# has_more semantics differ by mode:
# - cursor mode: a non-empty next_cursor means there are more results.
# - offset mode: derived from total - (offset + page size).
if q.after is not None:
has_more = result.next_cursor is not None
else:
has_more = (q.offset + len(summaries)) < result.total

payload = schemas_out.AssetsList(
assets=summaries,
total=result.total,
has_more=(q.offset + len(summaries)) < result.total,
has_more=has_more,
next_cursor=result.next_cursor,
)
return web.json_response(payload.model_dump(mode="json", exclude_none=True))

Expand Down Expand Up @@ -519,18 +533,14 @@ async def update_asset_route(request: web.Request) -> web.Response:
@_require_assets_feature_enabled
async def delete_asset_route(request: web.Request) -> web.Response:
reference_id = str(uuid.UUID(request.match_info["id"]))
delete_content_param = request.query.get("delete_content")
delete_content = (
False
if delete_content_param is None
else delete_content_param.lower() not in {"0", "false", "no"}
)

try:
# Deleting an asset is a soft delete of the reference; the underlying
# content is preserved (it may be shared with other references).
deleted = delete_asset_reference(
reference_id=reference_id,
owner_id=USER_MANAGER.get_request_user_id(request),
delete_content_if_orphan=delete_content,
delete_content_if_orphan=False,
)
except Exception:
logging.exception(
Expand Down Expand Up @@ -575,8 +585,8 @@ async def get_tags(request: web.Request) -> web.Response:
)

tags = [
schemas_out.TagUsage(name=name, count=count, type=tag_type)
for (name, tag_type, count) in rows
schemas_out.TagUsage(name=name, count=count)
for (name, count) in rows
]
payload = schemas_out.TagsList(
tags=tags, total=total, has_more=(query.offset + len(tags)) < total
Expand Down
5 changes: 5 additions & 0 deletions app/assets/api/schemas_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ class ListAssetsQuery(BaseModel):

limit: conint(ge=1, le=500) = 20
offset: conint(ge=0) = 0
# Opaque keyset cursor. When supplied, `offset` is ignored. Cursor pagination
# is supported for sort values `created_at`, `updated_at`, `name`, `size`.
# Supplying `after` together with `sort=last_access_time` returns
# 400 INVALID_CURSOR; that sort only supports offset/limit.
after: str | None = None

sort: Literal["name", "created_at", "updated_at", "size", "last_access_time"] = (
"created_at"
Expand Down
3 changes: 2 additions & 1 deletion app/assets/api/schemas_out.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ class AssetsList(BaseModel):
assets: list[Asset]
total: int
has_more: bool
# Opaque cursor for the next page. Omitted when there are no more results.
next_cursor: str | None = None


class TagUsage(BaseModel):
name: str
count: int
type: str


class TagsList(BaseModel):
Expand Down
3 changes: 0 additions & 3 deletions app/assets/database/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,6 @@ class Tag(Base):
__tablename__ = "tags"

name: Mapped[str] = mapped_column(String(512), primary_key=True)
tag_type: Mapped[str] = mapped_column(String(32), nullable=False, default="user")

asset_reference_links: Mapped[list[AssetReferenceTag]] = relationship(
back_populates="tag",
Expand All @@ -240,7 +239,5 @@ class Tag(Base):
overlaps="asset_reference_links,tag_links,tags,asset_reference",
)

__table_args__ = (Index("ix_tags_tag_type", "tag_type"),)

def __repr__(self) -> str:
return f"<Tag {self.name}>"
35 changes: 33 additions & 2 deletions app/assets/database/queries/asset_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -266,9 +266,18 @@ def list_references_page(
metadata_filter: dict | None = None,
sort: str | None = None,
order: str | None = None,
after_cursor_value: object | None = None,
after_cursor_id: str | None = None,
) -> tuple[list[AssetReference], dict[str, list[str]], int]:
"""List references with pagination, filtering, and sorting.
When ``after_cursor_value``/``after_cursor_id`` are supplied the query uses
keyset pagination — ``offset`` is ignored and a WHERE clause selects rows
strictly after the given ``(sort_col, id)`` position in the active sort
direction. The cursor value must already be typed for the column
(datetime for time sorts, int for size, str for name); the caller decodes
the opaque cursor string and resolves to the typed value.
Returns (references, tag_map, total_count).
"""
base = (
Expand Down Expand Up @@ -297,9 +306,31 @@ def list_references_page(
"size": Asset.size_bytes,
}
sort_col = sort_map.get(sort, AssetReference.created_at)
sort_exp = sort_col.desc() if order == "desc" else sort_col.asc()
descending = order == "desc"

# 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)

# Secondary ORDER BY id (matching the primary direction) gives the keyset
# comparison a deterministic tiebreaker on duplicate sort_col values.
id_exp = AssetReference.id.desc() if descending else AssetReference.id.asc()
sort_exp = sort_col.desc() if descending else sort_col.asc()

base = base.order_by(sort_exp).limit(limit).offset(offset)
base = base.order_by(sort_exp, id_exp).limit(limit)
if after_cursor_id is None:
base = base.offset(offset)

count_stmt = (
select(sa.func.count())
Expand Down
13 changes: 5 additions & 8 deletions app/assets/database/queries/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,13 +55,11 @@ def validate_tags_exist(session: Session, tags: list[str]) -> None:
raise ValueError(f"Unknown tags: {missing}")


def ensure_tags_exist(
session: Session, names: Iterable[str], tag_type: str = "user"
) -> None:
def ensure_tags_exist(session: Session, names: Iterable[str]) -> None:
wanted = normalize_tags(list(names))
if not wanted:
return
rows = [{"name": n, "tag_type": tag_type} for n in list(dict.fromkeys(wanted))]
rows = [{"name": n} for n in list(dict.fromkeys(wanted))]
ins = (
sqlite.insert(Tag)
.values(rows)
Expand Down Expand Up @@ -97,7 +95,7 @@ def set_reference_tags(
to_remove = [t for t in current if t not in desired]

if to_add:
ensure_tags_exist(session, to_add, tag_type="user")
ensure_tags_exist(session, to_add)
session.add_all(
[
AssetReferenceTag(
Expand Down Expand Up @@ -142,7 +140,7 @@ def add_tags_to_reference(
return AddTagsResult(added=[], already_present=[], total_tags=total)

if create_if_missing:
ensure_tags_exist(session, norm, tag_type="user")
ensure_tags_exist(session, norm)

current = set(get_reference_tags(session, reference_id))

Expand Down Expand Up @@ -289,7 +287,6 @@ def list_tags_with_usage(
q = (
select(
Tag.name,
Tag.tag_type,
func.coalesce(counts_sq.c.cnt, 0).label("count"),
)
.select_from(Tag)
Expand Down Expand Up @@ -331,7 +328,7 @@ def list_tags_with_usage(
rows = (session.execute(q.limit(limit).offset(offset))).all()
total = (session.execute(total_q)).scalar_one()

rows_norm = [(name, ttype, int(count or 0)) for (name, ttype, count) in rows]
rows_norm = [(name, int(count or 0)) for (name, count) in rows]
return rows_norm, int(total or 0)


Expand Down
7 changes: 6 additions & 1 deletion app/assets/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
verify_file_unchanged,
)
from app.assets.services.hashing import HashCheckpoint, compute_blake3_hash
from app.assets.services.image_dimensions import extract_image_dimensions
from app.assets.services.metadata_extract import extract_file_metadata
from app.assets.services.path_utils import (
compute_relative_filename,
Expand Down Expand Up @@ -354,7 +355,7 @@ def insert_asset_specs(specs: list[SeedAssetSpec], tag_pool: set[str]) -> int:
return 0
with create_session() as sess:
if tag_pool:
ensure_tags_exist(sess, tag_pool, tag_type="user")
ensure_tags_exist(sess, tag_pool)
result = batch_insert_seed_assets(sess, specs=specs, owner_id="")
sess.commit()
return result.inserted_refs
Expand Down Expand Up @@ -506,6 +507,10 @@ def enrich_asset(

if extract_metadata and metadata:
system_metadata = metadata.to_user_metadata()
if mime_type and mime_type.startswith("image/"):
dims = extract_image_dimensions(file_path, mime_type=mime_type)
if dims:
system_metadata.update(dims)
set_reference_system_metadata(session, reference_id, system_metadata)

if full_hash:
Expand Down
Loading
Loading