diff --git a/model_registry.json b/model_registry.json index 72d57df..0cbfd92 100644 --- a/model_registry.json +++ b/model_registry.json @@ -4,7 +4,7 @@ { "name": "Flux.1-dev-fp8.safetensors", "category": "Photorealism", - "url": "https://huggingface.co/black-forest-labs/FLUX.1-dev", + "url": "https://huggingface.co/black-forest-labs/FLUX.1-dev/resolve/main/Flux.1-dev-fp8.safetensors", "tags": [ "flux", "realism", @@ -29,7 +29,7 @@ { "name": "Flux.2-dev-fp16.safetensors", "category": "Photorealism", - "url": "https://huggingface.co/black-forest-labs/FLUX.2-dev", + "url": "https://huggingface.co/black-forest-labs/FLUX.2-dev/resolve/main/Flux.2-dev-fp16.safetensors", "tags": [ "flux", "hyper-realism", @@ -97,7 +97,7 @@ { "name": "Z-Image-Turbo-v1.safetensors", "category": "Photorealism", - "url": "https://huggingface.co/Tongyi-MAI/Z-Image-Turbo", + "url": "https://huggingface.co/Tongyi-MAI/Z-Image-Turbo/resolve/main/Z-Image-Turbo-v1.safetensors", "tags": [ "photorealism", "architecture", @@ -165,7 +165,7 @@ { "name": "IC-Light-XL-Realistic.safetensors", "category": "Photorealism", - "url": "https://huggingface.co/lllyasviel/ic-light", + "url": "https://huggingface.co/lllyasviel/ic-light/resolve/main/IC-Light-XL-Realistic.safetensors", "tags": [ "photorealism", "relighting", @@ -253,7 +253,7 @@ { "name": "Mobius_XL_v1.safetensors", "category": "Photorealism", - "url": "https://civitai.com/models/mobius", + "url": "https://civitai.com/models/17589", "tags": [ "photorealism", "fantasy-realism", @@ -296,7 +296,7 @@ { "name": "ThinkDiffusionXL_v1.safetensors", "category": "Photorealism", - "url": "https://civitai.com/models/thinkdiffusion", + "url": "https://civitai.com/models/926617", "tags": [ "photorealism", "advertisement", @@ -317,7 +317,7 @@ { "name": "NightVision_XL_v9.safetensors", "category": "Photorealism", - "url": "https://civitai.com/models/nightvision", + "url": "https://civitai.com/models/586010", "tags": [ "photorealism", "night-photography", @@ -339,7 +339,7 @@ { "name": "CyberRealistic_XL_v6.safetensors", "category": "Photorealism", - "url": "https://civitai.com/models/cyberrealistic", + "url": "https://civitai.com/models/1531982", "tags": [ "photorealism", "portrait", @@ -361,7 +361,7 @@ { "name": "Illustrious-XL-v1.0.safetensors", "category": "Anime", - "url": "https://civitai.com/models/illustrious", + "url": "https://civitai.com/models/2384857", "tags": [ "anime", "illustration", @@ -405,7 +405,7 @@ { "name": "AAM_XL_AnimeMix_v2.safetensors", "category": "Anime", - "url": "https://civitai.com/models/anime-mix", + "url": "https://civitai.com/models/269232", "tags": [ "anime", "scenery", @@ -426,7 +426,7 @@ { "name": "MeinaMix_V11.safetensors", "category": "Anime", - "url": "https://civitai.com/models/meinamix", + "url": "https://civitai.com/models/153831", "tags": [ "anime", "soft-shading", @@ -447,7 +447,7 @@ { "name": "Cetus-Mix-Final.safetensors", "category": "Anime", - "url": "https://civitai.com/models/cetus", + "url": "https://civitai.com/models/1947515", "tags": [ "anime", "semi-realistic", @@ -468,7 +468,7 @@ { "name": "NoobAI-XL-v1.safetensors", "category": "Anime", - "url": "https://civitai.com/models/noobai", + "url": "https://civitai.com/models/2339056", "tags": [ "anime", "tag-adherence", @@ -489,7 +489,7 @@ { "name": "Animagine_XL_v4.0.safetensors", "category": "Anime", - "url": "https://civitai.com/models/animagine", + "url": "https://civitai.com/models/1564077", "tags": [ "anime", "official-art-style", @@ -510,7 +510,7 @@ { "name": "AbyssOrangeMix3_AOM3.safetensors", "category": "Anime", - "url": "https://civitai.com/models/abyssorangemix", + "url": "https://civitai.com/models/21503", "tags": [ "anime", "dark-fantasy", @@ -531,7 +531,7 @@ { "name": "Counterfeit-V3.0.safetensors", "category": "Anime", - "url": "https://civitai.com/models/counterfeit", + "url": "https://civitai.com/models/4468", "tags": [ "anime", "landscape-anime", @@ -552,7 +552,7 @@ { "name": "BlueberryMix_XL.safetensors", "category": "Anime", - "url": "https://civitai.com/models/blueberry", + "url": "https://civitai.com/models/329642", "tags": [ "anime", "cute-style", @@ -573,7 +573,7 @@ { "name": "Mistoon_Anime_v2.safetensors", "category": "Anime", - "url": "https://civitai.com/models/mistoon", + "url": "https://civitai.com/models/32022", "tags": [ "anime", "flat-color", @@ -651,7 +651,7 @@ { "name": "Waifu-Diffusion-XL.safetensors", "category": "Anime", - "url": "https://huggingface.co/waifu-diffusion", + "url": "https://huggingface.co/waifu-diffusion/resolve/main/Waifu-Diffusion-XL.safetensors", "tags": [ "base-anime", "massive-dataset", @@ -670,7 +670,7 @@ { "name": "Painterly_XL_Check_v2.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/painterly", + "url": "https://civitai.com/models/2326354", "tags": [ "oil-painting", "canvas", @@ -710,7 +710,7 @@ { "name": "Watercolor_Stylized_v3.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/watercolor", + "url": "https://civitai.com/models/2289270", "tags": [ "water-wash", "ink-bleed", @@ -729,7 +729,7 @@ { "name": "Copax_TimeLess_XL_v9.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/timeless", + "url": "https://civitai.com/models/118111", "tags": [ "atmospheric", "ethereal", @@ -748,7 +748,7 @@ { "name": "Oil_Brush_Pro_v2.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/oilbrush", + "url": "https://civitai.com/models/118223", "tags": [ "thick-paint", "museum-quality", @@ -767,7 +767,7 @@ { "name": "Arcane_Style_XL.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/arcane", + "url": "https://civitai.com/models/2183167", "tags": [ "stylized", "neon", @@ -787,7 +787,7 @@ { "name": "Inkpunk_Diffusion_v2.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/inkpunk", + "url": "https://civitai.com/models/2124568", "tags": [ "ink-sketch", "messy-lines", @@ -806,7 +806,7 @@ { "name": "Abstract_Master_XL.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/abstract", + "url": "https://civitai.com/models/2149089", "tags": [ "geometric", "non-representative", @@ -825,7 +825,7 @@ { "name": "Charcoal_Pro_v1.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/charcoal", + "url": "https://civitai.com/models/2100594", "tags": [ "black-and-white", "hand-drawn", @@ -844,7 +844,7 @@ { "name": "Ukiyo-e_XL_v2.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/ukiyoe", + "url": "https://civitai.com/models/1018901", "tags": [ "japanese-print", "woodblock", @@ -863,7 +863,7 @@ { "name": "ComicMix_XL_v3.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/comicmix", + "url": "https://civitai.com/models/631262", "tags": [ "western-comic", "marvel-style", @@ -882,7 +882,7 @@ { "name": "Gouache_Style_XL.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/gouache", + "url": "https://civitai.com/models/330040", "tags": [ "matte-paint", "thick-wash", @@ -920,7 +920,7 @@ { "name": "Synthwave_Master_XL.safetensors", "category": "Artistic", - "url": "https://civitai.com/models/synthwave", + "url": "https://civitai.com/models/1463207", "tags": [ "80s-aesthetic", "retro-future", @@ -958,7 +958,7 @@ { "name": "Modern_Architecture_XL.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/arch", + "url": "https://civitai.com/models/2380151", "tags": [ "blueprints", "glass", @@ -978,7 +978,7 @@ { "name": "Interior_Design_Flux_v2.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/interior", + "url": "https://civitai.com/models/2373956", "tags": [ "home-staging", "photorealistic-rooms", @@ -997,7 +997,7 @@ { "name": "Abandoned_Places_XL.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/overgrown", + "url": "https://civitai.com/models/304561", "tags": [ "ruins", "nature-reclaimed", @@ -1017,7 +1017,7 @@ { "name": "CyberCity_XL_Pro.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/cybercity", + "url": "https://civitai.com/models/1731924", "tags": [ "skyscrapers", "neon", @@ -1036,7 +1036,7 @@ { "name": "Brutalist_Logic_XL.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/brutalist", + "url": "https://civitai.com/models/1636847", "tags": [ "concrete", "geometric", @@ -1055,7 +1055,7 @@ { "name": "Gothic_Stone_Carving.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/gothic", + "url": "https://civitai.com/models/2268702", "tags": [ "cathedral", "intricate-stone", @@ -1093,7 +1093,7 @@ { "name": "Steampunk_City_v2.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/steamcity", + "url": "https://civitai.com/models/2357299", "tags": [ "brass", "pipes", @@ -1113,7 +1113,7 @@ { "name": "Skyscraper_High_Angle.safetensors", "category": "Architecture", - "url": "https://civitai.com/models/skyscraper", + "url": "https://civitai.com/models/1170746", "tags": [ "aerial-view", "glass-reflections", @@ -1170,7 +1170,7 @@ { "name": "GrandCanyon_RedRock_XL.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/canyon", + "url": "https://civitai.com/models/901560", "tags": [ "desert-textures", "utah-scenery", @@ -1189,7 +1189,7 @@ { "name": "Misty_Forest_XL_v3.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/misty", + "url": "https://civitai.com/models/1447721", "tags": [ "atmospheric-fog", "pine-trees", @@ -1208,7 +1208,7 @@ { "name": "Arctic_Tundra_Pro.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/arctic", + "url": "https://civitai.com/models/1632372", "tags": [ "snow-ice", "blue-hour", @@ -1246,7 +1246,7 @@ { "name": "Volcano_Fury_XL.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/volcano", + "url": "https://civitai.com/models/160763", "tags": [ "lava", "volcanic-smoke", @@ -1265,7 +1265,7 @@ { "name": "Alpine_Valley_v2.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/alpine", + "url": "https://civitai.com/models/852086", "tags": [ "mountains", "green-valley", @@ -1284,7 +1284,7 @@ { "name": "Lavender_Sunset_XL.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/lavender", + "url": "https://civitai.com/models/2268947", "tags": [ "floral-fields", "soft-sunset", @@ -1303,7 +1303,7 @@ { "name": "Ocean_Deep_Pro.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/ocean", + "url": "https://civitai.com/models/2385281", "tags": [ "underwater", "coral-reefs", @@ -1322,7 +1322,7 @@ { "name": "Martian_Colony_XL.safetensors", "category": "Landscape", - "url": "https://civitai.com/models/mars", + "url": "https://civitai.com/models/2242763", "tags": [ "red-dust", "alien-sky", @@ -1341,7 +1341,7 @@ { "name": "Wan_2.1_T2V_14B.safetensors", "category": "Video", - "url": "https://huggingface.co/Wan-AI/Wan2.1-T2V-14B", + "url": "https://huggingface.co/Wan-AI/Wan2.1-T2V-14B/resolve/main/Wan_2.1_T2V_14B.safetensors", "tags": [ "text-to-video", "cinematic-motion", @@ -1359,7 +1359,7 @@ { "name": "Wan2.1-t2v-1.3B.safetensors", "category": "Video", - "url": "https://huggingface.co/Wan-AI/Wan2.1-T2V-1.3B", + "url": "https://huggingface.co/Wan-AI/Wan2.1-T2V-1.3B/resolve/main/Wan2.1-t2v-1.3B.safetensors", "tags": [ "text-to-video", "efficient", @@ -1377,7 +1377,7 @@ { "name": "HunyuanVideo_v1.safetensors", "category": "Video", - "url": "https://huggingface.co/tencent/HunyuanVideo", + "url": "https://huggingface.co/tencent/HunyuanVideo/resolve/main/HunyuanVideo_v1.safetensors", "tags": [ "ai-video", "long-duration", @@ -1395,7 +1395,7 @@ { "name": "SVD-XT-1.1.safetensors", "category": "Video", - "url": "https://huggingface.co/stabilityai/stable-video-diffusion", + "url": "https://huggingface.co/stabilityai/stable-video-diffusion/resolve/main/SVD-XT-1.1.safetensors", "tags": [ "image-to-video", "professional-motion" @@ -1409,7 +1409,7 @@ { "name": "LivePortrait_XL_v2.safetensors", "category": "Video", - "url": "https://civitai.com/models/liveportrait", + "url": "https://civitai.com/models/736694", "tags": [ "facial-animation", "vlog-automation", @@ -1447,7 +1447,7 @@ { "name": "Blender-Style-XL.safetensors", "category": "3D Render", - "url": "https://civitai.com/models/blender", + "url": "https://civitai.com/models/1448527", "tags": [ "cycles-render", "hard-surface", @@ -1466,7 +1466,7 @@ { "name": "Clay-Aardman-Style.safetensors", "category": "3D Render", - "url": "https://civitai.com/models/clay", + "url": "https://civitai.com/models/2171802", "tags": [ "stop-motion", "fingerprint-texture", @@ -1485,7 +1485,7 @@ { "name": "Unreal-Engine-5-XL.safetensors", "category": "3D Render", - "url": "https://civitai.com/models/ue5", + "url": "https://civitai.com/models/111551", "tags": [ "game-engine-lighting", "high-poly", @@ -1504,7 +1504,7 @@ { "name": "ZBrush-Sculpt-XL.safetensors", "category": "3D Render", - "url": "https://civitai.com/models/zbrush", + "url": "https://civitai.com/models/938778", "tags": [ "digital-sculpt", "grey-clay", @@ -1523,7 +1523,7 @@ { "name": "ControlNet-Union-SDXL.safetensors", "category": "Utility", - "url": "https://huggingface.co/xinsir/controlnet-union", + "url": "https://huggingface.co/xinsir/controlnet-union/resolve/main/ControlNet-Union-SDXL.safetensors", "tags": [ "depth", "canny", @@ -1543,7 +1543,7 @@ { "name": "IP-Adapter-FaceID-XL.safetensors", "category": "Utility", - "url": "https://civitai.com/models/faceid", + "url": "https://civitai.com/models/301776", "tags": [ "face-preservation", "identity-consistency" @@ -1561,7 +1561,7 @@ { "name": "OmniGen-v2.safetensors", "category": "Utility", - "url": "https://huggingface.co/BectorSpaceLab/OmniGen-v2", + "url": "https://huggingface.co/BectorSpaceLab/OmniGen-v2/resolve/main/OmniGen-v2.safetensors", "tags": [ "multi-image-composition", "instruct-editing" @@ -1579,7 +1579,7 @@ { "name": "Hyper-SDXL-Lightning.safetensors", "category": "Utility", - "url": "https://huggingface.co/ByteDance/Hyper-SD", + "url": "https://huggingface.co/ByteDance/Hyper-SD/resolve/main/Hyper-SDXL-Lightning.safetensors", "tags": [ "1-step-generation", "real-time-previews" @@ -1597,7 +1597,7 @@ { "name": "Segment-Anything-2.safetensors", "category": "Utility", - "url": "https://huggingface.co/facebook/sam2", + "url": "https://huggingface.co/facebook/sam2/resolve/main/Segment-Anything-2.safetensors", "tags": [ "object-masking", "surgical-selection", @@ -1616,7 +1616,7 @@ { "name": "Food-Photography-XL.safetensors", "category": "Photography", - "url": "https://civitai.com/models/food", + "url": "https://civitai.com/models/2300106", "tags": [ "restaurant-lighting", "macro-food", @@ -1635,7 +1635,7 @@ { "name": "Macro-Nature-XL.safetensors", "category": "Photography", - "url": "https://civitai.com/models/macro", + "url": "https://civitai.com/models/1501570", "tags": [ "insect-detail", "shallow-depth", @@ -1654,7 +1654,7 @@ { "name": "Product-Commercial-XL.safetensors", "category": "Photography", - "url": "https://civitai.com/models/product", + "url": "https://civitai.com/models/2366596", "tags": [ "clean-background", "product-lighting", @@ -1673,7 +1673,7 @@ { "name": "Mature-Faces-XL-v2.safetensors", "category": "Portrait", - "url": "https://civitai.com/models/silver", + "url": "https://civitai.com/models/2364595", "tags": [ "elderly", "silver-hair", @@ -1692,7 +1692,7 @@ { "name": "Fashion-Editorial-XL.safetensors", "category": "Portrait", - "url": "https://civitai.com/models/fashion", + "url": "https://civitai.com/models/2302224", "tags": [ "vogue-style", "high-contrast", @@ -1711,7 +1711,7 @@ { "name": "Tarot-Card-Check-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/tarot", + "url": "https://civitai.com/models/1743070", "tags": [ "mystical-border", "symbolic-art", @@ -1730,7 +1730,7 @@ { "name": "Stained-Glass-v4.safetensors", "category": "Niche", - "url": "https://civitai.com/models/glass", + "url": "https://civitai.com/models/2378571", "tags": [ "backlit", "translucent", @@ -1749,7 +1749,7 @@ { "name": "Blueprint-Draft-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/blueprint", + "url": "https://civitai.com/models/236991", "tags": [ "technical-drawing", "grid-lines", @@ -1768,7 +1768,7 @@ { "name": "Pixel-Art-Ultra-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/pixelart", + "url": "https://civitai.com/models/2355675", "tags": [ "retro-gaming", "64-bit", @@ -1787,7 +1787,7 @@ { "name": "Graffiti-Wall-Pro-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/graffiti", + "url": "https://civitai.com/models/1690260", "tags": [ "spray-paint", "street-art", @@ -1806,7 +1806,7 @@ { "name": "Chalkboard-Menu-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/chalk", + "url": "https://civitai.com/models/1269967", "tags": [ "dusty-chalk", "handwritten", @@ -1825,7 +1825,7 @@ { "name": "Embroidery-Craft-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/embroidery", + "url": "https://civitai.com/models/2333323", "tags": [ "sewn-texture", "thread-patterns", @@ -1844,7 +1844,7 @@ { "name": "Neon-Signs-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/neonsign", + "url": "https://civitai.com/models/190785", "tags": [ "glowing-tubes", "dark-city-streets" @@ -1862,7 +1862,7 @@ { "name": "Infographic-Clean-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/info", + "url": "https://civitai.com/models/1606567", "tags": [ "charts", "clean-typography", @@ -1881,7 +1881,7 @@ { "name": "Qwen2.5-VL-7B-Instruct.safetensors", "category": "Logic", - "url": "https://huggingface.co/Qwen/Qwen2.5-VL", + "url": "https://huggingface.co/Qwen/Qwen2.5-VL/resolve/main/Qwen2.5-VL-7B-Instruct.safetensors", "tags": [ "vlm", "image-understanding", @@ -1900,7 +1900,7 @@ { "name": "Llama-4-8B-Vision.safetensors", "category": "Logic", - "url": "https://huggingface.co/meta-llama/Llama-4", + "url": "https://huggingface.co/meta-llama/Llama-4/resolve/main/Llama-4-8B-Vision.safetensors", "tags": [ "reasoning", "captioning", @@ -1919,7 +1919,7 @@ { "name": "Medical-Anatomy-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/medical", + "url": "https://civitai.com/models/1854950", "tags": [ "scientific-illustration", "biological-accuracy" @@ -1956,7 +1956,7 @@ { "name": "Comic-Book-Inker-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/inker", + "url": "https://civitai.com/models/326264", "tags": [ "thick-black-lines", "cross-hatching" @@ -1974,7 +1974,7 @@ { "name": "Retro-VCR-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/vcr", + "url": "https://civitai.com/models/1421406", "tags": [ "glitch-art", "tracking-lines", @@ -1993,7 +1993,7 @@ { "name": "Miniature-World-XL.safetensors", "category": "Niche", - "url": "https://civitai.com/models/miniature", + "url": "https://civitai.com/models/2081244", "tags": [ "tilt-shift", "toy-figures", @@ -3364,4 +3364,4 @@ "enabled": false } ] -} \ No newline at end of file +} diff --git a/sync_registry.py b/sync_registry.py index 9776eea..fe6de11 100644 --- a/sync_registry.py +++ b/sync_registry.py @@ -1,22 +1,62 @@ +import argparse import json import os import sys +from datetime import datetime # Add ComfyUI root to path to import folder_paths # Assumes this script is in custom_nodes/ComfyUI-Cluster/ current_dir = os.path.dirname(os.path.abspath(__file__)) comfy_root = os.path.abspath(os.path.join(current_dir, "..", "..")) -sys.path.append(comfy_root) +if comfy_root not in sys.path: + sys.path.append(comfy_root) try: import folder_paths except ImportError: - print("Error: Could not import folder_paths. Make sure this script is running inside the ComfyUI environment or folder structure.") + print("Error: Could not import folder_paths.") + print("Make sure this script runs inside the ComfyUI folder structure.") print(f"Constructed ComfyUI root path: {comfy_root}") sys.exit(1) + REGISTRY_FILE = "model_registry.json" CONFIG_FILE = "cluster_config.json" +SCAN_LOG_FILE = "model_scan.log" + +CATEGORY_SPECS = [ + ("checkpoints", "checkpoints"), + ("loras", "loras"), + ("controlnets", "controlnet"), + ("vae", "vae"), + ("text_encoders", "clip"), + ("diffusion_models", "diffusion_models"), + ("clip", "clip"), + ("embeddings", "embeddings"), +] + +FOLDER_TYPE_ALIAS = { + "checkpoint": "checkpoints", + "checkpoints": "checkpoints", + "lora": "loras", + "loras": "loras", + "controlnet": "controlnet", + "controlnets": "controlnet", + "clip": "clip", + "clips": "clip", + "text_encoder": "clip", + "text_encoders": "clip", + "vae": "vae", + "unet": "diffusion_models", + "diffusion_model": "diffusion_models", + "diffusion_models": "diffusion_models", + "embedding": "embeddings", + "embeddings": "embeddings", +} + +_FOLDER_ROOTS_CACHE = {} +_FILE_INDEX_CACHE = {} + def load_registry(path): if not os.path.exists(path): @@ -24,10 +64,12 @@ def load_registry(path): with open(path, "r", encoding="utf-8") as f: return json.load(f) + def save_registry(path, data): with open(path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) + def load_config(): config_path = os.path.join(current_dir, CONFIG_FILE) if os.path.exists(config_path): @@ -35,147 +77,428 @@ def load_config(): return json.load(f) return {} + def save_config(data): config_path = os.path.join(current_dir, CONFIG_FILE) with open(config_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2) -def get_user_root(): + +def normalize_path(path): + return os.path.normpath(os.path.abspath(path)) + + +def normalize_for_compare(path_value): + if not isinstance(path_value, str): + return "" + value = path_value.strip().strip('"') + if not value: + return "" + value = os.path.expandvars(os.path.expanduser(value)) + if not os.path.isabs(value): + value = os.path.join(current_dir, value) + return normalize_path(value) + + +def canonical_folder_type(folder_type, default_type): + raw = (folder_type or default_type or "").strip().lower() + if not raw: + return default_type + return FOLDER_TYPE_ALIAS.get(raw, raw) + + +def expand_folder_types(folder_type): + canonical = canonical_folder_type(folder_type, "checkpoints") + + if canonical == "checkpoints": + expanded = ["checkpoints", "diffusion_models", "unet"] + elif canonical == "diffusion_models": + expanded = ["diffusion_models", "unet", "checkpoints"] + elif canonical == "clip": + expanded = ["clip", "text_encoders", "checkpoints"] + elif canonical == "vae": + expanded = ["vae", "checkpoints"] + elif canonical == "controlnet": + expanded = ["controlnet"] + elif canonical == "loras": + expanded = ["loras"] + elif canonical == "embeddings": + expanded = ["embeddings"] + else: + expanded = [canonical] + + if canonical not in expanded: + expanded.insert(0, canonical) + + deduped = [] + seen = set() + for ft in expanded: + if ft in seen: + continue + seen.add(ft) + deduped.append(ft) + return deduped + + +def get_folder_roots(folder_type): + if folder_type in _FOLDER_ROOTS_CACHE: + return _FOLDER_ROOTS_CACHE[folder_type] + + roots = [] + try: + roots = folder_paths.get_folder_paths(folder_type) or [] + except Exception: + roots = [] + + cleaned = [] + seen = set() + for root in roots: + if not root: + continue + norm = normalize_path(root) + if norm in seen: + continue + seen.add(norm) + cleaned.append(norm) + + _FOLDER_ROOTS_CACHE[folder_type] = cleaned + return cleaned + + +def build_filename_index(root): + norm_root = normalize_path(root) + if norm_root in _FILE_INDEX_CACHE: + return _FILE_INDEX_CACHE[norm_root] + + index = {} + if os.path.isdir(norm_root): + for walk_root, _, files in os.walk(norm_root): + for file_name in files: + key = file_name.lower() + index.setdefault(key, []).append(os.path.join(walk_root, file_name)) + + _FILE_INDEX_CACHE[norm_root] = index + return index + + +def is_subpath(path, root): + try: + path_n = normalize_path(path) + root_n = normalize_path(root) + return os.path.commonpath([path_n, root_n]) == root_n + except Exception: + return False + + +def infer_folder_type_from_path(path, candidate_types, user_root=None): + for ft in candidate_types: + for root in get_folder_roots(ft): + if is_subpath(path, root): + return ft + + if user_root and is_subpath(path, user_root): + rel = os.path.relpath(path, user_root) + first_part = rel.split(os.sep)[0].lower() if rel else "" + if first_part: + first_part = canonical_folder_type(first_part, first_part) + for ft in candidate_types: + if canonical_folder_type(ft, ft) == first_part: + return ft + + return candidate_types[0] if candidate_types else None + + +def validate_existing_path(existing_path, expected_name): + resolved = normalize_for_compare(existing_path) + if not resolved: + return None + if not os.path.isfile(resolved): + return None + if os.path.basename(resolved).lower() != expected_name.lower(): + return None + return resolved + + +def resolve_model_path(filename, preferred_folder_type, user_root=None, current_path=None): + if not filename: + return (None, preferred_folder_type, "missing_name") + + candidate_types = expand_folder_types(preferred_folder_type) + target_name = filename.strip() + target_name_l = target_name.lower() + + # 1) If registry path already points to the expected file, trust it. + existing = validate_existing_path(current_path, target_name) + if existing: + inferred = infer_folder_type_from_path(existing, candidate_types, user_root) + return (existing, inferred, "registry_path") + + # 2) ComfyUI resolver. + for ft in candidate_types: + try: + resolved = folder_paths.get_full_path(ft, target_name) + except Exception: + resolved = None + if resolved and os.path.isfile(resolved): + return (normalize_path(resolved), ft, "comfy_get_full_path") + + # 3) Search ComfyUI configured roots. + searched_roots = set() + for ft in candidate_types: + for root in get_folder_roots(ft): + if root in searched_roots: + continue + searched_roots.add(root) + + direct = os.path.join(root, target_name) + if os.path.isfile(direct): + return (normalize_path(direct), ft, "comfy_root_exact") + + index = build_filename_index(root) + matches = index.get(target_name_l, []) + if matches: + return (normalize_path(matches[0]), ft, "comfy_root_recursive") + + # 4) Search user root (external models root), if configured. + if user_root and os.path.isdir(user_root): + direct = os.path.join(user_root, target_name) + if os.path.isfile(direct): + inferred = infer_folder_type_from_path(direct, candidate_types, user_root) + return (normalize_path(direct), inferred, "user_root_direct") + + for ft in candidate_types: + sub = os.path.join(user_root, ft, target_name) + if os.path.isfile(sub): + return (normalize_path(sub), ft, "user_root_subfolder") + + user_index = build_filename_index(user_root) + matches = user_index.get(target_name_l, []) + if matches: + best = normalize_path(matches[0]) + inferred = infer_folder_type_from_path(best, candidate_types, user_root) + return (best, inferred, "user_root_recursive") + + return (None, candidate_types[0], "not_found") + + +def get_user_root(prompt_if_missing=True): config = load_config() root = config.get("models_root") - - # Verify validity of stored root + if root and os.path.isdir(root): + root = normalize_path(root) print(f"Using configured models root: {root}") return root - - print("\n--- Configuration Required ---") - print("Could not automatically locate your models using ComfyUI defaults.") - print("Please enter the path to your 'models' directory (e.g. D:\\ComfyUI\\models or I:\\models).") - + + if not prompt_if_missing: + return None + + print("\n--- Optional Configuration ---") + print("Could not load a saved models root directory.") + print("Enter your models root path if you keep models outside ComfyUI defaults.") + print("Press Enter to skip.") + while True: try: - root = input("Models Root Path: ").strip().strip('"') # Remove quotes if user copy-pasted + entered = input("Models Root Path: ").strip().strip('"') except EOFError: - print("\nInput canceled.") - sys.exit(1) - - if os.path.isdir(root): + print("Input unavailable. Continuing without custom models root.") + return None + + if not entered: + print("Skipping custom models root.") + return None + + if os.path.isdir(entered): + root = normalize_path(entered) config["models_root"] = root save_config(config) print("Configuration saved.\n") return root - else: - print(f"Error: Directory not found: {root}") - print("Please try again.") -def check_exists(folder_type, filename, user_root=None): - # 1. Try ComfyUI internal resolution (preferred) - # This checks known paths including extra_model_paths.yaml if loaded correctly - try: - path = folder_paths.get_full_path(folder_type, filename) - if path: return True - except: - pass # folder_paths might error on unknown types - - # 2. Manual check against User Root - if user_root: - # Logic: user_root + folder_type + filename - # Normalize folder_type to directory name if needed (usually they match: loras -> loras) - - candidates = [folder_type] - - # Add symmetric fallbacks for checkpoints - if folder_type in ["checkpoints", "diffusion_models", "unet"]: - candidates = ["checkpoints", "diffusion_models", "unet"] - - for d in candidates: - # Check direct join - p = os.path.join(user_root, d, filename) - if os.path.exists(p): return True - - # Recursive / Subdirectory check? - # Some users put things in models/checkpoints/SDXL/model.safetensors - # But the registry filename is usually just "model.safetensors" - # We won't do deep scan for performance unless requested, matching GUI manual approach - - return False + print(f"Error: Directory not found: {entered}") -def sync_models(): + +def write_scan_log( + log_path, + scan_time, + user_root, + stats, + missing_models, + status_changes, + path_changes, + folder_type_changes, + dry_run, +): + with open(log_path, "w", encoding="utf-8") as log: + log.write(f"Scan Date: {scan_time}\n") + log.write(f"Dry Run: {dry_run}\n") + log.write(f"User Models Root: {user_root or '(not set)'}\n") + log.write(f"Scanned Items: {stats['scanned']}\n") + log.write(f"Status Changes: {stats['status_changes']}\n") + log.write(f"Path Changes: {stats['path_changes']}\n") + log.write(f"Folder Type Changes: {stats['folder_type_changes']}\n") + log.write(f"Missing Models: {stats['missing']}\n") + + log.write("\n== Status Changes ==\n") + for entry in status_changes: + log.write(f"{entry}\n") + + log.write("\n== Path Changes ==\n") + for entry in path_changes: + log.write(f"{entry}\n") + + log.write("\n== Folder Type Changes ==\n") + for entry in folder_type_changes: + log.write(f"{entry}\n") + + log.write("\n== Missing Models ==\n") + for entry in missing_models: + log.write(f"{entry}\n") + + +def sync_models(dry_run=False, prompt_for_root=True): registry_path = os.path.join(current_dir, REGISTRY_FILE) - log_path = os.path.join(current_dir, "model_scan.log") - + log_path = os.path.join(current_dir, SCAN_LOG_FILE) + registry = load_registry(registry_path) - if not registry: print(f"Registry not found at {registry_path}") return - # Ensure we have a root to check against if Comfy fails - user_root = get_user_root() + user_root = get_user_root(prompt_if_missing=prompt_for_root) - print(f"Scanning models for registry: {registry_path}") - changes = 0 + print(f"\nScanning models for registry: {registry_path}") + scan_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + stats = { + "scanned": 0, + "status_changes": 0, + "path_changes": 0, + "folder_type_changes": 0, + "missing": 0, + } missing_models = [] + status_change_details = [] + path_change_details = [] + folder_type_change_details = [] - # Helper for processing lists - def process_list(category_name, default_folder_type): - nonlocal changes + for category_name, default_folder_type in CATEGORY_SPECS: items = registry.get(category_name, []) - if not items: return + if not items: + continue + + print(f"Scanning {category_name} ({len(items)} items)...") - print(f"Scanning {category_name}...") for item in items: - name = item.get("name") - ftype = item.get("folder_type", default_folder_type) - - # Pass user_root to check_exists - exists = check_exists(ftype, name, user_root) - - old_status = item.get("enabled", None) - item["enabled"] = exists - - if old_status != exists: - changes += 1 - status_str = "ENABLED" if exists else "DISABLED" - print(f" [{status_str}] {name}") - - if not exists: + name = str(item.get("name", "")).strip() + if not name: + continue + + stats["scanned"] += 1 + + preferred_ft = canonical_folder_type(item.get("folder_type"), default_folder_type) + + if "folder_type" in item and item.get("folder_type") != preferred_ft: + old_ft = item.get("folder_type") + item["folder_type"] = preferred_ft + stats["folder_type_changes"] += 1 + folder_type_change_details.append( + f"[{category_name}] {name}: {old_ft} -> {preferred_ft} (normalized alias)" + ) + + resolved_path, resolved_ft, source = resolve_model_path( + filename=name, + preferred_folder_type=preferred_ft, + user_root=user_root, + current_path=item.get("path"), + ) + + exists = resolved_path is not None + old_enabled = item.get("enabled") + if old_enabled != exists: + item["enabled"] = exists + stats["status_changes"] += 1 + status_word = "ENABLED" if exists else "DISABLED" + print(f" [{status_word}] {name}") + status_change_details.append( + f"[{category_name}] {name}: {old_enabled} -> {exists}" + ) + else: + item["enabled"] = exists + + if exists: + old_path_raw = item.get("path") + old_path_norm = normalize_for_compare(old_path_raw) + if old_path_norm != resolved_path: + item["path"] = resolved_path + stats["path_changes"] += 1 + print(f" [PATH FIXED] {name}") + path_change_details.append( + f"[{category_name}] {name}: {old_path_raw} -> {resolved_path} ({source})" + ) + + should_store_folder_type = ("folder_type" in item) or (resolved_ft != default_folder_type) + if should_store_folder_type and item.get("folder_type") != resolved_ft: + old_ft = item.get("folder_type") + item["folder_type"] = resolved_ft + stats["folder_type_changes"] += 1 + print(f" [TYPE FIXED] {name}: {old_ft} -> {resolved_ft}") + folder_type_change_details.append( + f"[{category_name}] {name}: {old_ft} -> {resolved_ft}" + ) + else: + stats["missing"] += 1 missing_models.append(f"[{category_name}] {name}") + if "path" in item: + old_path_raw = item.get("path") + if old_path_raw: + stats["path_changes"] += 1 + print(f" [PATH CLEARED] {name}") + path_change_details.append( + f"[{category_name}] {name}: {old_path_raw} -> (removed, not found)" + ) + item.pop("path", None) - # Process all categories - process_list("checkpoints", "checkpoints") - process_list("loras", "loras") - process_list("controlnets", "controlnet") - process_list("vae", "vae") - process_list("text_encoders", "clip") - process_list("diffusion_models", "diffusion_models") - process_list("clip", "clip") - process_list("embeddings", "embeddings") + total_changes = stats["status_changes"] + stats["path_changes"] + stats["folder_type_changes"] - if changes > 0: - print(f"\nUpdating registry with {changes} status changes...") + if dry_run: + print("\nDry run complete. No changes were written.") + elif total_changes > 0: + print(f"\nUpdating registry with {total_changes} changes...") save_registry(registry_path, registry) print("Done.") else: - print("\nAll model statuses are up to date.") - - # Write Log - with open(log_path, "w", encoding="utf-8") as log: - log.write(f"Scan Date: {os.path.basename(current_dir)}\n") - log.write(f"User Models Root: {user_root}\n") - log.write(f"Missing Models ({len(missing_models)}):\n") - for m in missing_models: - log.write(f"{m}\n") - + print("\nAll model statuses and paths are up to date.") + + write_scan_log( + log_path=log_path, + scan_time=scan_time, + user_root=user_root, + stats=stats, + missing_models=missing_models, + status_changes=status_change_details, + path_changes=path_change_details, + folder_type_changes=folder_type_change_details, + dry_run=dry_run, + ) print(f"\nScan log written to: {log_path}") - if missing_models: - print(f"WARNING: {len(missing_models)} models were not found. Check log for details.") + print( + f"Summary: scanned={stats['scanned']}, " + f"status_changes={stats['status_changes']}, " + f"path_changes={stats['path_changes']}, " + f"folder_type_changes={stats['folder_type_changes']}, " + f"missing={stats['missing']}" + ) + + if stats["missing"] > 0: + print(f"WARNING: {stats['missing']} models were not found. Check log for details.") + def reset_registry(): registry_path = os.path.join(current_dir, REGISTRY_FILE) registry = load_registry(registry_path) - + if not registry: print(f"Registry not found at {registry_path}") return @@ -183,43 +506,75 @@ def reset_registry(): print("Resetting all models to ENABLED...") count = 0 - for category in ["checkpoints", "loras", "controlnets", "vae", "text_encoders", "diffusion_models", "clip", "embeddings"]: + for category, _default_folder_type in CATEGORY_SPECS: if category in registry: for item in registry[category]: item["enabled"] = True count += 1 - + save_registry(registry_path, registry) print(f"Done. {count} items enabled.") -if __name__ == "__main__": - if len(sys.argv) > 1: - if sys.argv[1] == "--sync": - sync_models() - elif sys.argv[1] == "--reset": + +def parse_args(): + parser = argparse.ArgumentParser( + description="Sync model_registry.json with actual disk paths and availability." + ) + mode = parser.add_mutually_exclusive_group() + mode.add_argument( + "--sync", + action="store_true", + help="Scan models, fix path/folder_type, and update enabled status.", + ) + mode.add_argument( + "--reset", + action="store_true", + help="Set enabled=true for all known registry entries.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Run sync logic without writing model_registry.json.", + ) + parser.add_argument( + "--no-prompt", + action="store_true", + help="Do not prompt for models_root when no saved root exists.", + ) + return parser.parse_args() + + +def run_interactive_menu(): + print("\n--- Model Registry Manager ---") + print("1. Sync + Repair Paths (write changes)") + print("2. Dry Run (preview changes only)") + print("3. Reset Registry (enable all)") + print("4. Exit") + + while True: + try: + choice = input("\nSelect option (1-4): ").strip() + except EOFError: + break + + if choice == "1": + sync_models(dry_run=False, prompt_for_root=True) + elif choice == "2": + sync_models(dry_run=True, prompt_for_root=True) + elif choice == "3": reset_registry() + elif choice == "4": + break else: - print("Usage: sync_registry.py [--sync | --reset]") + print("Invalid option.") + + +if __name__ == "__main__": + args = parse_args() + + if args.reset: + reset_registry() + elif args.sync or args.dry_run: + sync_models(dry_run=args.dry_run, prompt_for_root=not args.no_prompt) else: - print("\n--- Model Registry Manager ---") - print("1. Sync with Disk (Enable installed, Disable missing)") - print("2. Reset Registry (Enable ALL models)") - print("3. Exit") - - # Interactive mode: Loop until exit - while True: - try: - choice = input("\nSelect option (1-3): ").strip() - except EOFError: - break - - if choice == "1": - sync_models() - elif choice == "2": - reset_registry() - elif choice == "3": - break - else: - print("Invalid option.") - - + run_interactive_menu()