From f9e8ce3eb4befb114528452061ea16d8eb3d8658 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 4 Jun 2026 19:48:12 +0200 Subject: [PATCH] Add Local Database status and update feature for model-list.json Add backend endpoints (/model_linker/model-list/status, /model_linker/model-list/update) that let users check whether their bundled ComfyUI-Manager model-list.json is current and download the latest version from GitHub, with SHA tracking and cache invalidation. Add corresponding UI in the Options dialog under a new 'Local Database' section with status display, remote check, and update controls. --- __init__.py | 31 ++++ core/sources/model_list.py | 167 +++++++++++++++++++++ metadata/model-list.json | 268 +++++++++++++++++++++++++--------- metadata/model-list.meta.json | 9 ++ web/css/linker-main.css | 45 ++++++ web/linker.js | 164 ++++++++++++++++++++- 6 files changed, 613 insertions(+), 71 deletions(-) create mode 100644 metadata/model-list.meta.json diff --git a/__init__.py b/__init__.py index 5fd4a66..f960e05 100644 --- a/__init__.py +++ b/__init__.py @@ -94,6 +94,8 @@ class ModelLinkerExtension: from .core.sources.model_list import ( search_model_list, search_model_list_multiple, + get_model_list_update_status, + update_model_list_from_remote, ) from .core.sources.huggingface import ( search_huggingface_for_file, @@ -1266,6 +1268,35 @@ class ModelLinkerExtension: log_exception(f"Clear search cache error: {e}") return web.json_response({"error": str(e)}, status=500) + @routes.get("/model_linker/model-list/status") + async def model_list_status_route(request): + """Return local model-list status and optionally compare with GitHub.""" + try: + check_remote = ( + str(request.query.get("check_remote", "")).lower() + in {"1", "true", "yes"} + ) + return web.json_response( + get_model_list_update_status(check_remote=check_remote) + ) + except Exception as e: + log_exception(f"Model list status error: {e}") + return web.json_response({"error": str(e)}, status=500) + + @routes.post("/model_linker/model-list/update") + async def model_list_update_route(request): + """Download latest ComfyUI-Manager model-list.json.""" + try: + result = await asyncio.to_thread(update_model_list_from_remote) + clear_huggingface_search_cache() + clear_civitai_search_cache() + clear_civarchive_search_cache() + clear_lora_manager_archive_search_cache() + return web.json_response(result) + except Exception as e: + log_exception(f"Model list update error: {e}") + return web.json_response({"error": str(e)}, status=500) + @routes.post("/model_linker/download") async def download_model(request): """Start downloading a model.""" diff --git a/core/sources/model_list.py b/core/sources/model_list.py index 0c734b2..c1c28cf 100644 --- a/core/sources/model_list.py +++ b/core/sources/model_list.py @@ -6,8 +6,13 @@ Search the ComfyUI Manager model-list.json database with fuzzy matching. import os import json +import hashlib +import shutil +import tempfile +from datetime import datetime, timezone from typing import Dict, Any, Optional, List from difflib import SequenceMatcher +from urllib.request import Request, urlopen from ..log_system.log_funcs import ( log_debug, @@ -22,11 +27,102 @@ METADATA_DIR = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "metadata" ) MODEL_LIST_FILE = os.path.join(METADATA_DIR, "model-list.json") +MODEL_LIST_META_FILE = os.path.join(METADATA_DIR, "model-list.meta.json") +MODEL_LIST_SOURCE_URL = ( + "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/model-list.json" +) +MODEL_LIST_GITHUB_API_URL = ( + "https://api.github.com/repos/Comfy-Org/ComfyUI-Manager/contents/model-list.json?ref=main" +) +HTTP_TIMEOUT = 30 # Cache for loaded data _model_list_cache: Optional[List[Dict]] = None +def _utc_now_iso() -> str: + return datetime.now(timezone.utc).replace(microsecond=0).isoformat() + + +def _fetch_json_url(url: str) -> Dict[str, Any]: + request = Request( + url, + headers={ + "Accept": "application/json", + "User-Agent": "comfyui-model-linker", + }, + ) + with urlopen(request, timeout=HTTP_TIMEOUT) as response: + return json.loads(response.read().decode("utf-8")) + + +def _read_json_file(path: str, default: Any) -> Any: + try: + if os.path.exists(path): + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + except Exception as e: + log_warn(f"Error reading {os.path.basename(path)}: {e}") + return default + + +def _write_json_file_atomic(path: str, data: Any): + os.makedirs(os.path.dirname(path), exist_ok=True) + fd, tmp_path = tempfile.mkstemp( + prefix=f".{os.path.basename(path)}.", + suffix=".tmp", + dir=os.path.dirname(path), + text=True, + ) + try: + with os.fdopen(fd, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + f.write("\n") + os.replace(tmp_path, path) + except Exception: + try: + os.remove(tmp_path) + except OSError: + pass + raise + + +def _read_model_list_file() -> Dict[str, Any]: + return _read_json_file(MODEL_LIST_FILE, {"models": []}) + + +def _read_model_list_meta() -> Dict[str, Any]: + return _read_json_file(MODEL_LIST_META_FILE, {}) + + +def _get_local_model_list_sha() -> str: + try: + with open(MODEL_LIST_FILE, "rb") as f: + content = f.read() + header = f"blob {len(content)}\0".encode("utf-8") + return hashlib.sha1(header + content).hexdigest() + except Exception as e: + log_warn(f"Error calculating model-list.json SHA: {e}") + return "" + + +def _get_remote_model_list_info() -> Dict[str, Any]: + data = _fetch_json_url(MODEL_LIST_GITHUB_API_URL) + return { + "sha": data.get("sha") or "", + "size": data.get("size") or 0, + "download_url": data.get("download_url") or MODEL_LIST_SOURCE_URL, + "html_url": data.get("html_url") or "https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/model-list.json", + } + + +def reload_model_list(): + """Clear in-memory cache so the next search reads model-list.json again.""" + global _model_list_cache + _model_list_cache = None + _load_model_list() + + def _load_model_list() -> List[Dict]: """Load model list database.""" global _model_list_cache @@ -50,6 +146,77 @@ def _load_model_list() -> List[Dict]: return _model_list_cache +def get_model_list_update_status(check_remote: bool = False) -> Dict[str, Any]: + """Return local model-list metadata and optionally compare it with GitHub.""" + data = _read_model_list_file() + models = data.get("models", []) if isinstance(data, dict) else [] + meta = _read_model_list_meta() + local_sha = meta.get("sha") or _get_local_model_list_sha() + local_updated_at = meta.get("updated_at") or "" + + status = { + "source_url": MODEL_LIST_SOURCE_URL, + "github_url": "https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/model-list.json", + "local_count": len(models) if isinstance(models, list) else 0, + "local_sha": local_sha, + "local_updated_at": local_updated_at, + "remote_sha": "", + "remote_size": 0, + "remote_checked_at": "", + "update_available": False, + "can_compare": bool(local_sha), + } + + if check_remote: + remote = _get_remote_model_list_info() + remote_sha = remote.get("sha") or "" + status.update( + { + "remote_sha": remote_sha, + "remote_size": remote.get("size") or 0, + "remote_checked_at": _utc_now_iso(), + "update_available": bool(remote_sha and local_sha and remote_sha != local_sha), + "can_compare": bool(local_sha and remote_sha), + } + ) + + return status + + +def update_model_list_from_remote() -> Dict[str, Any]: + """Download the latest ComfyUI-Manager model-list.json and refresh cache.""" + remote = _get_remote_model_list_info() + download_url = remote.get("download_url") or MODEL_LIST_SOURCE_URL + data = _fetch_json_url(download_url) + models = data.get("models", []) if isinstance(data, dict) else [] + if not isinstance(models, list) or not models: + raise ValueError("Downloaded model-list.json does not contain a non-empty models list") + + if os.path.exists(MODEL_LIST_FILE): + shutil.copy2(MODEL_LIST_FILE, f"{MODEL_LIST_FILE}.bak") + + _write_json_file_atomic(MODEL_LIST_FILE, data) + meta = { + "source": "Comfy-Org/ComfyUI-Manager", + "source_url": MODEL_LIST_SOURCE_URL, + "github_url": remote.get("html_url") + or "https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/model-list.json", + "sha": remote.get("sha") or "", + "size": remote.get("size") or 0, + "model_count": len(models), + "updated_at": _utc_now_iso(), + } + _write_json_file_atomic(MODEL_LIST_META_FILE, meta) + reload_model_list() + + return { + **get_model_list_update_status(check_remote=False), + "updated": True, + "remote_sha": meta["sha"], + "remote_size": meta["size"], + } + + def _normalize_filename(filename: str) -> str: """Normalize filename for comparison.""" # Remove extension diff --git a/metadata/model-list.json b/metadata/model-list.json index 108bf0f..db288bf 100644 --- a/metadata/model-list.json +++ b/metadata/model-list.json @@ -572,7 +572,6 @@ "url": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_offset_example-lora_1.0.safetensors", "size": "49.6MB" }, - { "name": "SDXL Lightning LoRA (2steps)", "type": "lora", @@ -606,7 +605,6 @@ "url": "https://huggingface.co/ByteDance/SDXL-Lightning/resolve/main/sdxl_lightning_8step_lora.safetensors", "size": "393.9MB" }, - { "name": "DMD2 LoRA (4steps)", "type": "lora", @@ -629,7 +627,6 @@ "url": "https://huggingface.co/tianweiy/DMD2/resolve/main/dmd2_sdxl_4step_lora_fp16.safetensors", "size": "394MB" }, - { "name": "Hyper-SD LoRA (8steps) - FLUX.1 [Dev]", "type": "lora", @@ -652,7 +649,6 @@ "url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-FLUX.1-dev-16steps-lora.safetensors", "size": "1.39GB" }, - { "name": "Hyper-SD LoRA (1step) - SD1.5", "type": "lora", @@ -719,7 +715,6 @@ "url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD15-12steps-CFG-lora.safetensors", "size": "269MB" }, - { "name": "Hyper-SD LoRA (1step) - SDXL", "type": "lora", @@ -786,7 +781,6 @@ "url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-12steps-CFG-lora.safetensors", "size": "787MB" }, - { "name": "Hyper-SD CFG LoRA (4steps) - SD3", "type": "lora", @@ -820,7 +814,6 @@ "url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SD3-16steps-CFG-lora.safetensors", "size": "472MB" }, - { "name": "comfyanonymous/flux_text_encoders - t5xxl (fp16)", "type": "clip", @@ -854,7 +847,6 @@ "url": "https://huggingface.co/comfyanonymous/flux_text_encoders/resolve/main/t5xxl_fp8_e4m3fn_scaled.safetensors", "size": "5.16GB" }, - { "name": "comfyanonymous/cosmos_cv8x8x8_1.0.safetensors", "type": "VAE", @@ -910,7 +902,6 @@ "url": "https://huggingface.co/mcmonkey/cosmos-1.0/resolve/main/Cosmos-1_0-Diffusion-14B-Video2World.safetensors", "size": "28.5GB" }, - { "name": "google-t5/t5-base", "type": "clip", @@ -944,8 +935,6 @@ "url": "https://huggingface.co/mcmonkey/google_t5-v1_1-xxl_encoderonly/resolve/main/t5xxl_fp8_e4m3fn.safetensors", "size": "4.89GB" }, - - { "name": "city96/t5-v1_1-xxl-encoder-Q3_K_L.gguf", "type": "clip", @@ -1067,7 +1056,6 @@ "url": "https://huggingface.co/city96/t5-v1_1-xxl-encoder-gguf/resolve/main/t5-v1_1-xxl-encoder-f32.gguf", "size": "19.1GB" }, - { "name": "Comfy-Org/clip_l", "type": "clip", @@ -1090,7 +1078,6 @@ "url": "https://huggingface.co/Comfy-Org/stable-diffusion-3.5-fp8/resolve/main/text_encoders/clip_g.safetensors", "size": "1.39GB" }, - { "name": "v1-5-pruned-emaonly.ckpt", "type": "checkpoint", @@ -1553,7 +1540,6 @@ "url": "https://huggingface.co/CiaraRowles/controlnet-temporalnet-sdxl-1.0/resolve/main/diffusion_pytorch_model.safetensors", "size": "5.00GB" }, - { "name": "Comfy-Org/sigclip_vision_384 (patch14_384)", "type": "clip_vision", @@ -1565,7 +1551,6 @@ "url": "https://huggingface.co/Comfy-Org/sigclip_vision_384/resolve/main/sigclip_vision_patch14_384.safetensors", "size": "857MB" }, - { "name": "CLIPVision model (stabilityai/clip_vision_g)", "type": "clip_vision", @@ -1973,7 +1958,6 @@ "url": "https://dl.fbaipublicfiles.com/segment_anything/sam_vit_b_01ec64.pth", "size": "375.0MB" }, - { "name": "sam2.1_hiera_tiny.pt", "type": "sam2.1", @@ -2018,7 +2002,6 @@ "url": "https://dl.fbaipublicfiles.com/segment_anything_2/092824/sam2.1_hiera_large.pt", "size": "857.0MB" }, - { "name": "sam2_hiera_tiny.pt", "type": "sam2", @@ -2063,7 +2046,6 @@ "url": "https://dl.fbaipublicfiles.com/segment_anything_2/072824/sam2_hiera_large.pt", "size": "857.0MB" }, - { "name": "seecoder v1.0", "type": "seecoder", @@ -2394,7 +2376,6 @@ "url": "https://huggingface.co/Leoxing/PIA/resolve/main/pia.ckpt", "size": "1.67GB" }, - { "name": "animatediff/v2_lora_PanLeft.ckpt (ComfyUI-AnimateDiff-Evolved) (Updated path)", "type": "motion lora", @@ -2879,9 +2860,6 @@ "url": "https://huggingface.co/Kwai-Kolors/Kolors-IP-Adapter-FaceID-Plus/resolve/main/ipa-faceid-plus.bin", "size": "2.39GB" }, - - - { "name": "GFPGANv1.4.pth", "type": "GFPGAN", @@ -3509,7 +3487,6 @@ "url": "https://huggingface.co/ViperYX/RGT/resolve/main/RGT_S/RGT_S_x4.pth", "size": "136.0MB" }, - { "name": "InstantX/FLUX.1-dev Controlnet (Union)", "type": "controlnet", @@ -3521,8 +3498,6 @@ "url": "https://huggingface.co/InstantX/FLUX.1-dev-Controlnet-Union/resolve/main/diffusion_pytorch_model.safetensors", "size": "6.6GB" }, - - { "name": "InstantX/FLUX.1-dev-IP-Adapter", "type": "IP-Adapter", @@ -3556,7 +3531,6 @@ "url": "https://huggingface.co/Kijai/flux-fp8/resolve/main/flux_shakker_labs_union_pro-fp8_e4m3fn.safetensors", "size": "3.3GB" }, - { "name": "jasperai/FLUX.1-dev-Controlnet-Upscaler", "type": "controlnet", @@ -3590,7 +3564,6 @@ "url": "https://huggingface.co/jasperai/Flux.1-dev-Controlnet-Surface-Normals/resolve/main/diffusion_pytorch_model.safetensors", "size": "3.58GB" }, - { "name": "xinsir/ControlNet++: All-in-one ControlNet", "type": "controlnet", @@ -3690,7 +3663,6 @@ "url": "https://huggingface.co/xinsir/controlnet-tile-sdxl-1.0/resolve/main/diffusion_pytorch_model.safetensors", "size": "2.50GB" }, - { "name": "InstantX/SD3-Controlnet-Canny", "type": "controlnet", @@ -3724,8 +3696,6 @@ "url": "https://huggingface.co/InstantX/SD3-Controlnet-Tile/resolve/main/diffusion_pytorch_model.safetensors", "size": "1.19GB" }, - - { "name": "stabilityai/SD3.5-Large-Controlnet-Blur", "type": "controlnet", @@ -3759,7 +3729,6 @@ "url": "https://huggingface.co/stabilityai/stable-diffusion-3.5-controlnets/resolve/main/sd3.5_large_controlnet_depth.safetensors", "size": "8.65GB" }, - { "name": "Kijai/ToonCrafter model checkpoint (interpolation fp16)", "type": "checkpoint", @@ -3947,7 +3916,6 @@ "url": "https://huggingface.co/Kijai/DepthAnythingV2-safetensors/resolve/main/depth_anything_v2_vits_fp32.safetensors", "size": "99.2MB" }, - { "name": "PixArt-Sigma-XL-2-1024-MS.pth (checkpoint)", "type": "checkpoint", @@ -3959,7 +3927,6 @@ "url": "https://huggingface.co/PixArt-alpha/PixArt-Sigma/resolve/main/PixArt-Sigma-XL-2-1024-MS.pth", "size": "2.47GB" }, - { "name": "PixArt-Sigma-XL-2-512-MS.safetensors (diffusion)", "type": "diffusion_model", @@ -3993,8 +3960,6 @@ "url": "https://huggingface.co/PixArt-alpha/PixArt-XL-2-1024-MS/resolve/main/transformer/diffusion_pytorch_model.safetensors", "size": "2.45GB" }, - - { "name": "hunyuan_dit_1.2.safetensors", "type": "checkpoint", @@ -4028,7 +3993,6 @@ "url": "https://huggingface.co/comfyanonymous/hunyuan_dit_comfyui/resolve/main/hunyuan_dit_1.0.safetensors", "size": "8.24GB" }, - { "name": "Comfy-Org/hunyuan_video_t2v_720p_bf16.safetensors", "type": "diffusion_model", @@ -4062,7 +4026,6 @@ "url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/diffusion_models/hunyuan_video_image_to_video_720p_bf16.safetensors", "size": "25.6GB" }, - { "name": "Comfy-Org/llava_llama3_fp8_scaled.safetensors", "type": "clip", @@ -4096,7 +4059,6 @@ "url": "https://huggingface.co/Comfy-Org/HunyuanVideo_repackaged/resolve/main/split_files/clip_vision/llava_llama3_vision.safetensors", "size": "649MB" }, - { "name": "Comfy-Org/omnigen2_fp16.safetensors", "type": "diffusion_model", @@ -4119,7 +4081,6 @@ "url": "https://huggingface.co/Comfy-Org/Omnigen2_ComfyUI_repackaged/resolve/main/split_files/text_encoders/qwen_2.5_vl_fp16.safetensors", "size": "7.51GB" }, - { "name": "FLUX.1 [Schnell] Diffusion model", "type": "diffusion_model", @@ -4131,7 +4092,6 @@ "url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/flux1-schnell.safetensors", "size": "23.8GB" }, - { "name": "FLUX.1 VAE model", "type": "VAE", @@ -4143,7 +4103,6 @@ "url": "https://huggingface.co/black-forest-labs/FLUX.1-schnell/resolve/main/ae.safetensors", "size": "335MB" }, - { "name": "kijai/FLUX.1 [schnell] Diffusion model (float8_e4m3fn)", "type": "diffusion_model", @@ -4155,7 +4114,6 @@ "url": "https://huggingface.co/Kijai/flux-fp8/resolve/main/flux1-schnell-fp8.safetensors", "size": "11.9GB" }, - { "name": "FLUX.1 [Dev] Diffusion model (scaled fp8)", "type": "diffusion_model", @@ -4178,7 +4136,6 @@ "url": "https://huggingface.co/Kijai/flux-fp8/resolve/main/flux1-dev-fp8.safetensors", "size": "11.9GB" }, - { "name": "Comfy Org/FLUX.1 [dev] Checkpoint model (fp8)", "type": "checkpoint", @@ -4201,7 +4158,6 @@ "url": "https://huggingface.co/Comfy-Org/flux1-schnell/resolve/main/flux1-schnell-fp8.safetensors", "size": "17.2GB" }, - { "name": "city96/flux1-dev-F16.gguf", "type": "diffusion_model", @@ -4323,7 +4279,6 @@ "url": "https://huggingface.co/city96/FLUX.1-dev-gguf/resolve/main/flux1-dev-Q8_0.gguf", "size": "12.7GB" }, - { "name": "city96/flux1-schnell-F16.gguf", "type": "diffusion_model", @@ -4445,7 +4400,6 @@ "url": "https://huggingface.co/city96/FLUX.1-schnell-gguf/resolve/main/flux1-schnell-Q8_0.gguf", "size": "12.7GB" }, - { "name": "ViT-L-14-TEXT-detail-improved-hiT-GmP-HF.safetensors [Long CLIP L]", "type": "clip", @@ -4468,7 +4422,6 @@ "url": "https://huggingface.co/zer0int/CLIP-GmP-ViT-L-14/resolve/main/ViT-L-14-TEXT-detail-improved-hiT-GmP-TE-only-HF.safetensors", "size": "323MB" }, - { "name": "Depth Pro model", "type": "depth-pro", @@ -4480,7 +4433,6 @@ "url": "https://huggingface.co/spacepxl/ml-depth-pro/resolve/main/depth_pro.fp16.safetensors", "size": "1.9GB" }, - { "name": "kijai/lotus depth d model v1.1 (fp16)", "type": "diffusion_model", @@ -4558,7 +4510,6 @@ "url": "https://huggingface.co/Kijai/lotus-comfyui/resolve/main/lotus-normal-g-v1-0.safetensors", "size": "3.47GB" }, - { "name": "Kolors UNet model", "type": "diffusion_model", @@ -4614,7 +4565,6 @@ "url": "https://huggingface.co/Kijai/ChatGLM3-safetensors/resolve/main/chatglm3-fp16.safetensors", "size": "12.52GB" }, - { "name": "pulid_flux_v0.9.1.safetensors", "type": "PuLID", @@ -4637,7 +4587,6 @@ "url": "https://huggingface.co/guozinan/PuLID/resolve/main/pulid_v1.1.safetensors", "size": "984MB" }, - { "name": "kijai/MoGe_ViT_L_fp16.safetensors", "type": "MoGe", @@ -4660,7 +4609,6 @@ "url": "https://huggingface.co/Kijai/MoGe_safetensors/resolve/main/MoGe_ViT_L_fp16.safetensors", "size": "1.26GB" }, - { "name": "LTX-Video 2B v0.9 Checkpoint", "type": "checkpoint", @@ -4694,7 +4642,6 @@ "url": "https://huggingface.co/Lightricks/LTX-Video/resolve/main/ltx-video-2b-v0.9.5.safetensors", "size": "6.34GB" }, - { "name": "XLabs-AI/flux-canny-controlnet-v3.safetensors", "type": "controlnet", @@ -4728,7 +4675,6 @@ "url": "https://huggingface.co/XLabs-AI/flux-controlnet-collections/resolve/main/flux-hed-controlnet-v3.safetensors", "size": "1.49GB" }, - { "name": "XLabs-AI/realism_lora.safetensors", "type": "lora", @@ -4762,7 +4708,6 @@ "url": "https://huggingface.co/XLabs-AI/flux-lora-collection/resolve/main/mjv6_lora.safetensors", "size": "44.8MB" }, - { "name": "XLabs-AI/flux-ip-adapter", "type": "lora", @@ -4774,7 +4719,6 @@ "url": "https://huggingface.co/XLabs-AI/flux-ip-adapter/resolve/main/ip_adapter.safetensors", "size": "982MB" }, - { "name": "efficient_sam_s_cpu.jit [ComfyUI-YoloWorld-EfficientSAM]", "type": "efficient_sam", @@ -4797,7 +4741,6 @@ "url": "https://huggingface.co/camenduru/YoloWorld-EfficientSAM/resolve/main/efficient_sam_s_gpu.jit", "size": "106.0MB" }, - { "name": "TencentARC/CustomNet V1", "type": "CustomNet", @@ -4820,7 +4763,6 @@ "url": "https://huggingface.co/TencentARC/CustomNet/resolve/main/customnet_inpaint_v1.pt", "size": "5.71GB" }, - { "name": "deepseek-ai/Janus-Pro-1B", "type": "Janus-Pro", @@ -4865,7 +4807,6 @@ "url": "https://huggingface.co/Kwai-Kolors/Kolors/resolve/main/vae/diffusion_pytorch_model.safetensors", "size": "335MB" }, - { "name": "Comfy-Org/Wan2.1 i2v 480p 14B (bf16)", "type": "diffusion_model", @@ -4954,7 +4895,6 @@ "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_i2v_720p_14B_fp8_scaled.safetensors", "size": "16.4GB" }, - { "name": "Comfy-Org/Wan2.1 t2v 1.3B (bf16)", "type": "diffusion_model", @@ -4977,7 +4917,6 @@ "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/diffusion_models/wan2.1_t2v_1.3B_fp16.safetensors", "size": "2.84GB" }, - { "name": "Comfy-Org/Wan2.1 t2v 14B (bf16)", "type": "diffusion_model", @@ -5044,7 +4983,6 @@ "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/clip_vision/clip_vision_h.safetensors", "size": "1.26GB" }, - { "name": "Comfy-Org/Wan2.2 i2v high noise 14B (fp16)", "type": "diffusion_model", @@ -5144,7 +5082,6 @@ "url": "https://huggingface.co/Comfy-Org/Wan_2.2_ComfyUI_Repackaged/resolve/main/split_files/diffusion_models/wan2.2_ti2v_5B_fp16.safetensors", "size": "10.0GB" }, - { "name": "Comfy-Org/umt5_xxl_fp16.safetensors", "type": "clip", @@ -5167,7 +5104,6 @@ "url": "https://huggingface.co/Comfy-Org/Wan_2.1_ComfyUI_repackaged/resolve/main/split_files/text_encoders/umt5_xxl_fp8_e4m3fn_scaled.safetensors", "size": "6.74GB" }, - { "name": "lllyasviel/FramePackI2V_HY", "type": "FramePackI2V", @@ -5179,7 +5115,204 @@ "url": "lllyasviel/FramePackI2V_HY", "size": "25.75GB" }, - + { + "name": "LTX-2 19B Dev FP8", + "type": "checkpoint", + "base": "LTX-2", + "save_path": "checkpoints/LTX-2", + "description": "LTX-2 19B Dev FP8 model.", + "reference": "https://huggingface.co/Lightricks/LTX-2", + "filename": "ltx-2-19b-dev-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev-fp8.safetensors", + "size": "27.1GB" + }, + { + "name": "LTX-2 19B Distilled FP8", + "type": "checkpoint", + "base": "LTX-2", + "save_path": "checkpoints/LTX-2", + "description": "LTX-2 19B Distilled FP8 model.", + "reference": "https://huggingface.co/Lightricks/LTX-2", + "filename": "ltx-2-19b-distilled-fp8.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-fp8.safetensors", + "size": "27.1GB" + }, + { + "name": "LTX-2 19B Dev", + "type": "checkpoint", + "base": "LTX-2", + "save_path": "checkpoints/LTX-2", + "description": "LTX-2 19B Dev model.", + "reference": "https://huggingface.co/Lightricks/LTX-2", + "filename": "ltx-2-19b-dev.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-dev.safetensors", + "size": "43.3GB" + }, + { + "name": "LTX-2 19B Distilled", + "type": "checkpoint", + "base": "LTX-2", + "save_path": "checkpoints/LTX-2", + "description": "LTX-2 19B Distilled model.", + "reference": "https://huggingface.co/Lightricks/LTX-2", + "filename": "ltx-2-19b-distilled.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled.safetensors", + "size": "43.3GB" + }, + { + "name": "LTX-2 Spatial Upscaler", + "type": "upscale", + "base": "upscale", + "save_path": "default", + "description": "Spatial upscaler model for LTX-2. This model enhances the spatial resolution of generated videos.", + "reference": "https://huggingface.co/Lightricks/LTX-2", + "filename": "ltx-2-spatial-upscaler-x2-1.0.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-spatial-upscaler-x2-1.0.safetensors", + "size": "996MB" + }, + { + "name": "LTX-2 Temporal Upscaler", + "type": "upscale", + "base": "upscale", + "save_path": "default", + "description": "Temporal upscaler model for LTX-2. This model enhances the temporal resolution of generated videos.", + "reference": "https://huggingface.co/Lightricks/LTX-2", + "filename": "ltx-2-temporal-upscaler-x2-1.0.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-temporal-upscaler-x2-1.0.safetensors", + "size": "262MB" + }, + { + "name": "LTX-2 19B Distilled LoRA", + "type": "lora", + "base": "LTX-2", + "save_path": "loras", + "description": "A LoRA adapter that transforms the standard LTX-2 19B model into a distilled version when loaded.", + "reference": "https://huggingface.co/Lightricks/LTX-2", + "filename": "ltx-2-19b-distilled-lora-384.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2/resolve/main/ltx-2-19b-distilled-lora-384.safetensors", + "size": "7.67GB" + }, + { + "name": "LTX-2 19B IC LoRA - Canny Control", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for canny control on LTX-2 19B IC model. Intended for advanced edge control and guided generation.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Canny-Control", + "filename": "ltx-2-19b-ic-lora-canny-control.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Canny-Control/resolve/main/ltx-2-19b-ic-lora-canny-control.safetensors", + "size": "654MB" + }, + { + "name": "LTX-2 19B IC LoRA - Depth Control", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for depth control on LTX-2 19B IC model. Adds depth-aware generation guidance.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Depth-Control", + "filename": "ltx-2-19b-ic-lora-depth-control.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Depth-Control/resolve/main/ltx-2-19b-ic-lora-depth-control.safetensors", + "size": "654MB" + }, + { + "name": "LTX-2 19B IC LoRA - Detailer", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA detailer for LTX-2 19B IC. Improves fine details and sharpness in generated outputs.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer", + "filename": "ltx-2-19b-ic-lora-detailer.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer/resolve/main/ltx-2-19b-ic-lora-detailer.safetensors", + "size": "2.62GB" + }, + { + "name": "LTX-2 19B IC LoRA - Pose Control", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for pose control on LTX-2 19B IC model. Enables pose-guided image/video generation.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control", + "filename": "ltx-2-19b-ic-lora-pose-control.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control/resolve/main/ltx-2-19b-ic-lora-pose-control.safetensors", + "size": "654MB" + }, + { + "name": "LTX-2 19B LoRA - Camera Control Dolly In", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for dolly-in camera control with LTX-2 19B. Simulates camera moving closer to subject.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In", + "filename": "ltx-2-19b-lora-camera-control-dolly-in.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In/resolve/main/ltx-2-19b-lora-camera-control-dolly-in.safetensors", + "size": "327MB" + }, + { + "name": "LTX-2 19B LoRA - Camera Control Dolly Left", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for dolly-left camera control with LTX-2 19B. Simulates camera moving left.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left", + "filename": "ltx-2-19b-lora-camera-control-dolly-left.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left/resolve/main/ltx-2-19b-lora-camera-control-dolly-left.safetensors", + "size": "327MB" + }, + { + "name": "LTX-2 19B LoRA - Camera Control Dolly Out", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for dolly-out camera control with LTX-2 19B. Simulates camera moving away from subject.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out", + "filename": "ltx-2-19b-lora-camera-control-dolly-out.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out/resolve/main/ltx-2-19b-lora-camera-control-dolly-out.safetensors", + "size": "327MB" + }, + { + "name": "LTX-2 19B LoRA - Camera Control Dolly Right", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for dolly-right camera control with LTX-2 19B. Simulates camera moving right.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right", + "filename": "ltx-2-19b-lora-camera-control-dolly-right.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right/resolve/main/ltx-2-19b-lora-camera-control-dolly-right.safetensors", + "size": "327MB" + }, + { + "name": "LTX-2 19B LoRA - Camera Control Jib Down", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for jib-down camera control with LTX-2 19B. Simulates vertical camera movement downwards.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down", + "filename": "ltx-2-19b-lora-camera-control-jib-down.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down/resolve/main/ltx-2-19b-lora-camera-control-jib-down.safetensors", + "size": "2.21GB" + }, + { + "name": "LTX-2 19B LoRA - Camera Control Jib Up", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for jib-up camera control with LTX-2 19B. Simulates vertical camera movement upwards.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up", + "filename": "ltx-2-19b-lora-camera-control-jib-up.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up/resolve/main/ltx-2-19b-lora-camera-control-jib-up.safetensors", + "size": "2.21GB" + }, + { + "name": "LTX-2 19B LoRA - Camera Control Static", + "type": "lora", + "base": "LTX-2", + "save_path": "loras/LTX-2", + "description": "LoRA for static camera control with LTX-2 19B. Simulates stationary/static camera view.", + "reference": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static", + "filename": "ltx-2-19b-lora-camera-control-static.safetensors", + "url": "https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static/resolve/main/ltx-2-19b-lora-camera-control-static.safetensors", + "size": "2.21GB" + }, { "name": "LTX-Video Spatial Upscaler v0.9.7", "type": "upscale", @@ -5422,7 +5555,6 @@ "url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_fp8_e4m3fn.safetensors", "size": "4.89GB" }, - { "name": "Qwen-Image-Edit 2509 Diffusion Model (bf16)", "type": "diffusion_model", @@ -5434,7 +5566,6 @@ "url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_2509_bf16.safetensors", "size": "9.78GB" }, - { "name": "Qwen-Image-Edit Diffusion Model (fp8_e4m3fn)", "type": "diffusion_model", @@ -5446,7 +5577,6 @@ "url": "https://huggingface.co/Comfy-Org/Qwen-Image-Edit_ComfyUI/resolve/main/split_files/diffusion_models/qwen_image_edit_fp8_e4m3fn.safetensors", "size": "4.89GB" }, - { "name": "Qwen-Image-Edit Diffusion Model (bf16)", "type": "diffusion_model", @@ -5668,4 +5798,4 @@ "size": "2.54GB" } ] -} \ No newline at end of file +} diff --git a/metadata/model-list.meta.json b/metadata/model-list.meta.json new file mode 100644 index 0000000..a21bd10 --- /dev/null +++ b/metadata/model-list.meta.json @@ -0,0 +1,9 @@ +{ + "source": "Comfy-Org/ComfyUI-Manager", + "source_url": "https://raw.githubusercontent.com/Comfy-Org/ComfyUI-Manager/main/model-list.json", + "github_url": "https://github.com/Comfy-Org/ComfyUI-Manager/blob/main/model-list.json", + "sha": "5fcb5f0637f9fbdedaf8d37da9a390f76ae0ded8", + "size": 268817, + "model_count": 527, + "updated_at": "2026-06-04T17:47:06+00:00" +} diff --git a/web/css/linker-main.css b/web/css/linker-main.css index 2447eed..97dcad5 100644 --- a/web/css/linker-main.css +++ b/web/css/linker-main.css @@ -3372,6 +3372,51 @@ font-weight: 700; color: var(--ml-text); } +.ml-options-db-summary { + display: grid; + gap: 1px; + border: 1px solid rgba(255,255,255,0.08); + border-radius: 6px; + overflow: hidden; + background: rgba(255,255,255,0.03); +} +.ml-options-db-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + min-height: 34px; + padding: 7px 10px; + background: rgba(0,0,0,0.16); + color: var(--ml-text-secondary); + font-size: 12px; +} +.ml-options-db-row strong { + color: var(--ml-text); + font-size: 12px; + font-weight: 700; + text-align: right; +} +.ml-options-db-message { + margin-top: 10px; + color: var(--ml-text-secondary); + font-size: 12px; + line-height: 1.35; +} +.ml-options-db-actions { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 8px; + margin-top: 12px; +} +.ml-options-db-actions .ml-btn { + gap: 6px; +} +.ml-options-db-actions .ml-btn svg { + width: 14px; + height: 14px; +} .ml-options-field { display: flex; flex-direction: column; diff --git a/web/linker.js b/web/linker.js index 75b755b..9fcdaa4 100644 --- a/web/linker.js +++ b/web/linker.js @@ -3738,6 +3738,13 @@ class LinkerManagerDialog extends ComfyDialog { 01 +
@@ -3748,14 +3755,14 @@ class LinkerManagerDialog extends ComfyDialog { CivitAI - 02 + 03
@@ -3777,6 +3784,37 @@ class LinkerManagerDialog extends ComfyDialog { +

CivitAI

@@ -3916,6 +3954,12 @@ class LinkerManagerDialog extends ComfyDialog { const sourceEnabledInputs = Array.from(this.contentElement.querySelectorAll('.ml-options-source-enabled')); const status = this.contentElement.querySelector('#ml-options-status'); const saveBtn = this.contentElement.querySelector('#ml-options-save'); + const modelListCountEl = this.contentElement.querySelector('#ml-options-model-list-count'); + const modelListUpdatedEl = this.contentElement.querySelector('#ml-options-model-list-updated'); + const modelListStateEl = this.contentElement.querySelector('#ml-options-model-list-state'); + const modelListMessageEl = this.contentElement.querySelector('#ml-options-model-list-message'); + const modelListCheckBtn = this.contentElement.querySelector('#ml-options-model-list-check'); + const modelListUpdateBtn = this.contentElement.querySelector('#ml-options-model-list-update'); const navButtons = Array.from(this.contentElement.querySelectorAll('.ml-options-nav-btn')); const optionSections = Array.from(this.contentElement.querySelectorAll('.ml-options-section')); const trackedInputs = [ @@ -3939,6 +3983,105 @@ class LinkerManagerDialog extends ComfyDialog { if (mode) status.classList.add(mode); }; + const formatOptionDate = (value) => { + if (!value) return 'Bundled'; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return value; + return date.toLocaleString(); + }; + + const shortSha = (value) => value ? String(value).slice(0, 8) : ''; + + const setModelListBusy = (busy) => { + if (modelListCheckBtn) modelListCheckBtn.disabled = busy; + if (modelListUpdateBtn) modelListUpdateBtn.disabled = busy; + }; + + const renderModelListStatus = (data = {}, checkedRemote = false) => { + if (modelListCountEl) { + modelListCountEl.textContent = Number.isFinite(Number(data.local_count)) + ? Number(data.local_count).toLocaleString() + : '-'; + } + if (modelListUpdatedEl) { + modelListUpdatedEl.textContent = formatOptionDate(data.local_updated_at); + } + if (modelListStateEl) { + if (checkedRemote && data.can_compare && data.update_available) { + modelListStateEl.textContent = 'Update available'; + } else if (checkedRemote && data.can_compare) { + modelListStateEl.textContent = 'Up to date'; + } else if (checkedRemote && !data.can_compare) { + modelListStateEl.textContent = 'Remote checked'; + } else { + modelListStateEl.textContent = data.local_sha ? 'Tracked' : 'Bundled'; + } + } + if (modelListMessageEl) { + const localSha = shortSha(data.local_sha); + const remoteSha = shortSha(data.remote_sha); + if (checkedRemote && data.update_available) { + modelListMessageEl.textContent = `A newer ComfyUI-Manager model-list.json is available (${localSha || 'local'} -> ${remoteSha}).`; + } else if (checkedRemote && data.can_compare) { + modelListMessageEl.textContent = `Local Database is current (${localSha}).`; + } else if (checkedRemote) { + modelListMessageEl.textContent = `Remote file was checked. Local SHA is not recorded yet; run update once to track future changes.`; + } else { + modelListMessageEl.textContent = `Local Database uses ComfyUI-Manager model-list.json.`; + } + } + }; + + const loadModelListStatus = async (checkRemote = false) => { + try { + setModelListBusy(true); + if (modelListStateEl) { + modelListStateEl.textContent = checkRemote ? 'Checking GitHub...' : 'Loading...'; + } + const response = await api.fetchApi(`/model_linker/model-list/status${checkRemote ? '?check_remote=1' : ''}`); + if (!response.ok) { + throw new Error(`Status failed: ${response.status}`); + } + const data = await response.json(); + renderModelListStatus(data, checkRemote); + return data; + } catch (error) { + console.error('Model Linker: model-list status error:', error); + if (modelListStateEl) modelListStateEl.textContent = 'Error'; + if (modelListMessageEl) modelListMessageEl.textContent = error.message || 'Failed to read Local Database status.'; + return null; + } finally { + setModelListBusy(false); + } + }; + + const updateModelList = async () => { + try { + setModelListBusy(true); + if (modelListStateEl) modelListStateEl.textContent = 'Updating...'; + if (modelListMessageEl) modelListMessageEl.textContent = 'Downloading latest ComfyUI-Manager model-list.json...'; + const response = await api.fetchApi('/model_linker/model-list/update', { + method: 'POST' + }); + const data = await response.json(); + if (!response.ok) { + throw new Error(data.error || `Update failed: ${response.status}`); + } + await this.clearSearchCaches(); + renderModelListStatus(data, false); + if (modelListStateEl) modelListStateEl.textContent = 'Updated'; + if (modelListMessageEl) modelListMessageEl.textContent = `Local Database updated. ${Number(data.local_count || 0).toLocaleString()} models loaded.`; + this.showNotification('Local Database updated', 'success'); + } catch (error) { + console.error('Model Linker: model-list update error:', error); + if (modelListStateEl) modelListStateEl.textContent = 'Error'; + if (modelListMessageEl) modelListMessageEl.textContent = error.message || 'Failed to update Local Database.'; + this.showNotification('Local Database update failed', 'error'); + } finally { + setModelListBusy(false); + } + }; + const setActiveNav = (targetId) => { navButtons.forEach((btn) => { btn.classList.toggle('is-active', btn.dataset.target === targetId); @@ -3996,9 +4139,26 @@ class LinkerManagerDialog extends ComfyDialog { const targetId = btn.dataset.target; if (!targetId) return; setVisibleSection(targetId); + if (targetId === 'ml-options-section-local-db') { + loadModelListStatus(false); + } }); }); + if (modelListCheckBtn) { + modelListCheckBtn.addEventListener('click', () => { + loadModelListStatus(true); + }); + } + + if (modelListUpdateBtn) { + modelListUpdateBtn.addEventListener('click', () => { + updateModelList(); + }); + } + + loadModelListStatus(false); + if (saveBtn) { saveBtn.addEventListener('click', async () => { const civitaiCandidateLimitRaw = parseInt(civitaiLimitInput?.value || `${tokens.civitai_candidate_limit}`, 10);