55from aiohttp import web
66from nodes import LoraLoader , LoraLoaderModelOnly
77import urllib .parse
8+ import hashlib
9+ import aiohttp
10+ import asyncio
11+ from urllib .parse import urlparse
812
913NunchakuFluxLoraLoader = None
1014is_nunchaku_available = False
2428VIDEO_EXTENSIONS = ['.mp4' , '.webm' , '.mov' , '.avi' ]
2529IMAGE_EXTENSIONS = ['.png' , '.jpg' , '.jpeg' , '.webp' , '.gif' ]
2630
31+ def calculate_sha256 (filepath ):
32+ """Calculates the SHA256 hash of a file efficiently."""
33+ if not os .path .exists (filepath ):
34+ return None
35+ hash_sha256 = hashlib .sha256 ()
36+ with open (filepath , "rb" ) as f :
37+ for chunk in iter (lambda : f .read (4096 ), b"" ):
38+ hash_sha256 .update (chunk )
39+ return hash_sha256 .hexdigest ()
40+
2741def load_json_file (file_path , default_data = {}):
2842 if not os .path .exists (file_path ):
2943 return default_data
@@ -51,17 +65,153 @@ def save_json_file(data, file_path):
5165load_presets = lambda : load_json_file (PRESETS_FILE )
5266save_presets = lambda data : save_json_file (data , PRESETS_FILE )
5367
54- def get_lora_preview_asset (lora_name ):
55- """Finds a preview asset (image or video) for a given LoRA."""
68+ def get_lora_preview_asset_info (lora_name ):
69+ """Finds a preview asset (image or video) for a given LoRA and returns its info ."""
5670 lora_path = folder_paths .get_full_path ("loras" , lora_name )
5771 if lora_path is None :
58- return None
72+ return None , "none"
5973 base_name , _ = os .path .splitext (lora_path )
6074
6175 for ext in IMAGE_EXTENSIONS + VIDEO_EXTENSIONS :
62- if os .path .exists (base_name + ext ):
63- return os .path .basename (base_name + ext )
64- return None
76+ preview_path = base_name + ext
77+ if os .path .exists (preview_path ):
78+ preview_filename = os .path .basename (preview_path )
79+ encoded_lora_name = urllib .parse .quote_plus (lora_name )
80+ encoded_filename = urllib .parse .quote_plus (preview_filename )
81+ url = f"/localloragallery/preview?filename={ encoded_filename } &lora_name={ encoded_lora_name } "
82+
83+ preview_type = "none"
84+ if ext .lower () in VIDEO_EXTENSIONS :
85+ preview_type = "video"
86+ elif ext .lower () in IMAGE_EXTENSIONS :
87+ preview_type = "image"
88+
89+ return url , preview_type
90+
91+ return None , "none"
92+
93+ @server .PromptServer .instance .routes .post ("/localloragallery/sync_civitai" )
94+ async def sync_civitai_metadata (request ):
95+ try :
96+ data = await request .json ()
97+ lora_name = data .get ("lora_name" )
98+ if not lora_name :
99+ return web .json_response ({"status" : "error" , "message" : "Missing lora_name" }, status = 400 )
100+
101+ lora_full_path = folder_paths .get_full_path ("loras" , lora_name )
102+ if not lora_full_path :
103+ return web .json_response ({"status" : "error" , "message" : "LoRA file not found" }, status = 404 )
104+
105+ metadata = load_metadata ()
106+ lora_meta = metadata .get (lora_name , {})
107+
108+ model_hash = lora_meta .get ('hash' )
109+ if not model_hash :
110+ print (f"Local Lora Gallery: Calculating hash for { lora_name } ..." )
111+ model_hash = calculate_sha256 (lora_full_path )
112+ if model_hash :
113+ lora_meta ['hash' ] = model_hash
114+ metadata [lora_name ] = lora_meta
115+ save_metadata (metadata )
116+ else :
117+ return web .json_response ({"status" : "error" , "message" : "Failed to calculate hash" }, status = 500 )
118+
119+ civitai_version_url = f"https://civitai.com/api/v1/model-versions/by-hash/{ model_hash } "
120+ async with aiohttp .ClientSession () as session :
121+ async with session .get (civitai_version_url ) as response :
122+ if response .status != 200 :
123+ return web .json_response ({"status" : "error" , "message" : f"Civitai API (version) returned { response .status } . Model not found or API error." }, status = response .status )
124+
125+ civitai_version_data = await response .json ()
126+ model_id = civitai_version_data .get ('modelId' )
127+ if not model_id :
128+ return web .json_response ({"status" : "error" , "message" : "Could not find modelId in Civitai API response." }, status = 500 )
129+
130+ # civitai_model_url = f"https://civitai.com/api/v1/models/{model_id}"
131+ # async with session.get(civitai_model_url) as response:
132+ # if response.status != 200:
133+ # print(f"Local Lora Gallery: Warning - Could not fetch model details for tags. Status: {response.status}")
134+ # civitai_model_data = {}
135+ # else:
136+ # civitai_model_data = await response.json()
137+
138+ images = civitai_version_data .get ('images' , [])
139+ if not images :
140+ print ("Local Lora Gallery: No preview images found on Civitai, but will save other metadata." )
141+ else :
142+ preview_media = next ((img for img in images if img .get ('type' ) == 'image' ), images [0 ])
143+ preview_url = preview_media .get ('url' )
144+ is_video = preview_media .get ('type' ) == 'video'
145+
146+ try :
147+ if is_video :
148+ if '/original=true/' in preview_url :
149+ temp_url = preview_url .replace ('/original=true/' , '/transcode=true,width=450,optimized=true/' )
150+ final_url = os .path .splitext (temp_url )[0 ] + '.webm'
151+ else :
152+ url_obj = urlparse (preview_url )
153+ path_parts = url_obj .path .split ('/' )
154+ filename = path_parts .pop ()
155+ filename_base = os .path .splitext (filename )[0 ]
156+ new_path = f"{ '/' .join (path_parts )} /transcode=true,width=450,optimized=true/{ filename_base } .webm"
157+ final_url = url_obj ._replace (path = new_path ).geturl ()
158+ file_ext = '.webm'
159+ else :
160+ if '/original=true/' in preview_url :
161+ final_url = preview_url .replace ('/original=true/' , '/width=450/' )
162+ else :
163+ final_url = preview_url .replace ('/width=\d+/' , '/width=450/' ) if '/width=' in preview_url else preview_url .replace (urlparse (preview_url ).path , f"/width=450{ urlparse (preview_url ).path } " )
164+
165+ path = urlparse (final_url ).path
166+ file_ext = os .path .splitext (path )[1 ]
167+ if not file_ext or file_ext .lower () not in IMAGE_EXTENSIONS :
168+ file_ext = '.jpg'
169+ except Exception as e :
170+ print (f"Local Lora Gallery: Failed to parse or modify URL '{ preview_url } '. Error: { e } " )
171+ final_url = preview_url
172+ file_ext = '.jpg' if not is_video else '.mp4'
173+
174+ lora_dir = os .path .dirname (lora_full_path )
175+ lora_basename = os .path .splitext (os .path .basename (lora_full_path ))[0 ]
176+ save_path = os .path .join (lora_dir , lora_basename + file_ext )
177+
178+ async with session .get (final_url ) as download_response :
179+ if download_response .status != 200 :
180+ print (f"Local Lora Gallery: Warning - Failed to download preview from { final_url } . Proceeding without preview." )
181+ else :
182+ with open (save_path , 'wb' ) as f :
183+ while True :
184+ chunk = await download_response .content .read (8192 )
185+ if not chunk : break
186+ f .write (chunk )
187+ print (f"Local Lora Gallery: Successfully downloaded preview to '{ save_path } '" )
188+
189+ trained_words = civitai_version_data .get ('trainedWords' , [])
190+ if trained_words :
191+ lora_meta ['trigger_words' ] = ", " .join (trained_words )
192+
193+ lora_meta ['download_url' ] = f"https://civitai.com/models/{ model_id } "
194+
195+ # tags = set(lora_meta.get('tags', []))
196+ # if 'tags' in civitai_model_data:
197+ # for tag in civitai_model_data['tags']:
198+ # tags.add(tag)
199+ # lora_meta['tags'] = sorted(list(tags))
200+
201+ metadata [lora_name ] = lora_meta
202+ save_metadata (metadata )
203+
204+ new_local_url , new_preview_type = get_lora_preview_asset_info (lora_name )
205+
206+ return web .json_response ({
207+ "status" : "ok" ,
208+ "metadata" : { "preview_url" : new_local_url , "preview_type" : new_preview_type , ** lora_meta }
209+ })
210+
211+ except Exception as e :
212+ import traceback
213+ print (f"Error in sync_civitai_metadata: { traceback .format_exc ()} " )
214+ return web .json_response ({"status" : "error" , "message" : str (e )}, status = 500 )
65215
66216@server .PromptServer .instance .routes .get ("/localloragallery/get_presets" )
67217async def get_presets (request ):
@@ -164,23 +314,11 @@ async def get_loras_endpoint(request):
164314 lora_info_list = []
165315 for lora in paginated_loras :
166316 lora_meta = metadata .get (lora , {})
167- preview_filename = get_lora_preview_asset (lora )
168- preview_url = ""
169- preview_type = "none"
170-
171- if preview_filename :
172- _ , ext = os .path .splitext (preview_filename )
173- if ext .lower () in VIDEO_EXTENSIONS :
174- preview_type = "video"
175- elif ext .lower () in IMAGE_EXTENSIONS :
176- preview_type = "image"
177-
178- encoded_lora_name = urllib .parse .quote (lora )
179- preview_url = f"/localloragallery/preview?filename={ preview_filename } &lora_name={ encoded_lora_name } "
317+ preview_url , preview_type = get_lora_preview_asset_info (lora )
180318
181319 lora_info_list .append ({
182320 "name" : lora ,
183- "preview_url" : preview_url ,
321+ "preview_url" : preview_url or "" ,
184322 "preview_type" : preview_type ,
185323 "tags" : lora_meta .get ('tags' , []),
186324 "trigger_words" : lora_meta .get ('trigger_words' , '' ),
@@ -204,14 +342,24 @@ async def get_loras_endpoint(request):
204342async def get_preview_image (request ):
205343 filename = request .query .get ('filename' )
206344 lora_name = request .query .get ('lora_name' )
345+
207346 if not filename or not lora_name or ".." in filename or "/" in filename or "\\ " in filename :
208347 return web .Response (status = 403 )
348+
209349 try :
210- lora_full_path = folder_paths .get_full_path ("loras" , lora_name )
350+ lora_name_decoded = urllib .parse .unquote_plus (lora_name )
351+ filename_decoded = urllib .parse .unquote_plus (filename )
352+
353+ lora_full_path = folder_paths .get_full_path ("loras" , lora_name_decoded )
211354 if not lora_full_path :
212- return web .Response (status = 404 )
213- image_path = os .path .join (os .path .dirname (lora_full_path ), filename )
214- return web .FileResponse (image_path ) if os .path .exists (image_path ) else web .Response (status = 404 )
355+ return web .Response (status = 404 , text = f"Lora '{ lora_name_decoded } ' not found." )
356+
357+ image_path = os .path .join (os .path .dirname (lora_full_path ), filename_decoded )
358+ if os .path .exists (image_path ):
359+ return web .FileResponse (image_path )
360+ else :
361+ return web .Response (status = 404 , text = f"Preview '{ filename_decoded } ' not found." )
362+
215363 except Exception as e :
216364 return web .json_response ({"error" : str (e )}, status = 500 )
217365
@@ -318,7 +466,7 @@ def INPUT_TYPES(cls):
318466 RETURN_NAMES = ("MODEL" , "CLIP" , "trigger_words" )
319467
320468 FUNCTION = "load_loras"
321- CATEGORY = "📜Asset Gallery/Local "
469+ CATEGORY = "📜Asset Gallery/Loras "
322470
323471 def load_loras (self , model , clip , unique_id , selection_data = "[]" , ** kwargs ):
324472 try :
@@ -384,7 +532,7 @@ def INPUT_TYPES(cls):
384532 RETURN_NAMES = ("MODEL" , "trigger_words" )
385533
386534 FUNCTION = "load_loras"
387- CATEGORY = "📜Asset Gallery/Local "
535+ CATEGORY = "📜Asset Gallery/Loras "
388536
389537 def load_loras (self , model , unique_id , selection_data = "[]" , ** kwargs ):
390538 try :
0 commit comments