Skip to content

Commit d0aacb9

Browse files
committed
Config Builder View Lora CivitAI Data Buttons, Cleaner Manifest, Auto Card & Row Sizes In Grid, Fixed Config Builder Lora Always Combining Loras, Easier Lora UI Settings
1 parent 9306623 commit d0aacb9

14 files changed

Lines changed: 1179 additions & 427 deletions

Roadmap.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,16 @@ But seriously, after updating your code see if it works. If it didn't send the e
3535

3636
### **ComfyUI Ultimate Sampler Grid – Development Roadmap**
3737

38-
### Implement Settings For Lookahead Async Model Cacher
3938

40-
### Feature: LoRA Lookup From Builder UI. Get metadata, images, url, tags, & more to view quickly from builder in comfyui
39+
## Calculate diff - pack into manifest, read in card view at top of card stats, add to sort favorites by lora with lora diff name
4140

42-
### Fix: Manifest doesn't need lora omit triggers list in every item
41+
## Implement Settings For Lookahead Async Model Cacher
4342

43+
## Feature: LoRA Lookup From Builder UI. Get metadata, images, url, tags, & more to view quickly from builder in comfyui
44+
45+
## Fix: Manifest doesn't need lora omit triggers list in every item
46+
47+
## Add attention options, xformers, sdpa, sage, flash, etc, option for test all, test all should clear ram & vram between each test.
4448

4549

4650
#### **1. Skip Logic for Optional Inputs**

__init__.py

Lines changed: 146 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,74 @@
1717
CONFIGS_DIR = os.path.join(folder_paths.get_output_directory(), "ultimate-configs")
1818
os.makedirs(CONFIGS_DIR, exist_ok=True)
1919

20-
# --- API: CONFIG MANAGEMENT ---
20+
# --- CONFIG BUILDER JS DIRECTORY ---
21+
EXTENSION_DIR = os.path.dirname(os.path.abspath(__file__))
22+
CONFIG_BUILDER_JS_DIR = os.path.join(EXTENSION_DIR, "js")
23+
24+
# =============================================================================
25+
# SERVE CONFIG BUILDER JS FILES (NOT AUTO-LOADED)
26+
# =============================================================================
27+
28+
@server.PromptServer.instance.routes.get("/ultimate_config_sampler/js/{filename:.*}")
29+
async def serve_config_builder_js(request):
30+
r"""
31+
Serve JavaScript files from /js/ directory with cache-busting headers.
32+
Supports subdirectories with both / and \ (e.g., config-builder/utilities.js)
33+
"""
34+
try:
35+
filename = request.match_info['filename']
36+
print(f"[ConfigBuilder] Request for: {filename}")
37+
38+
# Security: prevent directory traversal with ..
39+
if '..' in filename or filename.startswith('/') or filename.startswith('\\'):
40+
print(f"[ConfigBuilder] FORBIDDEN: {filename}")
41+
return web.Response(status=403, text="Forbidden")
42+
43+
# Normalize path separators (convert backslashes to forward slashes)
44+
filename = filename.replace('\\', '/')
45+
46+
# Build full path and normalize it
47+
file_path = os.path.normpath(os.path.join(CONFIG_BUILDER_JS_DIR, filename))
48+
49+
# Double-check the resolved path is still within our JS directory
50+
normalized_base = os.path.normpath(CONFIG_BUILDER_JS_DIR)
51+
52+
if not file_path.startswith(normalized_base):
53+
print(f"[ConfigBuilder] FORBIDDEN - Outside base dir: {file_path}")
54+
return web.Response(status=403, text="Forbidden - path outside base directory")
55+
56+
if not os.path.exists(file_path):
57+
print(f"[ConfigBuilder] NOT FOUND: {file_path}")
58+
return web.Response(status=404, text=f"File not found: {filename}")
59+
60+
print(f"[ConfigBuilder] Reading file: {file_path}")
61+
with open(file_path, 'r', encoding='utf-8') as f:
62+
content = f.read()
63+
64+
print(f"[ConfigBuilder] File read successfully, {len(content)} bytes")
65+
66+
# Return with cache-busting headers
67+
return web.Response(
68+
text=content,
69+
content_type='application/javascript',
70+
headers={
71+
'Cache-Control': 'no-cache, no-store, must-revalidate',
72+
'Pragma': 'no-cache',
73+
'Expires': '0'
74+
}
75+
)
76+
77+
except Exception as e:
78+
print(f"[ConfigBuilder] ERROR: {e}")
79+
import traceback
80+
traceback.print_exc()
81+
return web.Response(status=500, text=f"Server error: {str(e)}")
82+
83+
84+
# =============================================================================
85+
# API: CONFIG MANAGEMENT
86+
# =============================================================================
87+
2188
@server.PromptServer.instance.routes.get("/configbuilder/list_configs")
2289
async def list_configs(request):
2390
try:
@@ -82,7 +149,10 @@ async def load_config(request):
82149
return web.Response(status=500, text=str(e))
83150

