Skip to content

Commit b3baeb6

Browse files
committed
1.1.0
1 parent 3814143 commit b3baeb6

File tree

4 files changed

+431
-291
lines changed

4 files changed

+431
-291
lines changed

Local_Lora_Gallery.py

Lines changed: 174 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
from aiohttp import web
66
from nodes import LoraLoader, LoraLoaderModelOnly
77
import urllib.parse
8+
import hashlib
9+
import aiohttp
10+
import asyncio
11+
from urllib.parse import urlparse
812

913
NunchakuFluxLoraLoader = None
1014
is_nunchaku_available = False
@@ -24,6 +28,16 @@
2428
VIDEO_EXTENSIONS = ['.mp4', '.webm', '.mov', '.avi']
2529
IMAGE_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+
2741
def 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):
5165
load_presets = lambda: load_json_file(PRESETS_FILE)
5266
save_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")
67217
async 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):
204342
async 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:

README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
---
1414

1515
## 🇬🇧 English
16+
### Update Log (2025-10-02)
17+
* **Civitai Metadata Sync & Preview Downloader**:
18+
* Added a "Sync with Civitai" (☁️) button to each LoRA card. This feature calculates the model's hash to fetch metadata like trigger words and homepage URLs from Civitai.
19+
* When syncing, the plugin automatically downloads a small, web-optimized preview (image or video, 450px wide) and saves it locally alongside your LoRA file. This ensures fast, offline access to previews after the initial sync.
20+
1621
### Changelog (2025-09-12)
1722
* **Preset Management**: You can now save your favorite LoRA stacks as presets and load them with a single click.
1823
* **Folder Filtering**: A new dropdown menu allows you to filter LoRAs by their subfolder, making it easier to manage large collections.
@@ -92,6 +97,11 @@ It also features optional integration with **[comfyui-nunchaku](https://github.c
9297
-----
9398

9499
## 🇨🇳 中文
100+
### 更新日志 (2025-10-02)
101+
* **Civitai 元数据同步与预览图下载**:
102+
* 在每个 LoRA 卡片上增加了一个“与Civitai同步”(☁️) 按钮。此功能会计算模型哈希值,以从 Civitai 获取触发词和主页URL等元数据。
103+
* 同步时,插件会自动下载一个经网络优化的预览图(450px宽的图片或视频),并将其保存在您的 LoRA 文件旁边。这确保了在初次同步后,您可以在本地快速、离线地访问预览图。
104+
95105
### 更新日志 (2025-09-12)
96106
* **预设管理**: 现在您可以将常用的 LoRA 堆栈保存为预设,并一键加载。
97107
* **文件夹筛选**: 新增了文件夹筛选下拉菜单,可以按子文件夹显示 LoRA,便于管理庞大的模型库。

0 commit comments

Comments
 (0)