Sanitize registry sync and clean shared model URL metadata

This commit is contained in:
Geekatplay Dev
2026-02-12 20:48:01 -07:00
parent d02cec8fdd
commit 3feaaf0c57
2 changed files with 568 additions and 213 deletions
+82 -82
View File
@@ -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
}
]
}
}
+486 -131
View File
@@ -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()