84151

85-
# --- API: DELETE SESSION ---
152+
# =============================================================================
153+
# API: DELETE SESSION
154+
# =============================================================================
155+
86156
@server.PromptServer.instance.routes.post("/config_tester/delete_session")
87157
async def delete_session(request):
88158
try:
@@ -108,7 +178,10 @@ async def delete_session(request):
108178
except Exception as e:
109179
return web.Response(status=500, text=str(e))
110180

111-
# --- API: SAVE CHANGES (Optimized - Only Changed Items) ---
181+
# =============================================================================
182+
# API: SAVE CHANGES (Optimized - Only Changed Items)
183+
# =============================================================================
184+
112185
@server.PromptServer.instance.routes.post("/config_tester/save_changes")
113186
async def save_changes(request):
114187
"""
@@ -175,7 +248,10 @@ async def save_changes(request):
175248
traceback.print_exc()
176249
return web.Response(status=500, text=str(e))
177250

178-
# --- API: SAVE MANIFEST (Legacy - Full Save) ---
251+
# =============================================================================
252+
# API: SAVE MANIFEST (Legacy - Full Save)
253+
# =============================================================================
254+
179255
@server.PromptServer.instance.routes.post("/config_tester/save_manifest")
180256
async def save_manifest(request):
181257
"""
@@ -197,72 +273,85 @@ async def save_manifest(request):
197273
return web.Response(status=400, text="Missing session_name or manifest")
198274

199275
base_dir = os.path.join(folder_paths.get_output_directory(), "benchmarks", session_name)
200-
os.makedirs(base_dir, exist_ok=True)
201276
manifest_path = os.path.join(base_dir, "manifest.json")
202277

203-
# CRITICAL FIX: Merge with disk version to preserve generation's new items
204-
try:
205-
if os.path.exists(manifest_path):
206-
# Load what's currently on disk (may have new items from generation)
278+
# --- MERGE STRATEGY: Preserve server data ---
279+
# 1. Load server manifest (has newest images)
280+
server_manifest = None
281+
if os.path.exists(manifest_path):
282+
try:
207283
with open(manifest_path, "r") as f:
208-
disk_manifest = json.load(f)
209-
210-
# Create lookup of dashboard items by ID
211-
dashboard_items_dict = {
212-
item.get("id"): item
213-
for item in manifest_data.get("items", [])
214-
if "id" in item
215-
}
216-
217-
# Find items on disk that aren't in dashboard (newly generated)
218-
new_items = []
219-
for disk_item in disk_manifest.get("items", []):
220-
item_id = disk_item.get("id")
221-
if item_id and item_id not in dashboard_items_dict:
222-
# This item was generated after dashboard loaded
223-
new_items.append(disk_item)
224-
225-
# Add new items to dashboard's manifest
226-
if new_items:
227-
print(f"[ConfigTester] 📄 Preserving {len(new_items)} newly generated items not in dashboard")
228-
manifest_data["items"] = new_items + manifest_data.get("items", [])
229-
230-
# Preserve meta from disk (has latest settings)
231-
if "meta" in disk_manifest:
232-
# Keep user's changes but preserve generation settings
233-
manifest_data["meta"] = disk_manifest["meta"]
234-
235-
except Exception as e:
236-
print(f"[ConfigTester] ⚠️ Could not merge with disk manifest: {e}")
237-
# Continue with save anyway - dashboard data is more important than merge
238-
239-
# Save the merged manifest
284+
server_manifest = json.load(f)
285+
except:
286+
pass
287+
288+
if server_manifest:
289+
# Build lookup of items by ID from dashboard
290+
dashboard_items = {item.get("id"): item for item in manifest_data.get("items", [])}
291+
292+
# Merge: Update existing items, keep new items
293+
merged_items = []
294+
for server_item in server_manifest.get("items", []):
295+
item_id = server_item.get("id")
296+
if item_id in dashboard_items:
297+
# Item exists in dashboard: merge updates
298+
dashboard_item = dashboard_items[item_id]
299+
# Preserve server's metadata but update user actions
300+
merged_item = server_item.copy()
301+
merged_item["favorited"] = dashboard_item.get("favorited", False)
302+
merged_item["rejected"] = dashboard_item.get("rejected", False)
303+
merged_item["note"] = dashboard_item.get("note", "")
304+
merged_items.append(merged_item)
305+
else:
306+
# NEW item from server (generation added it): keep as-is
307+
merged_items.append(server_item)
308+
309+
# Update manifest
310+
manifest_data["items"] = merged_items
311+
312+
# Preserve server's meta
313+
if "meta" in server_manifest:
314+
manifest_data["meta"] = server_manifest["meta"]
315+
316+
# Save merged manifest
317+
os.makedirs(base_dir, exist_ok=True)
240318
with open(manifest_path, "w") as f:
241319
json.dump(manifest_data, f, indent=4)
242-
243-
print("Save Manifest at init")
320+
244321
return web.Response(status=200, text="Saved")
322+
245323
except Exception as e:
246324
print(f"[ConfigTester] Error saving manifest: {e}")
325+
import traceback
326+
traceback.print_exc()
247327
return web.Response(status=500, text=str(e))
248328

249-
# --- API: FETCH SESSION HTML ---
329+
# =============================================================================
330+
# API: GET SESSION HTML
331+
# =============================================================================
332+
250333
@server.PromptServer.instance.routes.post("/config_tester/get_session_html")
251334
async def get_session_html(request):
335+
"""
336+
Dynamically generate HTML for a session.
337+
This allows the dashboard to load without triggering a workflow execution.
338+
"""
252339
try:
253340
data = await request.json()
254341
session_name = data.get("session_name")
255-
node_id = data.get("node_id", "0") # Fallback ID
342+
node_id = data.get("node_id")
256343

257-
# --- sanitize ---
258344
if session_name:
259345
session_name = re.sub(r'[^\w\-]', '', session_name)
260-
346+
347+
if not session_name:
348+
return web.Response(status=400, text="Missing session_name")
349+
261350
base_dir = os.path.join(folder_paths.get_output_directory(), "benchmarks", session_name)
262351
manifest_path = os.path.join(base_dir, "manifest.json")
263352

264353
if not os.path.exists(manifest_path):
265-
return web.Response(status=404, text=f"Session '{session_name}' not found.")
354+
return web.Response(status=404, text=f"Session '{session_name}' not found")
266355

267356
with open(manifest_path, "r") as f:
268357
manifest = json.load(f)
@@ -274,7 +363,10 @@ async def get_session_html(request):
274363
except Exception as e:
275364
return web.Response(status=500, text=str(e))
276365

277-
# --- API: EXPORT FAVORITES ---
366+
# =============================================================================
367+
# API: EXPORT FAVORITES
368+
# =============================================================================
369+
278370
@server.PromptServer.instance.routes.post("/config_tester/export_favorites")
279371
async def export_favorites(request):
280372
"""
@@ -442,7 +534,10 @@ async def export_favorites(request):
442534
traceback.print_exc()
443535
return web.Response(status=500, text=str(e))
444536

445-
# --- MAPPINGS ---
537+
# =============================================================================
538+
# NODE MAPPINGS
539+
# =============================================================================
540+
446541
NODE_CLASS_MAPPINGS = {
447542
"UltimateSamplerGrid": SamplerGridTester,
448543
"UltimateGridDashboard": SamplerConfigDashboardViewer,
@@ -453,7 +548,7 @@ async def export_favorites(request):
453548
NODE_DISPLAY_NAME_MAPPINGS = {
454549
"UltimateSamplerGrid": "Ultimate Sampler Grid (Generator)",
455550
"UltimateGridDashboard": "Ultimate Grid Dashboard (Viewer)",
456-
"UltimateConfigBuilder": "Ultimate Config Builder (WIP)",
551+
"UltimateConfigBuilder": "Ultimate Config Builder",
457552
"SmartJSONText": "Smart JSON Text",
458553
}
459554

batch_encoding.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import comfy.model_management
99

1010

11-
def batch_encode_with_cache(clip_model, prompts, cond_cache, prompt_type="positive", batch_size=16, clip_skip=0):
11+
def batch_encode_with_cache(clip_model, prompts, cond_cache, prompt_type="positive", batch_size=64, clip_skip=0):
1212
"""
1313
Batch encode prompts while checking persistent cache first.
1414
Only encodes prompts that aren't already cached.

0 commit comments

Comments
 (0)